diff --git a/README.md b/README.md index 89bf8e5..46da771 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,15 @@ api.identify() ## Changelog ## +### 1.3.0 (28.11.2021) +- (daniel_2k) Added method for writing custom effects +- (daniel_2k) Added SSE subscription for getting push notifications (e.g. using gestures) +- (daniel_2k) Added setRythmMode +- (daniel_2k) Added duration parameter for setBrightness +- (daniel_2k) Changes to comply with nanoloeaf Aurora API +- (daniel_2k) Fixed handling timeout parameter + + ### 1.2.2 (2018.01.18) - (Xyala) Added script to get token. diff --git a/index.js b/index.js index 136b0f7..1a363df 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,9 @@ 'use strict'; const fs = require('fs'), - http = require('http'); + http = require('http'), + EventSource = require('eventsource'), + timeout = 2000; var AuroraApi = module.exports = function (options) { var self = this; @@ -27,7 +29,9 @@ var AuroraApi = module.exports = function (options) { this.port = options.port || null; this.base = options.base; this.accessToken = options.accessToken; - this.timeout = options.timeout || 2000; + this.timeout = options.timeout || timeout; + + this.SSEEventSource; this.makeOptions = function (pathname, method) { var options = { @@ -35,8 +39,9 @@ var AuroraApi = module.exports = function (options) { hostname: this.host, port: this.port, path: `${this.base}${this.accessToken}${pathname}`, - method: method - }; + method: method, + timeout: this.timeout + } return options; }; @@ -63,7 +68,7 @@ var AuroraApi = module.exports = function (options) { options.headers = { 'Content-Type': 'application/json', - 'Content-Length': bodyJSON.length + 'Content-Length': Buffer.byteLength(bodyJSON) }; requestOptions.body = bodyJSON; @@ -146,9 +151,46 @@ var AuroraApi = module.exports = function (options) { v: Math.round(v * 100) }; }; -}; + this.subscribeSSE = function(callback) { + return new Promise((resolve, reject) => { + + this.SSEEventSource = new EventSource(`${this.protocol}//${this.host}:${this.port}${this.base}${this.accessToken}/events?id=1,3,4`); + + this.SSEEventSource.onopen = function(event) { + resolve(); + }; + + this.SSEEventSource.onmessage = function(event) { + var data = {}; + var error; + + try { + var parsedData = JSON.parse(event.data); + + data.eventID = Number.parseInt(event.lastEventId); + data.events = parsedData.events; + } + catch (err) { + error = err; + data = {}; //empty incomplete data object + } + callback(data, error); + }; + + this.SSEEventSource.onerror = function(error) { + var errorMessage = error.type + ": " + error.message + " (status: " + error.status + ")"; + if (reject) reject(errorMessage); + else callback({}, errorMessage); + }; + }); + }; +}; +AuroraApi.Events = {state: 1, effects: 3, touch: 4}; +AuroraApi.StateAttributes = {on: 1, brightness: 2, hue: 3, saturation: 4, cct: 5, colorMode: 6}; +AuroraApi.EventAttributes = {event: 1, eventList: 2}; +AuroraApi.GestureID = {SingleTap: 0, DoubleTap: 1, SwipeUp: 2, SwipeDown: 3, SwipeLeft: 4, SwipeRight: 5}; AuroraApi.prototype.getInfo = function () { const requestOptions = this.makeGetRequestOptions('/'); @@ -164,9 +206,7 @@ AuroraApi.prototype.getPowerStatus = function () { AuroraApi.prototype.turnOn = function () { const requestOptions = this.makePutRequest('/state', { - on: { - value: true - } + on: { value: true } }); return this.doRequest(requestOptions); @@ -174,9 +214,7 @@ AuroraApi.prototype.turnOn = function () { AuroraApi.prototype.turnOff = function () { const requestOptions = this.makePutRequest('/state', { - on: { - value: false - } + on: { value: false } }); return this.doRequest(requestOptions); @@ -188,9 +226,12 @@ AuroraApi.prototype.getBrightness = function () { return this.doRequest(requestOptions); }; -AuroraApi.prototype.setBrightness = function (value) { +AuroraApi.prototype.setBrightness = function (value, duration = 0) { const requestOptions = this.makePutRequest('/state', { - brightness: value + brightness: { + value: value, + duration : duration + } }); return this.doRequest(requestOptions); @@ -204,7 +245,7 @@ AuroraApi.prototype.getHue = function () { AuroraApi.prototype.setHue = function (value) { const requestOptions = this.makePutRequest('/state', { - hue: value + hue: { value: value } }); return this.doRequest(requestOptions); @@ -218,7 +259,7 @@ AuroraApi.prototype.getSat = function () { AuroraApi.prototype.setSat = function (value) { const requestOptions = this.makePutRequest('/state', { - sat: value + sat: { value: value } }); return this.doRequest(requestOptions); @@ -226,9 +267,9 @@ AuroraApi.prototype.setSat = function (value) { AuroraApi.prototype.setHSV = function (h,s,v) { const requestOptions = this.makePutRequest('/state', { - sat: s, - hue: h, - brightness: v + sat: { value: s }, + hue: { value: h }, + brightness: { value: v } }); return this.doRequest(requestOptions); @@ -237,9 +278,9 @@ AuroraApi.prototype.setHSV = function (h,s,v) { AuroraApi.prototype.setRGB = function (r,g,b) { var hsv = this.rgb2hsv(r,g,b); const requestOptions = this.makePutRequest('/state', { - sat: hsv.s, - hue: hsv.h, - brightness: hsv.v + sat: { value: hsv.s }, + hue: { value: hsv.h }, + brightness: { value: hsv.v } }); return this.doRequest(requestOptions); @@ -253,7 +294,7 @@ AuroraApi.prototype.getColourTemperature = function () { AuroraApi.prototype.setColourTemperature = function (value) { const requestOptions = this.makePutRequest('/state', { - ct: value + ct: { value: value } }); return this.doRequest(requestOptions); @@ -273,7 +314,15 @@ AuroraApi.prototype.getEffect = function () { AuroraApi.prototype.setEffect = function (effect) { const requestOptions = this.makePutRequest('/effects', { - 'select': effect + select: effect + }); + + return this.doRequest(requestOptions); +}; + +AuroraApi.prototype.writeEffect = function (effectObj) { + const requestOptions = this.makePutRequest('/effects', { + write: effectObj }); return this.doRequest(requestOptions); @@ -302,3 +351,74 @@ AuroraApi.prototype.identify = function () { return this.doRequest(requestOptions); }; + +AuroraApi.prototype.setRhythmMode = function (rhythmMode) { + const requestOptions = this.makePutRequest('/rhythm/rhythmMode', { + rhythmMode: rhythmMode + }); + + return this.doRequest(requestOptions); +}; + +AuroraApi.prototype.startSSE = function(callback) { + return this.subscribeSSE(callback); +}; + +AuroraApi.prototype.stopSSE = function() { + if (this.SSEEventSource) this.SSEEventSource.close(); +}; + +// automatically obtain an auth token when device is in pairing mode +AuroraApi.getAuthToken = function (address, port) { + return new Promise((resolve, reject) => { + const options = { + hostname: address, + port: port, + path: "/api/v1/new", + method: "POST", + timeout: timeout + }; + + const req = http.request(options, (res) => { + const statusCode = res.statusCode; + const contentType = res.headers['content-type']; + + switch (statusCode) { + case 200: if (!/^application\/json/.test(contentType)) { + reject({errorCode: "ErrorJSON", message: "Error obtaining authorization token!", messageDetail: "Invalid content-type. Expected \"application/json\" but received " + contentType}); + return; + } + break; + case 401: reject({errorCode: "ErrorUnauthorized", message: "Getting authorization token failed because access is unauthorized (is the device in pairing mode?)"}); + return; + case 403: reject({errorCode: "ErrorUnauthorized", message: "Getting authorization token failed because permission denied (is the device in pairing mode?)"}); + return; + default: reject({errorCode: "ErrorConnection", message: "Connection to \"" + address + ":" + port + "\" failed: HTTP status code " + statusCode}); + return; + } + + let rawData = ""; + res.on("data", (chunk) => { rawData += chunk; }); + res.on("end", () => { + try { + const parsedData = JSON.parse(rawData); + if (parsedData["auth_token"]) { + resolve(parsedData["auth_token"]); + } + else { + reject({errorCode: "NoAuthTokenFound", message: "No authorization token found!", messageDetail: "JSON response does not contain an \"auth_token\""}); + } + } + catch (err) { + reject({errorCode: "NoAuthTokenFound", message: "No authorization token found!", messageDetail: "Error JSON parsing received data: " + err}); + } + }); + }); + + req.on("error", (err) => { + reject({errorCode: "ErrorConnection", message: "Connection to \"" + address + ":" + port + "\" failed, " + err}) + }); + + req.end(); + }); +}; diff --git a/package.json b/package.json index 2b3d05f..d52064e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanoleaf-aurora-client", - "version": "1.2.2", + "version": "1.3.0", "author": "Darren Thomas ", "description": "Nanoleaf Aurora API client", "main": "index.js",