const { HCColorLamp, StateUpdateManager, utils } = require('homecontrol-control-base'); const mqtt = require("async-mqtt"); class HCschalkematrix extends HCColorLamp { constructor(config) { super(config); if (!("server" in this._configuration)) { throw new Error(`Required configuration field "server" is missing"`); } if (!("control_topic" in this._configuration)) { throw new Error(`Required configuration field "control_topic" is missing"`); } if (!("state_topic" in this._configuration)) { throw new Error(`Required configuration field "state_topic" is missing"`); } if (!("max_brightness" in this._configuration)) { this._configuration.max_brightness = 150; } this._client = null; this._sumanager = new StateUpdateManager(this._state); } 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: "RGB Strobe", id: "1-80" }, {name: "Strobe", id: "2-99" }, {name: "Police", id: "6-90" }, {name: "Arrow Right SLOW", id: "3-40" }, {name: "Arrow Right FAST", id: "3-90" }, {name: "Arrow Left SLOW", id: "4-40" }, {name: "Arrow Left FAST", id: "4-90" }, {name: "Arrow Split SLOW", id: "5-50" }, {name: "Arrow Split FAST", id: "5-93" }, {name: "Wave White SLOW", id: "7-50" }, {name: "Wave White FAST", id: "7-90" }, {name: "Wave Red/Green SLOW", id: "10-50" }, {name: "Wave Red/Green FAST", id: "10-90" }, {name: "Spinner White", id: "11-80" }, {name: "Spinner Double", id: "14-80" }, {name: "Full Rainbow SLOW", id: "15-20" }, {name: "Full Rainbow MEDIUM", id: "15-50" }, {name: "Full Rainbow FAST", id: "15-90" }, {name: "Rainbow Slide SLOW", id: "17-50" }, {name: "Rainbow Slide FAST", id: "18-80" }, ]; } async init() { this._client = await mqtt.connectAsync(this._configuration.server); const initialState = this._sumanager.state; initialState.color = utils.RGB_to_HSL({red: 255, green: 255, blue: 255 }); initialState.brightness = Math.round((128 / this._configuration.max_brightness) * 100); this._sumanager.insertConfirmedState(initialState); this._client.on("message", (topic, msgRaw) => { let msg; try { msg = JSON.parse(msgRaw.toString()); } catch(e) { return; } let newState = this.state; newState.brightness = (msg.brightness / this._configuration.max_brightness) * 100; newState.on = state_isOn(msg); // only overwrite color if it isn't black if (msg.color.r != 0 || msg.color.g != 0 || msg.color.b != 0) { newState.color = utils.RGB_to_HSL({ red: msg.color.r, green: msg.color.g, blue: msg.color.b }); } let speeds = {}; for (let eff of this.effects) { const matches = eff.id.match(/(\d+)-(\d+)/); if (speeds[matches[1]] == undefined) { speeds[matches[1]] = []; } speeds[matches[1]].push({speed: Number(matches[2]), id: eff.id }); } // try to match the effect as best as possible if (speeds[msg.effect] != undefined) { if (speeds[msg.effect].length == 1) { newState.effect = speeds[msg.effect][0].id; } else { let lowestDist = Infinity; let lowestDistId = null; for (let speed of speeds[msg.effect]) { if (Math.abs(speed.speed - msg.speed) < lowestDist) { lowestDist = Math.abs(speed.speed - msg.speed); lowestDistId = speed.id; } } newState.effect = lowestDistId; } } else if (msg.effect == 0) { newState.effect = "none"; } this._sumanager.insertConfirmedState(newState); this.emit("state change", this.state); }); await this._client.subscribe(this._configuration.state_topic); } async deinit() { await this._client.end(); } turnOn() { let futureState = this.state; futureState.on = true; futureState.brightness = (128 / this._configuration.max_brightness) * 100; let suid = this._sumanager.registerUpdate(futureState); const prevColor = utils.HSL_to_RGB(this.state.color); return Promise.all([ this._client.publish(this._configuration.control_topic, JSON.stringify({ type: "color", r: prevColor.red, g: prevColor.green, b: prevColor.blue, exclusive: true, })), this._client.publish(this._configuration.control_topic, JSON.stringify({ type: "brightness", brightness: 128, })), ]).then(() => { this._sumanager.confirmUpdate(suid); if (this._sumanager.highestConfirmedId == suid) { this.emit("state change", this.state); } return true; }).catch(err => { this._sumanager.rejectUpdate(suid); throw err; }); } turnOff() { let futureState = this.state; futureState.on = false; futureState.brightness = (30 / this._configuration.max_brightness) * 100; let suid = this._sumanager.registerUpdate(futureState); return Promise.all([ this._client.publish(this._configuration.control_topic, JSON.stringify({ type: "color", r: 0, g: 0, b: 0, exclusive: false, })), this._client.publish(this._configuration.control_topic, JSON.stringify({ type: "clock", })), this._client.publish(this._configuration.control_topic, JSON.stringify({ type: "brightness", brightness: 30, })), ]).then(() => { this._sumanager.confirmUpdate(suid); if (this._sumanager.highestConfirmedId == suid) { this.emit("state change", this.state); } return true; }).catch(err => { this._sumanager.rejectUpdate(suid); throw err; }); } setBrightness(brightness) { let futureState = this.state; futureState.brightness = brightness; let suid = this._sumanager.registerUpdate(futureState); return this._client.publish(this._configuration.control_topic, JSON.stringify({ type: "brightness", brightness: Math.round(brightness / 100 * this._configuration.max_brightness), })).then(() => { this._sumanager.confirmUpdate(suid); if (this._sumanager.highestConfirmedId == suid) { this.emit("state change", this.state); } return true; }).catch(err => { this._sumanager.rejectUpdate(suid); throw err; }); } setColor(color) { let futureState = this.state; futureState.color = utils.fillPartialHSL(color, futureState.color); futureState.effect = "none"; let suid = this._sumanager.registerUpdate(futureState); let { red, green, blue } = utils.HSL_to_RGB(futureState.color); return this._client.publish(this._configuration.control_topic, JSON.stringify({ type: "color", r: red, g: green, b: blue, exclusive: true, })).then(() => { this._sumanager.confirmUpdate(suid); if (this._sumanager.highestConfirmedId == suid) { this.emit("state change", this.state); } return true; }).catch(err => { this._sumanager.rejectUpdate(suid); throw err; }); } setEffect(id) { const matches = id.match(/(\d+)-(\d+)/); let effect = null; let speed = null; if (matches != null) { effect = matches[1]; speed = matches[2]; } const futureState = this.state; let promise; if (id == "none" || effect == null || speed == null) { futureState.effect = "none"; let { red, green, blue } = utils.HSL_to_RGB(futureState.color); promise = this._client.publish(this._configuration.control_topic, JSON.stringify({ type: "color", r: red, g: green, b: blue, exclusive: true, })); } else { futureState.effect = id; // id is "valid" / can be parsed promise = this._client.publish(this._configuration.control_topic, JSON.stringify({ type: "effect", effect: effect, speed: speed, exclusive: true, })); } const suid = this._sumanager.registerUpdate(futureState); return promise.then(() => { this._sumanager.confirmUpdate(suid); if (this._sumanager.highestConfirmedId == suid) { this.emit("state change", this.state); } return true; }).catch(err => { this._sumanager.rejectUpdate(suid); throw err; }); } } module.exports = HCschalkematrix; function state_isOn(state) { return (state.color.r != 0 || state.color.g != 0 || state.color.b != 0 || state.color.overlay == 0 || state.effect != 0); }