const { HCColorLamp, StateUpdateManager, utils, } = require('homecontrol-control-base'); const { ControlAddressable, AddressableColorStopMode } = require('magic-home'); const RBM_EFFECTS = new Array(100).fill(null).map((_,i) => { return { name: `Random Effect ${i}`, id: `rbm-${i}` }; }); class HCMagicHomeAddressable extends HCColorLamp { constructor(config, registry) { super(config); if (!("address" in this._configuration)) { throw new Error(`Required configuration field "address" is missing"`); } if (!("strip_length" in this._configuration)) { throw new Error(`Required configuration field "strip_length" is missing"`); } const opts = { log_all_received: (this._configuration.logall !== undefined) ? this._configuration.logall : false, connect_timeout: (this._configuration.connect_timeout !== undefined) ? this._configuration.connect_timeout : 10000, command_timeout: (this._configuration.command_timeout !== undefined) ? this._configuration.command_timeout : 5000, }; this._control = new ControlAddressable(this._configuration.address, opts); this._sumanager = new StateUpdateManager(this._state); this._registry = registry; if ("registry_key" in this._configuration) { this._registry[this._configuration.registry_key] = this; } this._subsections = []; this._initialUpdate = true; } async deinit() { if ("registry_key" in this._configuration) { delete this._registry[this._configuration.registry_key]; } } static get version() { return require("./package.json").version; } // overwrite to make use of the SUManager get state() { return this._sumanager.state.clone(); } get effects() { return [ { name: "Subsection Color Mode", id: "subsections" }, ...RBM_EFFECTS, ]; } _createSubsectionMode() { const currentState = this.state; const mode = new AddressableColorStopMode(this._configuration.strip_length); let { red: bgRed, green: bgGreen, blue: bgBlue } = utils.HSL_to_RGB(currentState.color); bgRed = Math.round(bgRed * currentState.brightness / 100); bgGreen = Math.round(bgGreen * currentState.brightness / 100); bgBlue = Math.round(bgBlue * currentState.brightness / 100); mode.addColorStop(0, bgRed, bgGreen, bgBlue); for (let i = 0; i < this._subsections.length; i++) { const ss = this._subsections[i]; if (ss.start >= this._configuration.strip_length) { break; } mode.addColorStop(ss.start, ss.color.red, ss.color.green, ss.color.blue); if (i < this._subsections.length - 1) { if (this._subsections[i+1].start > ss.end && ss.end < this._configuration.strip_length) { mode.addColorStop(ss.end, bgRed, bgGreen, bgBlue); } } else { if (ss.end < this._configuration.strip_length) { mode.addColorStop(ss.end, bgRed, bgGreen, bgBlue); } } } return mode; } turnOn() { let futureState = this.state; futureState.on = true; let suid = this._sumanager.registerUpdate(futureState); return this._control.setPower(true).then(success => { this._sumanager.confirmUpdate(suid); if (this._sumanager.highestConfirmedId == suid) { this.emit("state change", this.state); } return success; }).catch(err => { this._sumanager.rejectUpdate(suid); throw err; }); } turnOff() { let futureState = this.state; futureState.on = false; let suid = this._sumanager.registerUpdate(futureState); return this._control.setPower(false).then(success => { this._sumanager.confirmUpdate(suid); if (this._sumanager.highestConfirmedId == suid) { this.emit("state change", this.state); } return success; }).catch(err => { this._sumanager.rejectUpdate(suid); throw err; }); } // color = {red, green, blue} setSubsectionColor(start, end, ssid, color) { this._subsections = this._subsections.filter(ss => ss.id != ssid); this._subsections.push({ start, end, id: ssid, color }); this._subsections.sort((a,b) => a.start - b.start); return this.setEffect("subsections"); } removeSubsection(ssid) { this._subsections = this._subsections.filter(ss => ss.id != ssid); if (this._subsections.length > 0) { return this.setEffect("subsections"); } else { return this.setEffect("none"); } } isSubsectionActive(ssid) { return this.state.on && this.state.effect === "subsections" && this._subsections.find(ss => ss.id === ssid) !== null; } setBrightness(brightness) { let futureState = this.state; futureState.brightness = brightness; let suid = this._sumanager.registerUpdate(futureState); let promise; if (!["none", "subsections"].includes(futureState.effect)) { promise = this.setEffect(futureState.effect); } else { promise = this.setColor(futureState.color); } return promise.then(success => { this._sumanager.confirmUpdate(suid); if (this._sumanager.highestConfirmedId == suid) { this.emit("state change", this.state); } return success; }).catch(err => { this._sumanager.rejectUpdate(suid); throw err; }); } setColor(color) { const futureState = this.state; futureState.color = utils.fillPartialHSL(color, futureState.color); futureState.on = true; // setting color turns on the controller let { red, green, blue } = utils.HSL_to_RGB(futureState.color); red = Math.round(red * futureState.brightness / 100); green = Math.round(green * futureState.brightness / 100); blue = Math.round(blue * futureState.brightness / 100); let promise; if (futureState.effect !== "subsections") { futureState.effect = "none"; const suid = this._sumanager.registerUpdate(futureState); promise = this._control.setFixedMode({ effect: 1, foreground: { red, green, blue } }).then(success => { this._sumanager.confirmUpdate(suid); if (this._sumanager.highestConfirmedId == suid) { this.emit("state change", this.state); } return success; }); } else { const suid = this._sumanager.registerUpdate(futureState); const mode = this._createSubsectionMode(); return this._control.setMultiColorMode(mode).then(success => { this._sumanager.confirmUpdate(suid); if (this._sumanager.highestConfirmedId == suid) { this.emit("state change", this.state); } return success; }); } return promise.catch(err => { this._sumanager.rejectUpdate(suid); throw err; }); } setEffect(id) { let futureState = this.state; futureState.effect = id; let suid = this._sumanager.registerUpdate(futureState); let promise = this.turnOn(); if (futureState.effect == "none" || futureState.effect == "subsections") { promise = this.setColor(futureState.color).then(success => { this._sumanager.confirmUpdate(suid); if (this._sumanager.highestConfirmedId == suid) { this.emit("state change", this.state); } return success; }) } else { const rbmMatch = id.match(/rbm\-(\d+)/); if (rbmMatch === null) { return Promise.reject(); } const rbmMode = Number(rbmMatch[1]); promise = this._control.setRbmMode(rbmMode, futureState.brightness, 100).then(success => { this._sumanager.confirmUpdate(suid); if (this._sumanager.highestConfirmedId == suid) { this.emit("state change", this.state); } return success; }); } return promise.catch(err => { this._sumanager.rejectUpdate(suid); throw err; }); } pullState() { return this._control.queryState().then(status => { let currentState = this.state; let futureState = currentState.clone(); // console.log(status); futureState.on = status.on; // let effect = (status.mode != 'color' && status.mode != 'warm_white' && status.mode != 'special') ? status.mode : 'none'; // if(effect != 'none') { // let speed = (status.speed > 80) ? 'fast' : (status.speed > 30) ? 'medium' : 'slow'; // futureState.effect = speed + '-' + effect; // } else { // futureState.effect = 'none'; // } if (status.mode == "fixed" || this._initialUpdate) { // only update color when no effects are playing futureState.brightness = extractBrightness(status.color); futureState.color = utils.RGB_to_HSL(removeBrightness(status.color)); } if (status.mode === "rbm") { futureState.effect = "rbm-" + status.effect; } // console.log(futureState); if (currentState.hash != futureState.hash) { // the state of the controller has changed externally this._sumanager.insertConfirmedState(futureState); this.emit("state change", this.state); } this._initialUpdate = false; }); } } module.exports = HCMagicHomeAddressable; function extractBrightness(rgb) { let max = Math.max(rgb.red, rgb.green, rgb.blue); return (max / 255) * 100; } function removeBrightness(rgb) { let max = Math.max(rgb.red, rgb.green, rgb.blue); let scale = (max > 0) ? 255 / max : 0; return { red: rgb.red * scale, green: rgb.green * scale, blue: rgb.blue * scale, }; }