snootforge

chee/kasa

a javascript for kasa veho lightbulbs

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
f0deaea6fb3535b5c6dd5e256d067fb490b4a893
  1. Fdiff --git a/.gitignore b/.gitignore
  2. new file mode 100644
  3. index 0000000..2ccbe46
  4. --- /dev/null
  5. +++ b/.gitignore
  6. H@@ -0,0 +1 @@
  7. +/node_modules/
  1. Fdiff --git a/README.md b/README.md
  2. new file mode 100644
  3. index 0000000..d58d574
  4. --- /dev/null
  5. +++ b/README.md
  6. H@@ -0,0 +1,74 @@
  7. +## smal lib for kasa veho (and tiktek?) smart bluetooth lightblibs
  8. +
  9. +small library for kasa veho lightbulbs and others like it.
  10. +
  11. +could not have finished this if it were not for
  12. +[https://mjg59.dreamwidth.org/43722.html](this blog post) and
  13. +[https://github.com/mjg59/python-tikteck](this python library) by
  14. +[https://twitter.com/mjg59](@mjg59). so thank you a billion times for writing
  15. +that and digging into that disassembled .so while i dug into nothing but shallow
  16. +graves
  17. +
  18. +you'll note that this is nearly a carbon copy of their library
  19. +
  20. +i couldn't run that because i wasn't on a loonix machine, so now this has
  21. +happened and should work on lanux and macos maybe even windows if the stars
  22. +align. relies on the wnoderful [noble](https://github.com/sandeepmistry/noble)
  23. +
  24. +## use:
  25. +
  26. +* with a bulb you know
  27. +
  28. +```js
  29. +const {pair, setColor} = require('kasa')
  30. +pair({
  31. + name: 'Smart Light',
  32. + password: 'password obtained from adb lolcat',
  33. + address: 'ca:fe:0f:be:ef' // address of light
  34. +}, dispatch => {
  35. + // red, green, blue, brightness
  36. + dispatch(setColor(0xff, 0x2a, 0x50, 0xff))
  37. +})
  38. +```
  39. +
  40. +* with a bulb you've never met:
  41. +
  42. +```js
  43. +const {discover, pair, setColor} = require('kasa')
  44. +discover({
  45. + name: 'Smart Light', // defaults to Smart Light
  46. + password: 'get this from adb lolcat'
  47. +}, blub => {
  48. + console.log('a real lootblub!', blub.address)
  49. + pair(blub, dispatch => {
  50. + dispatch(setColor(0x33, 0xcc, 0xff, 0xff))
  51. + })
  52. +})
  53. +```
  54. +
  55. +* there is also a promise api (note that you will only get the first bulb
  56. + discovered this way, but the callback will keep calling you up with every
  57. + light it finds)
  58. +
  59. +```js
  60. +const {discover, pair, setColor} = require('kasa')
  61. +
  62. +discover()
  63. + .then(pair)
  64. + .then(dispatch => dispatch(setColor(0xff, 0xff, 0xff, 0xff)))
  65. +```
  66. +
  67. +there are some silly examples in the directory called `play/`
  68. +
  69. +## todo:
  70. +
  71. +* clean up the code (vague!)
  72. +* fix vague todo items (which?)
  73. +* find out why my tongue has lumps on it
  74. +* revise the api when i have had literally any sleep in the past 3 years
  75. +* get rich, die old
  76. +* get to the top of Tom's top 8
  77. +* wear sunscreen
  78. +* learn the xaphoon
  79. +* see how lb is doin
  80. +* switch to crypto-js and pull parts out and see if this can run in web browsers
  1. Fdiff --git a/index.js b/index.js
  2. new file mode 100644
  3. index 0000000..6825db9
  4. --- /dev/null
  5. +++ b/index.js
  6. H@@ -0,0 +1,173 @@
  7. +const crypto = require('crypto')
  8. +const noble = require('noble')
  9. +
  10. +const nobleReady = new Promise(resolve =>
  11. + noble.on('stateChange', state => {
  12. + state === 'poweredOn' && resolve()
  13. + })
  14. +)
  15. +
  16. +const range = to => Array(to).fill().map((_, i) => i)
  17. +
  18. +function encrypt (key, data) {
  19. + key = Buffer.from(key)
  20. + key.reverse()
  21. + data = Buffer.from(data)
  22. + data.reverse()
  23. + const cipher = crypto.createCipheriv('aes-128-ecb', key, Buffer.from([]))
  24. + const encryptedData = cipher.update(data).reverse()
  25. + return encryptedData
  26. +}
  27. +
  28. +function generateSk (name, password, data1, data2) {
  29. + name = Buffer.from(name.padEnd(16, '\u0000'))
  30. + password = Buffer.from(password.padEnd(16, '\u0000'))
  31. + const key = []
  32. + name.forEach((byte, index) => {
  33. + key.push(byte ^ password[index])
  34. + })
  35. + const data = [...data1.slice(0, 8), ...data2.slice(0, 8)]
  36. + return encrypt(key, data)
  37. +}
  38. +
  39. +function encryptKey (name, password, data) {
  40. + name = Buffer.from(name.padEnd(16, '\u0000'))
  41. + password = Buffer.from(password.padEnd(16, '\u0000'))
  42. + const key = []
  43. + key.forEach.call(name, (byte, index) => {
  44. + key.push(byte ^ password[index])
  45. + })
  46. + return encrypt(data, key)
  47. +}
  48. +
  49. +// mutate me mor
  50. +function encryptPacket (sk, mac, packet) {
  51. + let tmp = [...mac.slice(0, 4), 0x01, ...packet.slice(0, 3), 15, 0, 0, 0, 0, 0, 0, 0]
  52. + tmp = encrypt(sk, tmp)
  53. +
  54. + range(15).forEach(i => {
  55. + tmp[i] = tmp[i] ^ packet[i + 5]
  56. + })
  57. +
  58. + tmp = encrypt(sk, tmp)
  59. +
  60. + range(2).forEach(i => {
  61. + packet[i + 3] = tmp[i]
  62. + })
  63. +
  64. + tmp = [0, ...mac.slice(0, 4), 0x01, ...packet.slice(0, 3), 0, 0, 0, 0, 0, 0, 0]
  65. +
  66. + tmp2 = []
  67. +
  68. + range(15).forEach(i => {
  69. + if (i === 0) {
  70. + tmp2 = encrypt(sk, tmp)
  71. + tmp[0] = tmp[0] + 1
  72. + }
  73. + packet[i + 5] ^= tmp2[i]
  74. + })
  75. +
  76. + return Buffer.from(packet)
  77. +}
  78. +
  79. +function connect (light, callback) {
  80. + return light.connect(() => callback(light))
  81. +}
  82. +
  83. +function discover (options = {}, callback) {
  84. + if (arguments.length === 1) {
  85. + if (typeof arguments[0] === 'function') {
  86. + callback = arguments[0]
  87. + options = {}
  88. + }
  89. + }
  90. + const {name = 'Smart Light', password = '234', address} = options
  91. + const discovery = new Promise(resolve => {
  92. + noble.on('discover', thing => {
  93. + if (thing.advertisement.localName !== name) return
  94. + thing.password = password
  95. + if (address) {
  96. + address === thing.address && connect(thing, (...args) => {
  97. + callback && callback(...args)
  98. + resolve(...args)
  99. + })
  100. + } else {
  101. + connect(thing, (...args) => {
  102. + callback && callback(...args)
  103. + resolve(...args)
  104. + })
  105. + }
  106. + })
  107. + })
  108. + noble.startScanning()
  109. + return discovery
  110. +}
  111. +
  112. +function pair (light, callback) {
  113. + let packetCount = Math.random() * 0xffff | 0
  114. + const name = light.name || light.advertisement.localName
  115. + const password = light.password
  116. + const mac = light.address
  117. + return new Promise(resolve => {
  118. + light.discoverAllServicesAndCharacteristics(() => {
  119. + const commandChar = light.services[1].characteristics[1]
  120. + const pairChar = light.services[1].characteristics[3]
  121. + const data = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0, 0, 0, 0, 0, 0, 0, 0]
  122. + const encryptedKey = encryptKey(name, password, data)
  123. + const packet = [0x0c]
  124. + .concat(data.slice(0, 8))
  125. + .concat([...encryptedKey].slice(0, 8))
  126. +
  127. + pairChar.write(new Buffer(packet), true, () => {
  128. + pairChar.read((error, received) => {
  129. + const sk = generateSk(name, password, data.slice(0, 8), received.slice(1, 9))
  130. + function dispatch ([id, command, data], callback) {
  131. + const packet = Array(20).fill(0)
  132. + packet[0] = packetCount & 0xff
  133. + packet[1] = packetCount >> 8 & 0xff
  134. + packet[5] = id & 0xff
  135. + packet[6] = id & 0xff | 0x80
  136. + packet[7] = command
  137. + packet[8] = 0x69
  138. + packet[9] = 0x69
  139. + packet[10] = data[0]
  140. + packet[11] = data[1]
  141. + packet[12] = data[2]
  142. + packet[13] = data[3]
  143. + const macKey = Buffer.from(mac.split(':').slice(0, 6).reverse().map(n => parseInt(n, 16)))
  144. + const encryptedPacket = encryptPacket(sk, macKey, [...packet])
  145. + packetCount = packetCount > 0xffff ? 1 : packetCount + 1
  146. + return new Promise(resolve => {
  147. + commandChar.write(encryptedPacket, false, (...args) => {
  148. + callback && callback(...args)
  149. + resolve(...args)
  150. + })
  151. + })
  152. + }
  153. + callback && callback(dispatch)
  154. + resolve(dispatch)
  155. + })
  156. + })
  157. + })
  158. + })
  159. +}
  160. +
  161. +function sendCommand (command, ...args) {
  162. + return [0xffff, command, args]
  163. +}
  164. +
  165. +// red, green, blue, brightness
  166. +function setColor (...values) {
  167. + return sendCommand(0xc1, ...values)
  168. +}
  169. +
  170. +function setDefaultColor (...values) {
  171. + return sendCommand(0xc4, ...values)
  172. +}
  173. +
  174. +module.exports = {
  175. + discover: (...args) => nobleReady.then(() => discover(...args)),
  176. + pair,
  177. + setColor,
  178. + setDefaultColor
  179. +}
  1. Fdiff --git a/package-lock.json b/package-lock.json
  2. new file mode 100644
  3. index 0000000..d9f9179
  4. --- /dev/null
  5. +++ b/package-lock.json
  6. H@@ -0,0 +1,427 @@
  7. +{
  8. + "name": "kasa",
  9. + "version": "1.0.0",
  10. + "lockfileVersion": 1,
  11. + "requires": true,
  12. + "dependencies": {
  13. + "accepts": {
  14. + "version": "1.3.3",
  15. + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
  16. + "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=",
  17. + "optional": true,
  18. + "requires": {
  19. + "mime-types": "2.1.15",
  20. + "negotiator": "0.6.1"
  21. + }
  22. + },
  23. + "array-flatten": {
  24. + "version": "1.1.1",
  25. + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
  26. + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=",
  27. + "optional": true
  28. + },
  29. + "body-parser": {
  30. + "version": "1.17.2",
  31. + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.17.2.tgz",
  32. + "integrity": "sha1-+IkqvI+eYn1Crtr7yma/WrmRBO4=",
  33. + "optional": true,
  34. + "requires": {
  35. + "bytes": "2.4.0",
  36. + "content-type": "1.0.2",
  37. + "debug": "2.6.7",
  38. + "depd": "1.1.0",
  39. + "http-errors": "1.6.1",
  40. + "iconv-lite": "0.4.15",
  41. + "on-finished": "2.3.0",
  42. + "qs": "6.4.0",
  43. + "raw-body": "2.2.0",
  44. + "type-is": "1.6.15"
  45. + }
  46. + },
  47. + "bplist-parser": {
  48. + "version": "0.0.6",
  49. + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.0.6.tgz",
  50. + "integrity": "sha1-ONo0cYF9+dRKs4kuJ3B7u9daEbk=",
  51. + "optional": true
  52. + },
  53. + "bytes": {
  54. + "version": "2.4.0",
  55. + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.4.0.tgz",
  56. + "integrity": "sha1-fZcZb51br39pNeJZhVSe3SpsIzk="
  57. + },
  58. + "content-disposition": {
  59. + "version": "0.5.2",
  60. + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
  61. + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=",
  62. + "optional": true
  63. + },
  64. + "content-type": {
  65. + "version": "1.0.2",
  66. + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz",
  67. + "integrity": "sha1-t9ETrueo3Se9IRM8TcJSnfFyHu0=",
  68. + "optional": true
  69. + },
  70. + "cookie": {
  71. + "version": "0.3.1",
  72. + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
  73. + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
  74. + "optional": true
  75. + },
  76. + "cookie-signature": {
  77. + "version": "1.0.6",
  78. + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
  79. + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=",
  80. + "optional": true
  81. + },
  82. + "debug": {
  83. + "version": "2.6.7",
  84. + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.7.tgz",
  85. + "integrity": "sha1-krrR9tBbu2u6Isyoi80OyJTChh4=",
  86. + "requires": {
  87. + "ms": "2.0.0"
  88. + }
  89. + },
  90. + "depd": {
  91. + "version": "1.1.0",
  92. + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz",
  93. + "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM="
  94. + },
  95. + "destroy": {
  96. + "version": "1.0.4",
  97. + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
  98. + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
  99. + },
  100. + "ee-first": {
  101. + "version": "1.1.1",
  102. + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
  103. + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
  104. + },
  105. + "encodeurl": {
  106. + "version": "1.0.1",
  107. + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz",
  108. + "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA="
  109. + },
  110. + "escape-html": {
  111. + "version": "1.0.3",
  112. + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
  113. + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
  114. + },
  115. + "etag": {
  116. + "version": "1.8.0",
  117. + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.0.tgz",
  118. + "integrity": "sha1-b2Ma7zNtbEY2K1F2QETOIWvjwFE="
  119. + },
  120. + "express": {
  121. + "version": "4.15.3",
  122. + "resolved": "https://registry.npmjs.org/express/-/express-4.15.3.tgz",
  123. + "integrity": "sha1-urZdDwOqgMNYQIly/HAPkWlEtmI=",
  124. + "optional": true,
  125. + "requires": {
  126. + "accepts": "1.3.3",
  127. + "array-flatten": "1.1.1",
  128. + "content-disposition": "0.5.2",
  129. + "content-type": "1.0.2",
  130. + "cookie": "0.3.1",
  131. + "cookie-signature": "1.0.6",
  132. + "debug": "2.6.7",
  133. + "depd": "1.1.0",
  134. + "encodeurl": "1.0.1",
  135. + "escape-html": "1.0.3",
  136. + "etag": "1.8.0",
  137. + "finalhandler": "1.0.3",
  138. + "fresh": "0.5.0",
  139. + "merge-descriptors": "1.0.1",
  140. + "methods": "1.1.2",
  141. + "on-finished": "2.3.0",
  142. + "parseurl": "1.3.1",
  143. + "path-to-regexp": "0.1.7",
  144. + "proxy-addr": "1.1.4",
  145. + "qs": "6.4.0",
  146. + "range-parser": "1.2.0",
  147. + "send": "0.15.3",
  148. + "serve-static": "1.12.3",
  149. + "setprototypeof": "1.0.3",
  150. + "statuses": "1.3.1",
  151. + "type-is": "1.6.15",
  152. + "utils-merge": "1.0.0",
  153. + "vary": "1.1.1"
  154. + }
  155. + },
  156. + "finalhandler": {
  157. + "version": "1.0.3",
  158. + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.0.3.tgz",
  159. + "integrity": "sha1-70fneVDpmXgOhgIqVg4yF+DQzIk=",
  160. + "optional": true,
  161. + "requires": {
  162. + "debug": "2.6.7",
  163. + "encodeurl": "1.0.1",
  164. + "escape-html": "1.0.3",
  165. + "on-finished": "2.3.0",
  166. + "parseurl": "1.3.1",
  167. + "statuses": "1.3.1",
  168. + "unpipe": "1.0.0"
  169. + }
  170. + },
  171. + "forwarded": {
  172. + "version": "0.1.0",
  173. + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz",
  174. + "integrity": "sha1-Ge+YdMSuHCl7zweP3mOgm2aoQ2M=",
  175. + "optional": true
  176. + },
  177. + "fresh": {
  178. + "version": "0.5.0",
  179. + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz",
  180. + "integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44="
  181. + },
  182. + "http-errors": {
  183. + "version": "1.6.1",
  184. + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz",
  185. + "integrity": "sha1-X4uO2YrKVFZWv1cplzh/kEpyIlc=",
  186. + "requires": {
  187. + "depd": "1.1.0",
  188. + "inherits": "2.0.3",
  189. + "setprototypeof": "1.0.3",
  190. + "statuses": "1.3.1"
  191. + }
  192. + },
  193. + "iconv-lite": {
  194. + "version": "0.4.15",
  195. + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz",
  196. + "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es="
  197. + },
  198. + "inherits": {
  199. + "version": "2.0.3",
  200. + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
  201. + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
  202. + },
  203. + "ipaddr.js": {
  204. + "version": "1.3.0",
  205. + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.3.0.tgz",
  206. + "integrity": "sha1-HgOlL9rYOou7KyXL9JmLTP/NPew=",
  207. + "optional": true
  208. + },
  209. + "media-typer": {
  210. + "version": "0.3.0",
  211. + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
  212. + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
  213. + "optional": true
  214. + },
  215. + "merge-descriptors": {
  216. + "version": "1.0.1",
  217. + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
  218. + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
  219. + "optional": true
  220. + },
  221. + "methods": {
  222. + "version": "1.1.2",
  223. + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
  224. + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
  225. + "optional": true
  226. + },
  227. + "mime": {
  228. + "version": "1.3.4",
  229. + "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz",
  230. + "integrity": "sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM="
  231. + },
  232. + "mime-db": {
  233. + "version": "1.27.0",
  234. + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz",
  235. + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE="
  236. + },
  237. + "mime-types": {
  238. + "version": "2.1.15",
  239. + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz",
  240. + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=",
  241. + "requires": {
  242. + "mime-db": "1.27.0"
  243. + }
  244. + },
  245. + "ms": {
  246. + "version": "2.0.0",
  247. + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
  248. + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
  249. + },
  250. + "nan": {
  251. + "version": "2.6.2",
  252. + "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz",
  253. + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=",
  254. + "optional": true
  255. + },
  256. + "negotiator": {
  257. + "version": "0.6.1",
  258. + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
  259. + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=",
  260. + "optional": true
  261. + },
  262. + "noble": {
  263. + "version": "1.8.1",
  264. + "resolved": "https://registry.npmjs.org/noble/-/noble-1.8.1.tgz",
  265. + "integrity": "sha1-7+iAgStyUa+qrbT+g3U8EwKmc/Y=",
  266. + "requires": {
  267. + "bplist-parser": "0.0.6",
  268. + "debug": "2.2.0",
  269. + "xpc-connection": "0.1.4"
  270. + },
  271. + "dependencies": {
  272. + "debug": {
  273. + "version": "2.2.0",
  274. + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
  275. + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
  276. + "requires": {
  277. + "ms": "0.7.1"
  278. + }
  279. + },
  280. + "ms": {
  281. + "version": "0.7.1",
  282. + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
  283. + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg="
  284. + }
  285. + }
  286. + },
  287. + "on-finished": {
  288. + "version": "2.3.0",
  289. + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
  290. + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
  291. + "requires": {
  292. + "ee-first": "1.1.1"
  293. + }
  294. + },
  295. + "parseurl": {
  296. + "version": "1.3.1",
  297. + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz",
  298. + "integrity": "sha1-yKuMkiO6NIiKpkopeyiFO+wY2lY="
  299. + },
  300. + "path-to-regexp": {
  301. + "version": "0.1.7",
  302. + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
  303. + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=",
  304. + "optional": true
  305. + },
  306. + "proxy-addr": {
  307. + "version": "1.1.4",
  308. + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.1.4.tgz",
  309. + "integrity": "sha1-J+VF9pYKRKYn2bREZ+NcG2tM4vM=",
  310. + "optional": true,
  311. + "requires": {
  312. + "forwarded": "0.1.0",
  313. + "ipaddr.js": "1.3.0"
  314. + }
  315. + },
  316. + "qs": {
  317. + "version": "6.4.0",
  318. + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
  319. + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=",
  320. + "optional": true
  321. + },
  322. + "range-parser": {
  323. + "version": "1.2.0",
  324. + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
  325. + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
  326. + },
  327. + "raw-body": {
  328. + "version": "2.2.0",
  329. + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.2.0.tgz",
  330. + "integrity": "sha1-mUl2z2pQlqQRYoQEkvC9xdbn+5Y=",
  331. + "optional": true,
  332. + "requires": {
  333. + "bytes": "2.4.0",
  334. + "iconv-lite": "0.4.15",
  335. + "unpipe": "1.0.0"
  336. + }
  337. + },
  338. + "send": {
  339. + "version": "0.15.3",
  340. + "resolved": "https://registry.npmjs.org/send/-/send-0.15.3.tgz",
  341. + "integrity": "sha1-UBP5+ZAj31DRvZiSwZ4979HVMwk=",
  342. + "requires": {
  343. + "debug": "2.6.7",
  344. + "depd": "1.1.0",
  345. + "destroy": "1.0.4",
  346. + "encodeurl": "1.0.1",
  347. + "escape-html": "1.0.3",
  348. + "etag": "1.8.0",
  349. + "fresh": "0.5.0",
  350. + "http-errors": "1.6.1",
  351. + "mime": "1.3.4",
  352. + "ms": "2.0.0",
  353. + "on-finished": "2.3.0",
  354. + "range-parser": "1.2.0",
  355. + "statuses": "1.3.1"
  356. + }
  357. + },
  358. + "serve-static": {
  359. + "version": "1.12.3",
  360. + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.12.3.tgz",
  361. + "integrity": "sha1-n0uhni8wMMVH+K+ZEHg47DjVseI=",
  362. + "optional": true,
  363. + "requires": {
  364. + "encodeurl": "1.0.1",
  365. + "escape-html": "1.0.3",
  366. + "parseurl": "1.3.1",
  367. + "send": "0.15.3"
  368. + }
  369. + },
  370. + "setprototypeof": {
  371. + "version": "1.0.3",
  372. + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
  373. + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
  374. + },
  375. + "statuses": {
  376. + "version": "1.3.1",
  377. + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
  378. + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
  379. + },
  380. + "tinycolor2": {
  381. + "version": "1.4.1",
  382. + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
  383. + "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=",
  384. + "optional": true
  385. + },
  386. + "tinygradient": {
  387. + "version": "0.3.1",
  388. + "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-0.3.1.tgz",
  389. + "integrity": "sha1-LZ5PvjSMSX7RHih6QzaDWz/RECI=",
  390. + "optional": true,
  391. + "requires": {
  392. + "tinycolor2": "1.4.1"
  393. + }
  394. + },
  395. + "type-is": {
  396. + "version": "1.6.15",
  397. + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz",
  398. + "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=",
  399. + "optional": true,
  400. + "requires": {
  401. + "media-typer": "0.3.0",
  402. + "mime-types": "2.1.15"
  403. + }
  404. + },
  405. + "unpipe": {
  406. + "version": "1.0.0",
  407. + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
  408. + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
  409. + "optional": true
  410. + },
  411. + "utils-merge": {
  412. + "version": "1.0.0",
  413. + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz",
  414. + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=",
  415. + "optional": true
  416. + },
  417. + "vary": {
  418. + "version": "1.1.1",
  419. + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.1.tgz",
  420. + "integrity": "sha1-Z1Neu2lMHVIldFeYRmUyP1h+jTc=",
  421. + "optional": true
  422. + },
  423. + "xpc-connection": {
  424. + "version": "0.1.4",
  425. + "resolved": "https://registry.npmjs.org/xpc-connection/-/xpc-connection-0.1.4.tgz",
  426. + "integrity": "sha1-3Nf6oq7Gt6bhjMXdrQQvejTHcVY=",
  427. + "optional": true,
  428. + "requires": {
  429. + "nan": "2.6.2"
  430. + }
  431. + }
  432. + }
  433. +}
  1. Fdiff --git a/package.json b/package.json
  2. new file mode 100644
  3. index 0000000..bf2a183
  4. --- /dev/null
  5. +++ b/package.json
  6. H@@ -0,0 +1,14 @@
  7. +{
  8. + "name": "kasa",
  9. + "version": "1.0.0",
  10. + "main": "index.js",
  11. + "license": "MIT",
  12. + "dependencies": {
  13. + "noble": "^1.8.1"
  14. + },
  15. + "optionalDependencies": {
  16. + "body-parser": "^1.17.2",
  17. + "express": "^4.15.3",
  18. + "tinygradient": "^0.3.1"
  19. + }
  20. +}
  1. Fdiff --git a/play/cli.js b/play/cli.js
  2. new file mode 100644
  3. index 0000000..a634fe9
  4. --- /dev/null
  5. +++ b/play/cli.js
  6. H@@ -0,0 +1,51 @@
  7. +const {createInterface} = require('readline')
  8. +const {discover, pair, setColor} = require('..')
  9. +
  10. +const blubs = []
  11. +const paired = []
  12. +
  13. +const prompt = 'hex color: '
  14. +const firstPrompt = `pls give me a ${prompt.slice(0, -2)} (like #ff2a50): `
  15. +const readline = createInterface({
  16. + input: process.stdin,
  17. + output: process.stdout,
  18. + prompt: firstPrompt
  19. +})
  20. +
  21. +const ready = new Promise(resolve => {
  22. + discover(blub => {
  23. + const {address} = blub
  24. + if (!paired.includes(address)) {
  25. + pair(blub, dispatch => {
  26. + resolve()
  27. + blubs.push(dispatch)
  28. + paired.push(address)
  29. + })
  30. + }
  31. + })
  32. +})
  33. +
  34. +const chunk = (size, sequence) => {
  35. + const chunks = []
  36. + let index = 0
  37. + while (index < sequence.length) {
  38. + chunks.push(sequence.slice(index, index += size))
  39. + }
  40. + return chunks
  41. +}
  42. +
  43. +const parse = string =>
  44. + chunk(2, string.replace(/^#/, '')).map(hex => parseInt(hex, 16))
  45. +
  46. +console.log('looking for blubs...')
  47. +
  48. +readline.on('line', line => {
  49. + const color = parse(line.trim())
  50. + // as a little bonus, you can pass a color like #ff2a50cc
  51. + // where the last byte is the brightness :O
  52. + blubs.forEach(dispatch => dispatch(setColor(...color, 0xff)))
  53. + readline.setPrompt(prompt)
  54. + readline.prompt()
  55. +})
  56. +
  57. +ready.then(() => readline.prompt())
  1. Fdiff --git a/play/go.js b/play/go.js
  2. new file mode 100644
  3. index 0000000..d59281c
  4. --- /dev/null
  5. +++ b/play/go.js
  6. H@@ -0,0 +1,19 @@
  7. +const Blub = require('..')
  8. +const blubs = []
  9. +const paired = []
  10. +
  11. +Blub.discover(blub => {
  12. + const {address} = blub
  13. + if (!paired.includes(address)) {
  14. + Blub.pair(blub, dispatch => {
  15. + console.log(`got a ${address}`)
  16. + blubs.push(dispatch)
  17. + paired.push(address)
  18. + })
  19. + }
  20. +})
  21. +
  22. +const randomByte = () => Math.random() * 0xf00 | 0
  23. +
  24. +setInterval(() =>
  25. + blubs.forEach(dispatch => dispatch(Blub.setColor(randomByte(), randomByte(), randomByte(), 0xff))), 250)
  1. Fdiff --git a/play/promise/blue.js b/play/promise/blue.js
  2. new file mode 100644
  3. index 0000000..f3339e3
  4. --- /dev/null
  5. +++ b/play/promise/blue.js
  6. H@@ -0,0 +1,7 @@
  7. +const {discover, setColor, pair} = require('../..')
  8. +
  9. +const setColorWithDispatch = (...color) => dispatch => dispatch(setColor(...color, 0xff))
  10. +
  11. +discover()
  12. + .then(pair)
  13. + .then(setColorWithDispatch(0x33, 0x99, 0xff))
  1. Fdiff --git a/play/socket.js b/play/socket.js
  2. new file mode 100644
  3. index 0000000..00c93eb
  4. --- /dev/null
  5. +++ b/play/socket.js
  6. H@@ -0,0 +1,38 @@
  7. +const fs = require('fs')
  8. +const net = require('net')
  9. +const Blub = require('..')
  10. +const blubs = []
  11. +const paired = []
  12. +
  13. +Blub.discover(blub => {
  14. + const {address} = blub
  15. + if (!paired.includes(address)) {
  16. + Blub.pair(blub, dispatch => {
  17. + blubs.push(dispatch)
  18. + paired.push(address)
  19. + })
  20. + }
  21. +})
  22. +
  23. +const path = '/tmp/blub'
  24. +
  25. +fs.unlink(path, () => {
  26. + const server = net.createServer(connection => {
  27. + connection.on('data', data => {
  28. + const hexmatch = data.toString().match(/^#([\da-z]{2})([\da-z]{2})([\da-z]{2})$/)
  29. + const rgbmatch = data.toString().match(/^rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\)$/)
  30. + const brightness = 0xaa
  31. + let red, green, blue
  32. + if (hexmatch) {
  33. + [red, green, blue] = hexmatch.slice(1).map(n => parseInt(n, 16))
  34. + }
  35. + if (rgbmatch) {
  36. + [red, green, blue] = rgbmatch.slice(1)
  37. + }
  38. + blubs.forEach(dispatch => dispatch(Blub.setColor(red, green, blue, brightness)))
  39. + })
  40. + }).listen(path, () => {
  41. + console.log('server bound on %s', path)
  42. + })
  43. +})
  44. +
  1. Fdiff --git a/play/socket/? b/play/socket/?
  2. new file mode 100644
  3. index 0000000..db2b2df
  4. --- /dev/null
  5. +++ b/play/socket/?
  6. H@@ -0,0 +1 @@
  7. +these files require '../socket' to be running
  1. Fdiff --git a/play/socket/http.js b/play/socket/http.js
  2. new file mode 100644
  3. index 0000000..c253b8d
  4. --- /dev/null
  5. +++ b/play/socket/http.js
  6. H@@ -0,0 +1,17 @@
  7. +// note: this one requires ./socket to be running
  8. +const net = require('net')
  9. +const express = require('express')
  10. +const bodyParser = require('body-parser')
  11. +const app = express()
  12. +const socket = net.connect('/tmp/blub')
  13. +
  14. +app.use(express.static('public'))
  15. +app.use(bodyParser.json())
  16. +app.use(bodyParser.urlencoded({extended: true}))
  17. +app.post('/colors', function (request, response) {
  18. + socket.write(request.body.color)
  19. + response.send('thanks!')
  20. +})
  21. +console.log('check http://localhost:8018/')
  22. +app.listen(8018)
  23. +
  1. Fdiff --git a/play/socket/public/index.html b/play/socket/public/index.html
  2. new file mode 100644
  3. index 0000000..25bcf03
  4. --- /dev/null
  5. +++ b/play/socket/public/index.html
  6. H@@ -0,0 +1,48 @@
  7. +<!doctype html>
  8. +<title>did you know that to be a valid html5 document you only need a doctype and a title</title>
  9. +<style>
  10. + @keyframes woggle {
  11. + 0% {
  12. + transform: rotate(-1deg)
  13. + }
  14. +
  15. + 100% {
  16. + transform: rotate(1deg)
  17. + }
  18. + }
  19. +
  20. + * {
  21. + border: 2px solid hotpink;
  22. + }
  23. +
  24. + body {
  25. + background: lime;
  26. + }
  27. +
  28. + input {
  29. + display: 'block';
  30. + font-size: 4em;
  31. + height: 1em;
  32. + min-width: 2em;
  33. + line-height: 0.9;
  34. + }
  35. +
  36. + pleasemakethis1wordadifferentcolour {
  37. + color: blue;
  38. + background: LightGoldenRodYellow;
  39. + padding: 1px;
  40. + animation: 0.123455678s infinite alternate woggle;
  41. + animation-delay: 1s;
  42. + display: inline-block;
  43. + }
  44. +
  45. + [type="submit"] {
  46. + animation: 0.2s infinite woggle;
  47. + }
  48. +</style>
  49. +
  50. +<h1> select a colour and then please press <pleasemakethis1wordadifferentcolour>please</pleasemakethis1wordadifferentcolour></h1>
  51. +<form action="http://localhost:8018/colors" method="POST">
  52. + <input type="color" name="color">
  53. + <input type="submit" value="please">
  54. +</form>
  1. Fdiff --git a/play/socket/rainbow-gradient.js b/play/socket/rainbow-gradient.js
  2. new file mode 100644
  3. index 0000000..8fdc9af
  4. --- /dev/null
  5. +++ b/play/socket/rainbow-gradient.js
  6. H@@ -0,0 +1,26 @@
  7. +const net = require('net')
  8. +const socket = net.connect('/tmp/blub')
  9. +const tinygradient = require('tinygradient')
  10. +const sleep = require('../util/sleep')
  11. +
  12. +const rainbow = tinygradient([
  13. + '#ff2217',
  14. + '#ffaa11',
  15. + '#eeee22',
  16. + '#33ff66',
  17. + '#33ccff',
  18. + '#1111ee',
  19. + 'purple',
  20. + '#9933ec',
  21. + '#ff22ee',
  22. + '#ff2255'
  23. +]).rgb(200)
  24. +
  25. +;(async () => {
  26. + while (true) {
  27. + for (const color of rainbow) {
  28. + socket.write(color.toHexString())
  29. + await sleep(7 * 7 * 2)
  30. + }
  31. + }
  32. +})()
  1. Fdiff --git a/play/socket/rainbow.js b/play/socket/rainbow.js
  2. new file mode 100644
  3. index 0000000..b2b643f
  4. --- /dev/null
  5. +++ b/play/socket/rainbow.js
  6. H@@ -0,0 +1,20 @@
  7. +const net = require('net')
  8. +const sleep = require('../util/sleep')
  9. +const socket = net.connect('/tmp/blub')
  10. +
  11. +const colors = [
  12. + '#ff2217',
  13. + '#ff4522',
  14. + '#ffff00',
  15. + '#33ff66',
  16. + '#33ccff',
  17. + '#1111ee',
  18. + '#9933ec',
  19. + '#ff2a50'
  20. +]
  21. +
  22. +;(async function rainbow (index = 0) {
  23. + socket.write(colors[index])
  24. + await sleep(500)
  25. + rainbow(index === colors.length - 1 ? 0 : index + 1)
  26. +})()
  1. Fdiff --git a/play/socket/vap.js b/play/socket/vap.js
  2. new file mode 100644
  3. index 0000000..0efd0ad
  4. --- /dev/null
  5. +++ b/play/socket/vap.js
  6. H@@ -0,0 +1,11 @@
  7. +const net = require('net')
  8. +const socket = net.connect('/tmp/blub')
  9. +const sleep = require('../util/sleep')
  10. +const pink = '#f8caff'
  11. +const blue = '#72f0ca'
  12. +
  13. +;(async function aesthetic (color = pink) {
  14. + socket.write(color)
  15. + await sleep(3000)
  16. + aesthetic(color === pink ? blue : pink)
  17. +})()
  1. Fdiff --git a/play/util/sleep.js b/play/util/sleep.js
  2. new file mode 100644
  3. index 0000000..bfb666f
  4. --- /dev/null
  5. +++ b/play/util/sleep.js
  6. H@@ -0,0 +1 @@
  7. +module.exports = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms))