From f0deaea6fb3535b5c6dd5e256d067fb490b4a893 Mon Sep 17 00:00:00 2001 From: chee Date: Thu, 13 Jul 2017 03:42:51 +0100 Subject: [PATCH] do a git commit where it adds all the files because up until now thhe files were just on my computer, but i would liek it if i could store the files in decentralised repositories, perhaps one on the proprietary but apparently universally trusted software web site of the privately owned company Github, Inc. and perhaps one on my own internet computer and maybe another on another computer look i'm not sure you're getting it, like it'll be the same files but then they'll be in different places and people can use and download them and make changes to them and then share those changes but then i've licensed this MIT by accident so they don't have to share their changes because freedom apparently means letting people restrict your freedom because you have to let them be free to do that otherwise you are restricting their freedom or something anyway i'll download the cpmputer and put all the other computers and the internet and baskets waeving baskets out of hair and leather and little piles of leather and flowers and butter and happy dogs and happy go lucky farming folk and what's the story and help and handshakes and i'm sorry and i can't help and i'm sorry that i can't help and i'm not ok i'm super fine thanks i'm doing really well, i'm doing super well and i'm fine , thankyou yes you too thanksb bye --- .gitignore | 1 + README.md | 74 ++++++ index.js | 173 +++++++++++++ package-lock.json | 427 ++++++++++++++++++++++++++++++++ package.json | 14 ++ play/cli.js | 51 ++++ play/go.js | 19 ++ play/promise/blue.js | 7 + play/socket.js | 38 +++ play/socket/? | 1 + play/socket/http.js | 17 ++ play/socket/public/index.html | 48 ++++ play/socket/rainbow-gradient.js | 26 ++ play/socket/rainbow.js | 20 ++ play/socket/vap.js | 11 + play/util/sleep.js | 1 + 16 files changed, 928 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 index.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 play/cli.js create mode 100644 play/go.js create mode 100644 play/promise/blue.js create mode 100644 play/socket.js create mode 100644 play/socket/? create mode 100644 play/socket/http.js create mode 100644 play/socket/public/index.html create mode 100644 play/socket/rainbow-gradient.js create mode 100644 play/socket/rainbow.js create mode 100644 play/socket/vap.js create mode 100644 play/util/sleep.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ccbe46 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..d58d574 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +## smal lib for kasa veho (and tiktek?) smart bluetooth lightblibs + +small library for kasa veho lightbulbs and others like it. + +could not have finished this if it were not for +[https://mjg59.dreamwidth.org/43722.html](this blog post) and +[https://github.com/mjg59/python-tikteck](this python library) by +[https://twitter.com/mjg59](@mjg59). so thank you a billion times for writing +that and digging into that disassembled .so while i dug into nothing but shallow +graves + +you'll note that this is nearly a carbon copy of their library + +i couldn't run that because i wasn't on a loonix machine, so now this has +happened and should work on lanux and macos maybe even windows if the stars +align. relies on the wnoderful [noble](https://github.com/sandeepmistry/noble) + +## use: + +* with a bulb you know + +```js +const {pair, setColor} = require('kasa') +pair({ + name: 'Smart Light', + password: 'password obtained from adb lolcat', + address: 'ca:fe:0f:be:ef' // address of light +}, dispatch => { + // red, green, blue, brightness + dispatch(setColor(0xff, 0x2a, 0x50, 0xff)) +}) +``` + +* with a bulb you've never met: + +```js +const {discover, pair, setColor} = require('kasa') +discover({ + name: 'Smart Light', // defaults to Smart Light + password: 'get this from adb lolcat' +}, blub => { + console.log('a real lootblub!', blub.address) + pair(blub, dispatch => { + dispatch(setColor(0x33, 0xcc, 0xff, 0xff)) + }) +}) +``` + +* there is also a promise api (note that you will only get the first bulb + discovered this way, but the callback will keep calling you up with every + light it finds) + +```js +const {discover, pair, setColor} = require('kasa') + +discover() + .then(pair) + .then(dispatch => dispatch(setColor(0xff, 0xff, 0xff, 0xff))) +``` + +there are some silly examples in the directory called `play/` + +## todo: + +* clean up the code (vague!) +* fix vague todo items (which?) +* find out why my tongue has lumps on it +* revise the api when i have had literally any sleep in the past 3 years +* get rich, die old +* get to the top of Tom's top 8 +* wear sunscreen +* learn the xaphoon +* see how lb is doin +* switch to crypto-js and pull parts out and see if this can run in web browsers diff --git a/index.js b/index.js new file mode 100644 index 0000000..6825db9 --- /dev/null +++ b/index.js @@ -0,0 +1,173 @@ +const crypto = require('crypto') +const noble = require('noble') + +const nobleReady = new Promise(resolve => + noble.on('stateChange', state => { + state === 'poweredOn' && resolve() + }) +) + +const range = to => Array(to).fill().map((_, i) => i) + +function encrypt (key, data) { + key = Buffer.from(key) + key.reverse() + data = Buffer.from(data) + data.reverse() + const cipher = crypto.createCipheriv('aes-128-ecb', key, Buffer.from([])) + const encryptedData = cipher.update(data).reverse() + return encryptedData +} + +function generateSk (name, password, data1, data2) { + name = Buffer.from(name.padEnd(16, '\u0000')) + password = Buffer.from(password.padEnd(16, '\u0000')) + const key = [] + name.forEach((byte, index) => { + key.push(byte ^ password[index]) + }) + const data = [...data1.slice(0, 8), ...data2.slice(0, 8)] + return encrypt(key, data) +} + +function encryptKey (name, password, data) { + name = Buffer.from(name.padEnd(16, '\u0000')) + password = Buffer.from(password.padEnd(16, '\u0000')) + const key = [] + key.forEach.call(name, (byte, index) => { + key.push(byte ^ password[index]) + }) + return encrypt(data, key) +} + +// mutate me mor +function encryptPacket (sk, mac, packet) { + let tmp = [...mac.slice(0, 4), 0x01, ...packet.slice(0, 3), 15, 0, 0, 0, 0, 0, 0, 0] + tmp = encrypt(sk, tmp) + + range(15).forEach(i => { + tmp[i] = tmp[i] ^ packet[i + 5] + }) + + tmp = encrypt(sk, tmp) + + range(2).forEach(i => { + packet[i + 3] = tmp[i] + }) + + tmp = [0, ...mac.slice(0, 4), 0x01, ...packet.slice(0, 3), 0, 0, 0, 0, 0, 0, 0] + + tmp2 = [] + + range(15).forEach(i => { + if (i === 0) { + tmp2 = encrypt(sk, tmp) + tmp[0] = tmp[0] + 1 + } + packet[i + 5] ^= tmp2[i] + }) + + return Buffer.from(packet) +} + +function connect (light, callback) { + return light.connect(() => callback(light)) +} + +function discover (options = {}, callback) { + if (arguments.length === 1) { + if (typeof arguments[0] === 'function') { + callback = arguments[0] + options = {} + } + } + const {name = 'Smart Light', password = '234', address} = options + const discovery = new Promise(resolve => { + noble.on('discover', thing => { + if (thing.advertisement.localName !== name) return + thing.password = password + if (address) { + address === thing.address && connect(thing, (...args) => { + callback && callback(...args) + resolve(...args) + }) + } else { + connect(thing, (...args) => { + callback && callback(...args) + resolve(...args) + }) + } + }) + }) + noble.startScanning() + return discovery +} + +function pair (light, callback) { + let packetCount = Math.random() * 0xffff | 0 + const name = light.name || light.advertisement.localName + const password = light.password + const mac = light.address + return new Promise(resolve => { + light.discoverAllServicesAndCharacteristics(() => { + const commandChar = light.services[1].characteristics[1] + const pairChar = light.services[1].characteristics[3] + const data = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0, 0, 0, 0, 0, 0, 0, 0] + const encryptedKey = encryptKey(name, password, data) + const packet = [0x0c] + .concat(data.slice(0, 8)) + .concat([...encryptedKey].slice(0, 8)) + + pairChar.write(new Buffer(packet), true, () => { + pairChar.read((error, received) => { + const sk = generateSk(name, password, data.slice(0, 8), received.slice(1, 9)) + function dispatch ([id, command, data], callback) { + const packet = Array(20).fill(0) + packet[0] = packetCount & 0xff + packet[1] = packetCount >> 8 & 0xff + packet[5] = id & 0xff + packet[6] = id & 0xff | 0x80 + packet[7] = command + packet[8] = 0x69 + packet[9] = 0x69 + packet[10] = data[0] + packet[11] = data[1] + packet[12] = data[2] + packet[13] = data[3] + const macKey = Buffer.from(mac.split(':').slice(0, 6).reverse().map(n => parseInt(n, 16))) + const encryptedPacket = encryptPacket(sk, macKey, [...packet]) + packetCount = packetCount > 0xffff ? 1 : packetCount + 1 + return new Promise(resolve => { + commandChar.write(encryptedPacket, false, (...args) => { + callback && callback(...args) + resolve(...args) + }) + }) + } + callback && callback(dispatch) + resolve(dispatch) + }) + }) + }) + }) +} + +function sendCommand (command, ...args) { + return [0xffff, command, args] +} + +// red, green, blue, brightness +function setColor (...values) { + return sendCommand(0xc1, ...values) +} + +function setDefaultColor (...values) { + return sendCommand(0xc4, ...values) +} + +module.exports = { + discover: (...args) => nobleReady.then(() => discover(...args)), + pair, + setColor, + setDefaultColor +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d9f9179 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,427 @@ +{ + "name": "kasa", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "accepts": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz", + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=", + "optional": true, + "requires": { + "mime-types": "2.1.15", + "negotiator": "0.6.1" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "optional": true + }, + "body-parser": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.17.2.tgz", + "integrity": "sha1-+IkqvI+eYn1Crtr7yma/WrmRBO4=", + "optional": true, + "requires": { + "bytes": "2.4.0", + "content-type": "1.0.2", + "debug": "2.6.7", + "depd": "1.1.0", + "http-errors": "1.6.1", + "iconv-lite": "0.4.15", + "on-finished": "2.3.0", + "qs": "6.4.0", + "raw-body": "2.2.0", + "type-is": "1.6.15" + } + }, + "bplist-parser": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.0.6.tgz", + "integrity": "sha1-ONo0cYF9+dRKs4kuJ3B7u9daEbk=", + "optional": true + }, + "bytes": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz", + "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk=" + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "optional": true + }, + "content-type": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz", + "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=", + "optional": true + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "optional": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "optional": true + }, + "debug": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz", + "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz", + "integrity": "sha1-b2Ma7zNtbEY2K1F2QETOIWvjwFE=" + }, + "express": { + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.15.3.tgz", + "integrity": "sha1-urZdDwOqgMNYQIly/HAPkWlEtmI=", + "optional": true, + "requires": { + "accepts": "1.3.3", + "array-flatten": "1.1.1", + "content-disposition": "0.5.2", + "content-type": "1.0.2", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.7", + "depd": "1.1.0", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "etag": "1.8.0", + "finalhandler": "1.0.3", + "fresh": "0.5.0", + "merge-descriptors": "1.0.1", + "methods": "1.1.2", + "on-finished": "2.3.0", + "parseurl": "1.3.1", + "path-to-regexp": "0.1.7", + "proxy-addr": "1.1.4", + "qs": "6.4.0", + "range-parser": "1.2.0", + "send": "0.15.3", + "serve-static": "1.12.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1", + "type-is": "1.6.15", + "utils-merge": "1.0.0", + "vary": "1.1.1" + } + }, + "finalhandler": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.3.tgz", + "integrity": "sha1-70fneVDpmXgOhgIqVg4yF+DQzIk=", + "optional": true, + "requires": { + "debug": "2.6.7", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "parseurl": "1.3.1", + "statuses": "1.3.1", + "unpipe": "1.0.0" + } + }, + "forwarded": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz", + "integrity": "sha1-Ge+YdMSuHCl7zweP3mOgm2aoQ2M=", + "optional": true + }, + "fresh": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz", + "integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44=" + }, + "http-errors": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz", + "integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=", + "requires": { + "depd": "1.1.0", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": "1.3.1" + } + }, + "iconv-lite": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", + "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.3.0.tgz", + "integrity": "sha1-HgOlL9rYOou7KyXL9JmLTP/NPew=", + "optional": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "optional": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "optional": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "optional": true + }, + "mime": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz", + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=" + }, + "mime-db": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" + }, + "mime-types": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "requires": { + "mime-db": "1.27.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nan": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=", + "optional": true + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "optional": true + }, + "noble": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/noble/-/noble-1.8.1.tgz", + "integrity": "sha1-7+iAgStyUa+qrbT+g3U8EwKmc/Y=", + "requires": { + "bplist-parser": "0.0.6", + "debug": "2.2.0", + "xpc-connection": "0.1.4" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "requires": { + "ms": "0.7.1" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" + } + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", + "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "optional": true + }, + "proxy-addr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.4.tgz", + "integrity": "sha1-J+VF9pYKRKYn2bREZ+NcG2tM4vM=", + "optional": true, + "requires": { + "forwarded": "0.1.0", + "ipaddr.js": "1.3.0" + } + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "optional": true + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.2.0.tgz", + "integrity": "sha1-mUl2z2pQlqQRYoQEkvC9xdbn+5Y=", + "optional": true, + "requires": { + "bytes": "2.4.0", + "iconv-lite": "0.4.15", + "unpipe": "1.0.0" + } + }, + "send": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/send/-/send-0.15.3.tgz", + "integrity": "sha1-UBP5+ZAj31DRvZiSwZ4979HVMwk=", + "requires": { + "debug": "2.6.7", + "depd": "1.1.0", + "destroy": "1.0.4", + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "etag": "1.8.0", + "fresh": "0.5.0", + "http-errors": "1.6.1", + "mime": "1.3.4", + "ms": "2.0.0", + "on-finished": "2.3.0", + "range-parser": "1.2.0", + "statuses": "1.3.1" + } + }, + "serve-static": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.3.tgz", + "integrity": "sha1-n0uhni8wMMVH+K+ZEHg47DjVseI=", + "optional": true, + "requires": { + "encodeurl": "1.0.1", + "escape-html": "1.0.3", + "parseurl": "1.3.1", + "send": "0.15.3" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + }, + "tinycolor2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", + "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=", + "optional": true + }, + "tinygradient": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-0.3.1.tgz", + "integrity": "sha1-LZ5PvjSMSX7RHih6QzaDWz/RECI=", + "optional": true, + "requires": { + "tinycolor2": "1.4.1" + } + }, + "type-is": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "optional": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "2.1.15" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "optional": true + }, + "utils-merge": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=", + "optional": true + }, + "vary": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz", + "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=", + "optional": true + }, + "xpc-connection": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/xpc-connection/-/xpc-connection-0.1.4.tgz", + "integrity": "sha1-3Nf6oq7Gt6bhjMXdrQQvejTHcVY=", + "optional": true, + "requires": { + "nan": "2.6.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bf2a183 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "kasa", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "noble": "^1.8.1" + }, + "optionalDependencies": { + "body-parser": "^1.17.2", + "express": "^4.15.3", + "tinygradient": "^0.3.1" + } +} diff --git a/play/cli.js b/play/cli.js new file mode 100644 index 0000000..a634fe9 --- /dev/null +++ b/play/cli.js @@ -0,0 +1,51 @@ +const {createInterface} = require('readline') +const {discover, pair, setColor} = require('..') + +const blubs = [] +const paired = [] + +const prompt = 'hex color: ' +const firstPrompt = `pls give me a ${prompt.slice(0, -2)} (like #ff2a50): ` +const readline = createInterface({ + input: process.stdin, + output: process.stdout, + prompt: firstPrompt +}) + +const ready = new Promise(resolve => { + discover(blub => { + const {address} = blub + if (!paired.includes(address)) { + pair(blub, dispatch => { + resolve() + blubs.push(dispatch) + paired.push(address) + }) + } + }) +}) + +const chunk = (size, sequence) => { + const chunks = [] + let index = 0 + while (index < sequence.length) { + chunks.push(sequence.slice(index, index += size)) + } + return chunks +} + +const parse = string => + chunk(2, string.replace(/^#/, '')).map(hex => parseInt(hex, 16)) + +console.log('looking for blubs...') + +readline.on('line', line => { + const color = parse(line.trim()) + // as a little bonus, you can pass a color like #ff2a50cc + // where the last byte is the brightness :O + blubs.forEach(dispatch => dispatch(setColor(...color, 0xff))) + readline.setPrompt(prompt) + readline.prompt() +}) + +ready.then(() => readline.prompt()) diff --git a/play/go.js b/play/go.js new file mode 100644 index 0000000..d59281c --- /dev/null +++ b/play/go.js @@ -0,0 +1,19 @@ +const Blub = require('..') +const blubs = [] +const paired = [] + +Blub.discover(blub => { + const {address} = blub + if (!paired.includes(address)) { + Blub.pair(blub, dispatch => { + console.log(`got a ${address}`) + blubs.push(dispatch) + paired.push(address) + }) + } +}) + +const randomByte = () => Math.random() * 0xf00 | 0 + +setInterval(() => + blubs.forEach(dispatch => dispatch(Blub.setColor(randomByte(), randomByte(), randomByte(), 0xff))), 250) diff --git a/play/promise/blue.js b/play/promise/blue.js new file mode 100644 index 0000000..f3339e3 --- /dev/null +++ b/play/promise/blue.js @@ -0,0 +1,7 @@ +const {discover, setColor, pair} = require('../..') + +const setColorWithDispatch = (...color) => dispatch => dispatch(setColor(...color, 0xff)) + +discover() + .then(pair) + .then(setColorWithDispatch(0x33, 0x99, 0xff)) diff --git a/play/socket.js b/play/socket.js new file mode 100644 index 0000000..00c93eb --- /dev/null +++ b/play/socket.js @@ -0,0 +1,38 @@ +const fs = require('fs') +const net = require('net') +const Blub = require('..') +const blubs = [] +const paired = [] + +Blub.discover(blub => { + const {address} = blub + if (!paired.includes(address)) { + Blub.pair(blub, dispatch => { + blubs.push(dispatch) + paired.push(address) + }) + } +}) + +const path = '/tmp/blub' + +fs.unlink(path, () => { + const server = net.createServer(connection => { + connection.on('data', data => { + const hexmatch = data.toString().match(/^#([\da-z]{2})([\da-z]{2})([\da-z]{2})$/) + const rgbmatch = data.toString().match(/^rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\)$/) + const brightness = 0xaa + let red, green, blue + if (hexmatch) { + [red, green, blue] = hexmatch.slice(1).map(n => parseInt(n, 16)) + } + if (rgbmatch) { + [red, green, blue] = rgbmatch.slice(1) + } + blubs.forEach(dispatch => dispatch(Blub.setColor(red, green, blue, brightness))) + }) + }).listen(path, () => { + console.log('server bound on %s', path) + }) +}) + diff --git a/play/socket/? b/play/socket/? new file mode 100644 index 0000000..db2b2df --- /dev/null +++ b/play/socket/? @@ -0,0 +1 @@ +these files require '../socket' to be running diff --git a/play/socket/http.js b/play/socket/http.js new file mode 100644 index 0000000..c253b8d --- /dev/null +++ b/play/socket/http.js @@ -0,0 +1,17 @@ +// note: this one requires ./socket to be running +const net = require('net') +const express = require('express') +const bodyParser = require('body-parser') +const app = express() +const socket = net.connect('/tmp/blub') + +app.use(express.static('public')) +app.use(bodyParser.json()) +app.use(bodyParser.urlencoded({extended: true})) +app.post('/colors', function (request, response) { + socket.write(request.body.color) + response.send('thanks!') +}) +console.log('check http://localhost:8018/') +app.listen(8018) + diff --git a/play/socket/public/index.html b/play/socket/public/index.html new file mode 100644 index 0000000..25bcf03 --- /dev/null +++ b/play/socket/public/index.html @@ -0,0 +1,48 @@ + +did you know that to be a valid html5 document you only need a doctype and a title + + +

select a colour and then please press please

+
+ + +
diff --git a/play/socket/rainbow-gradient.js b/play/socket/rainbow-gradient.js new file mode 100644 index 0000000..8fdc9af --- /dev/null +++ b/play/socket/rainbow-gradient.js @@ -0,0 +1,26 @@ +const net = require('net') +const socket = net.connect('/tmp/blub') +const tinygradient = require('tinygradient') +const sleep = require('../util/sleep') + +const rainbow = tinygradient([ + '#ff2217', + '#ffaa11', + '#eeee22', + '#33ff66', + '#33ccff', + '#1111ee', + 'purple', + '#9933ec', + '#ff22ee', + '#ff2255' +]).rgb(200) + +;(async () => { + while (true) { + for (const color of rainbow) { + socket.write(color.toHexString()) + await sleep(7 * 7 * 2) + } + } +})() diff --git a/play/socket/rainbow.js b/play/socket/rainbow.js new file mode 100644 index 0000000..b2b643f --- /dev/null +++ b/play/socket/rainbow.js @@ -0,0 +1,20 @@ +const net = require('net') +const sleep = require('../util/sleep') +const socket = net.connect('/tmp/blub') + +const colors = [ + '#ff2217', + '#ff4522', + '#ffff00', + '#33ff66', + '#33ccff', + '#1111ee', + '#9933ec', + '#ff2a50' +] + +;(async function rainbow (index = 0) { + socket.write(colors[index]) + await sleep(500) + rainbow(index === colors.length - 1 ? 0 : index + 1) +})() diff --git a/play/socket/vap.js b/play/socket/vap.js new file mode 100644 index 0000000..0efd0ad --- /dev/null +++ b/play/socket/vap.js @@ -0,0 +1,11 @@ +const net = require('net') +const socket = net.connect('/tmp/blub') +const sleep = require('../util/sleep') +const pink = '#f8caff' +const blue = '#72f0ca' + +;(async function aesthetic (color = pink) { + socket.write(color) + await sleep(3000) + aesthetic(color === pink ? blue : pink) +})() diff --git a/play/util/sleep.js b/play/util/sleep.js new file mode 100644 index 0000000..bfb666f --- /dev/null +++ b/play/util/sleep.js @@ -0,0 +1 @@ +module.exports = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms))