diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/ColorlampBase.js b/ColorlampBase.js index 45fb27d..aa9dc47 100644 --- a/ColorlampBase.js +++ b/ColorlampBase.js @@ -10,6 +10,10 @@ class HCColorlamp extends HCControlBase { return "colorlamp"; } + get effects() { + return []; + } + turnOn() { return Promise.reject(); } @@ -26,7 +30,19 @@ class HCColorlamp extends HCControlBase { return Promise.reject(); } - changeColor(hue, sat, light) { + /** + * Sets the color to the values given in the colors object + * Also accepts partial values (only update hue and sat for example) + * @param {Object} color + * @param {Number} color.hue Hue + * @param {Number} color.sat Saturation + * @param {Number} color.l Lightness + */ + changeColor(color) { + return Promise.reject(); + } + + setEffect(id) { return Promise.reject(); } } diff --git a/ControlBase.js b/ControlBase.js index e1eea24..49d6d19 100644 --- a/ControlBase.js +++ b/ControlBase.js @@ -9,8 +9,12 @@ class HCControlBase extends EventEmitter { constructor(configuration, state) { super(); + if (!configuration.name) throw new Error("Device name is missing"); + this._configuration = configuration; this._state = state; + + this._name = this._configuration.name; } get state() { @@ -21,6 +25,10 @@ class HCControlBase extends EventEmitter { return "none"; } + get name() { + return this._name; + } + /** * Updates the state by polling the controlled device * @returns A promise which resolves when the state has been pulled diff --git a/StateUpdateManager.js b/StateUpdateManager.js new file mode 100644 index 0000000..6fd890c --- /dev/null +++ b/StateUpdateManager.js @@ -0,0 +1,92 @@ +/** + * Optional utility classes can use when states need to be partially updated + */ + +class StateUpdateManager { + constructor(initialState) { + this._highestConfirmedId = 0; + this._currentId = 0; + this._nextId = 1; + + this._updateHistory = [{ id: 0, state: initialState }]; + } + + get highestConfirmedId() { + return this._highestConfirmedId; + } + + /** + * Returns the latest (unconfirmed state) + */ + get state() { + return this._updateHistory.find(su => su.id == this._currentId).state; + } + + /** + * Returns the latest confirmed state + */ + get confirmedState() { + return this._updateHistory.find(su => su.id == this._highestConfirmedId).state; + } + + /** + * Registers the potential update of the state + * @param {Object} state + * @returns {Number} id which is used to confirm or reject the update later + */ + registerUpdate(state) { + let updateId = this._nextId; + this._nextId += 1; + this._currentId = updateId; // until rejected, this is the most recent id + + this._updateHistory.push({ + id: updateId, + state + }); + + return updateId; + } + + /** + * Add a new state and instantly confirm it + * @param {Object} state + */ + insertConfirmedState(state) { + let suid = this.registerUpdate(); + this.confirmUpdate(suid); + } + + /** + * Confirm the update with the specified id + * Also remove all updates with lower ids, as these are now confirmed to be outdated + * @param {Number} uid + */ + confirmUpdate(uid) { + if (uid < this._highestConfirmedId) return; // already outdated + + this._highestConfirmedId = uid; + + // remove history entries with lower ids + this._updateHistory = this._updateHistory.filter(su => su.id >= this._highestConfirmedId); + } + + /** + * Rejects the update with the specified id + * If the id is outdated it is ignored + * Otherwise it is thrown out, and the currentId decremented if required + * @param {Number} uid + */ + rejectUpdate(uid) { + if (uid < this._highestConfirmedId) return; // already outdated + + // remove element from history + this._updateHistory = this._updateHistory.filter(su => su.id != uid); + + if (this._currentId == uid) { + // set current id to highest known uid + this._currentId = this._updateHistory.reduce((highest, su) => (su.id > highest) ? su.id : highest, -1); + } + } +} + +module.exports = StateUpdateManager; \ No newline at end of file diff --git a/index.js b/index.js index 20a320f..3c8b7ac 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ module.exports = { HCColorLamp: require('./ColorlampBase'), HCSwitch: require('./SwitchBase'), + StateUpdateManager: require('./StateUpdateManager'), }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index ecbaf21..58097d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,11 @@ "requires": { "is-plain-obj": "1.1.0" } + }, + "node-object-hash": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/node-object-hash/-/node-object-hash-1.4.1.tgz", + "integrity": "sha512-JQVqSM5/mOaUoUhCYR0t1vgm8RFo7qpJtPvnoFCLeqQh1xrfmr3BCD3nGBnACzpIEF7F7EVgqGD3O4lao/BY/A==" } } } diff --git a/package.json b/package.json index 23b7a50..6593126 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "author": "Jan Scheiper", "license": "UNLICENSED", "dependencies": { - "merge-options": "^1.0.1" + "merge-options": "^1.0.1", + "node-object-hash": "^1.4.1" } } diff --git a/states/BaseState.js b/states/BaseState.js index 8b96017..0fcf0ef 100644 --- a/states/BaseState.js +++ b/states/BaseState.js @@ -1,3 +1,5 @@ +const objectHash = require('node-object-hash'); + class BaseState { constructor(cloneObj) { } @@ -6,12 +8,16 @@ class BaseState { return {}; } + get hash() { + return objectHash(this.asObj); + } + static get default() { return {}; } clone() { - return this(this.asObj); + return new this.constructor(this.asObj); } } diff --git a/states/ColorlampState.js b/states/ColorlampState.js index 2632897..46889df 100644 --- a/states/ColorlampState.js +++ b/states/ColorlampState.js @@ -13,6 +13,7 @@ class ColorlampState extends BaseState { sat: cloneState.color.sat, l: cloneState.color.l }; + this.effect = cloneState.effect; } static get default() { @@ -23,7 +24,8 @@ class ColorlampState extends BaseState { hue: 0, sat: 0, l: 0 - } + }, + effect: "none" }; } @@ -35,7 +37,8 @@ class ColorlampState extends BaseState { hue: this.color.hue, sat: this.color.sat, l: this.color.l - } + }, + effect: this.effect }; } }