From 3abfc232b7568850261f5ccad4bf9236686f5e13 Mon Sep 17 00:00:00 2001 From: Jan Scheiper Date: Sun, 16 Feb 2020 18:06:07 +0100 Subject: [PATCH] initial commit --- .gitignore | 1 + HCTasmota.js | 136 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 ++ package-lock.json | 63 +++++++++++++++++++++ package.json | 19 +++++++ 5 files changed, 223 insertions(+) create mode 100644 .gitignore create mode 100644 HCTasmota.js create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/HCTasmota.js b/HCTasmota.js new file mode 100644 index 0000000..ad28dcd --- /dev/null +++ b/HCTasmota.js @@ -0,0 +1,136 @@ +const { + HCColorLamp, + StateUpdateManager, + utils +} = require('homecontrol-control-base'); + +const axios = require('axios'); + +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); + } + + // overwrite to make use of the SUManager + get state() { + return this._sumanager.state.clone(); + } + + turnOn() { + let futureState = this.state; + futureState.on = true; + let suid = this._sumanager.registerUpdate(futureState); + + let url = new URL("/cm", this._configuration.address); + url.searchParams.append("cmnd", "Power on"); + + return axios.post(url.toString()).then(resolveHelper(this, suid), rejectHelper(this, suid)); + } + + turnOff() { + let futureState = this.state; + futureState.on = false; + let suid = this._sumanager.registerUpdate(futureState); + + let url = new URL("/cm", this._configuration.address); + url.searchParams.append("cmnd", "Power off"); + + return axios.post(url.toString()).then(resolveHelper(this, suid), rejectHelper(this, suid)); + } + + setBrightness(brightness) { + let futureState = this.state; + futureState.brightness = brightness; + let suid = this._sumanager.registerUpdate(futureState); + + let url = new URL("/cm", this._configuration.address); + url.searchParams.append("cmnd", `HsbColor3 ${Math.round(brightness)}`); + + return axios.post(url.toString()).then(resolveHelper(this, suid), rejectHelper(this, suid)); + } + + setColor(color) { + let futureState = this.state; + futureState.color = utils.fillPartialHSL(color, futureState.color); + let suid = this._sumanager.registerUpdate(futureState); + + let url = new URL("/cm", this._configuration.address); + url.searchParams.append("cmnd", `HsbColor ${Math.round(color.hue)},${Math.round(color.sat)},${futureState.brightness}`); + + return axios.post(url.toString()).then(resolveHelper(this, suid), rejectHelper(this, suid)); + } + + setEffect(id) { + + } + + 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); + + futureState.on = res.data.POWER == "ON"; + futureState.color = hsl; + 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}`); + } + }); + } +} + +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 + }; +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b28e90d --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +## Configuration + +- `address` +HTTP base url of the device (e.g. `http://192.168.0.225`) **!! Do not forget the `http://` !!** \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8d84ff0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,63 @@ +{ + "name": "hc-tasmota", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "homecontrol-control-base": { + "version": "git+https://git.literalchaos.de/jan/homecontrol-control-base.git#b2bf9ba767050ec1581dd3641885fe1132ac45aa", + "from": "git+https://git.literalchaos.de/jan/homecontrol-control-base.git", + "requires": { + "merge-options": "^1.0.1", + "node-object-hash": "^1.4.1" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "merge-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-1.0.1.tgz", + "integrity": "sha512-iuPV41VWKWBIOpBsjoxjDZw8/GbSfZ2mk7N1453bwMrfzdrIk7EzBd+8UVR6rkw67th7xnk9Dytl3J+lHPdxvg==", + "requires": { + "is-plain-obj": "^1.1" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node-object-hash": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/node-object-hash/-/node-object-hash-1.4.2.tgz", + "integrity": "sha512-UdS4swXs85fCGWWf6t6DMGgpN/vnlKeSGEQ7hJcrs7PBFoxoKLmibc3QRb7fwiYsjdL7PX8iI/TMSlZ90dgHhQ==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..518b39a --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "hc-tasmota", + "version": "1.0.0", + "description": "A plugin to support the communication with Tasmota devices via http", + "main": "HCTasmota.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git@git.literalchaos.de:jan/hc-tasmota.git" + }, + "author": "Jan Scheiper", + "license": "UNLICENSED", + "dependencies": { + "axios": "^0.19.2", + "homecontrol-control-base": "git+https://git.literalchaos.de/jan/homecontrol-control-base.git" + } +}