const { HCColorLamp, StateUpdateManager, utils } = require('homecontrol-control-base'); const axios = require('axios'); const { URL } = require('url'); class ColorJumpEffect { constructor(colors, delay, sendCommand) { this._colors = colors; this._delay = delay; this._next = 0; this.sendCommand = sendCommand; } get delay() { return this._delay; } step() { let color = this._colors[this._next]; let cmd = `Color ${color[0]},${color[1]},${color[2]}`; this._next = (this._next + 1) % this._colors.length; // console.log(cmd); return this.sendCommand(cmd); } init() { return Promise.resolve(); } deinit() { return Promise.resolve(); } } class SchemeEffect { constructor(scheme, speed, sendCommand) { this._scheme = scheme; this._speed = speed; this.sendCommand = sendCommand; } init() { return this.sendCommand(`Backlog Speed ${this._speed}; Scheme ${this._scheme}`); } deinit() { return this.sendCommand(`Scheme 0`); } } class HCTasmota extends HCColorLamp { constructor(config) { super(config); if (!("address" in this._configuration)) { throw new Error(`Required configuration field "address" is missing"`); } this._sumanager = new StateUpdateManager(this._state); this._effectInterval = null; this._effect = null; } // overwrite to make use of the SUManager get state() { return this._sumanager.state.clone(); } get effects() { return [ { name: "Color Jump 3 SLOW", id: "jump3-slow" }, { name: "Color Jump 3 FAST", id: "jump3-fast" }, { name: "Color Cycle Up SLOW", id: "scheme-2-slow" }, { name: "Color Cycle Up FAST", id: "scheme-2-fast" }, { name: "Color Cycle Down SLOW", id: "scheme-3-slow" }, { name: "Color Cycle Down FAST", id: "scheme-3-fast" }, { name: "Color Cycle Random SLOW", id: "scheme-4-slow" }, { name: "Color Cycle Random FAST", id: "scheme-4-fast" }, ]; } _sendCommand(cmd) { let url = new URL("/cm", this._configuration.address); url.searchParams.append("cmnd", cmd); return axios.post(url.toString()); } init() { return this._sendCommand("State").then(res => { if (res.data.Scheme != 0) { return this._sendCommand("Scheme 0"); } }); } turnOn() { if (this.state.effect != "none") { return this.setEffect("none").then(() => this.turnOn()); } let futureState = this.state; futureState.on = true; let suid = this._sumanager.registerUpdate(futureState); return this._sendCommand("Power on").then(resolveHelper(this, suid), rejectHelper(this, suid)); } turnOff() { if (this.state.effect != "none") { return this.setEffect("none").then(() => this.turnOff()); } let futureState = this.state; futureState.on = false; let suid = this._sumanager.registerUpdate(futureState); return this._sendCommand("Power off").then(resolveHelper(this, suid), rejectHelper(this, suid)); } setBrightness(brightness) { if (this.state.effect != "none") { return this.setEffect("none").then(() => this.setBrightness(brightness)); } let futureState = this.state; futureState.brightness = Math.round(brightness); futureState.on = true; let suid = this._sumanager.registerUpdate(futureState); return this._sendCommand(`HsbColor3 ${futureState.brightness}`).then(resolveHelper(this, suid), rejectHelper(this, suid)); } setColor(color) { if (this.state.effect != "none") { return this.setEffect("none").then(() => this.setColor(color)); } let futureState = this.state; futureState.on = true; futureState.color = utils.fillPartialHSL(color, futureState.color); futureState.color.hue = Math.round(futureState.color.hue); futureState.color.sat = Math.round(futureState.color.sat); futureState.color.l = Math.round(futureState.color.l); let suid = this._sumanager.registerUpdate(futureState); let url = new URL("/cm", this._configuration.address); url.searchParams.append("cmnd", `HsbColor ${futureState.color.hue},${futureState.color.sat},${futureState.brightness}`); return axios.post(url.toString()).then(resolveHelper(this, suid), rejectHelper(this, suid)); } setEffect(id) { let futureState = this.state; let promise; if (this._effectInterval != null) { clearTimeout(this._effectInterval); } if (this._effect != null) { promise = this._effect.deinit(); } else { promise = Promise.resolve(); } if (id == "none") { promise.then(() => { this._effectInterval = null; this._effect = null; futureState.effect = "none"; let url = new URL("/cm", this._configuration.address); url.searchParams.append("cmnd", `HsbColor ${Math.round(futureState.color.hue)},${Math.round(futureState.color.sat)},${futureState.brightness}`); return axios.post(url.toString()); }); } else { promise.then(() => { switch(id) { case "jump3-fast": futureState.effect = id; this._effect = new ColorJumpEffect([ [255, 0, 0], [0, 255, 0], [0, 0, 255], ], 200, this._sendCommand.bind(this)); return this._effect.init().then(() => { return this._stepEffect(); }); case "jump3-slow": futureState.effect = id; this._effect = new ColorJumpEffect([ [255, 0, 0], [0, 255, 0], [0, 0, 255], ], 800, this._sendCommand.bind(this)); return this._effect.init().then(() => { return this._stepEffect(); }); case "scheme-2-slow": futureState.effect = id; this._effect = new SchemeEffect(2, 30, this._sendCommand.bind(this)); return this._effect.init(); case "scheme-3-slow": futureState.effect = id; this._effect = new SchemeEffect(3, 30, this._sendCommand.bind(this)); return this._effect.init(); case "scheme-4-slow": futureState.effect = id; this._effect = new SchemeEffect(4, 30, this._sendCommand.bind(this)); return this._effect.init(); case "scheme-2-fast": futureState.effect = id; this._effect = new SchemeEffect(2, 1, this._sendCommand.bind(this)); return this._effect.init(); case "scheme-3-fast": futureState.effect = id; this._effect = new SchemeEffect(3, 1, this._sendCommand.bind(this)); return this._effect.init(); case "scheme-4-fast": futureState.effect = id; this._effect = new SchemeEffect(4, 1, this._sendCommand.bind(this)); return this._effect.init(); default: return Promise.reject(new Error("Invalid effect id")); } }); } let suid = this._sumanager.registerUpdate(futureState); return promise.then(resolveHelper(this, suid), rejectHelper(this, suid)); } pullState() { let url = new URL("/cm", this._configuration.address); url.searchParams.append("cmnd", `State`); return axios.get(url.toString()).then(res => { if (res.status) { let currentState = this.state; let futureState = currentState.clone(); let { hsl, brightness } = parseHsbString(res.data.HSBColor); // only update color info if we're not currently changing it constantly with an effect if (currentState.effect == "none") { futureState.color = hsl; if (res.data.Scheme != 0) { this.sendCommand("Scheme 0"); } } futureState.on = res.data.POWER == "ON"; futureState.brightness = brightness; if (currentState.hash != futureState.hash) { // the state of the controller has changed externally this._sumanager.insertConfirmedState(futureState); this.emit("state change", this.state); } } else { throw new Error(`Got HTTP ${res.status}: ${res.data}`); } }); } _stepEffect() { if (this._effect != null) { return this._effect.step().then(() => { this._effectInterval = setTimeout(() => this._stepEffect(), this._effect.delay); }); } { return Promise.resolve(); } } } module.exports = HCTasmota; function rejectHelper(self, suid) { return (err) => { self._sumanager.rejectUpdate(suid); throw err; // rethrow so we can catch it at a higher level }; } function resolveHelper(self, suid) { return () => { self._sumanager.confirmUpdate(suid); if (self._sumanager.highestConfirmedId == suid) { self.emit("state change", self.state); } }; } function parseHsbString(hsb) { let parts = hsb.split(","); let hue = Number(parts[0]); let sat = Number(parts[1]); let brightness = Number(parts[2]); let hsl = { hue, sat, l: 100 - sat/100 * 50, }; return { hsl, brightness }; }