From cbea9a6058532e9b881ddcac5fd951f770b5e0cd Mon Sep 17 00:00:00 2001 From: Christoph Willing Date: Thu, 7 Sep 2017 09:54:05 +1000 Subject: [PATCH] Initial iSpindel support --- Makefile | 1 + src/scripts/modules/configuration.js | 35 +- src/scripts/modules/fhem.js | 222 ++++++++++++ src/scripts/modules/gpioworker.js | 42 ++- src/scripts/modules/requestHandlers.js | 15 +- src/scripts/status.js | 457 ++++++++++++++++++++++++- styles/brewable.css | 77 ++++- test-status.js | 134 -------- 8 files changed, 812 insertions(+), 171 deletions(-) create mode 100644 src/scripts/modules/fhem.js delete mode 100644 test-status.js diff --git a/Makefile b/Makefile index 88da79c..97d365b 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,7 @@ SERVER_FILES = \ src/scripts/modules/server.js CLIENT_FILES = \ + styles/brewable.css \ src/scripts/status.js \ TEST_FILES = test-status.js \ diff --git a/src/scripts/modules/configuration.js b/src/scripts/modules/configuration.js index f494343..8fcaec1 100644 --- a/src/scripts/modules/configuration.js +++ b/src/scripts/modules/configuration.js @@ -10,12 +10,9 @@ var defaultConfigValues = function() { 'multiSensorMeanWeight' : parseInt(50), 'relayDelayPostON' : parseInt(180), 'relayDelayPostOFF' : parseInt(480), - 'iSpindelA' : { - "name":"iSpindel", - "calfactor0":0.00438551, - "calfactor1":0.13647658, - "calfactor2":6.968821422 - } + 'iSpindels' : [ + { "name":"iSpindel", "timeout":60 } + ] }; }; @@ -59,6 +56,7 @@ Configuration.prototype.loadConfigFromFile = function () { console.log("conf file: " + path.join(this._projectConfigDir, this._project + ".conf")); try { this._configuration = JSON.parse(fs.readFileSync(this._configFileName, 'utf8')); + console.log("config from file: " + JSON.stringify(this._configuration)); } catch (err) { console.log("Error reading " + this._configFileName + ". Using default configuration"); @@ -183,6 +181,31 @@ Configuration.prototype.setFudgeFactor = function (key, val) { fs.writeFileSync(this._configFileName, JSON.stringify(config)); }; +Configuration.prototype.setIspindelTimeout = function (key, val) { + console.log("setIspindelTimeout() " + key + ", val " + val); + + var config = this._configuration; + var found = false; + config.iSpindels.forEach( function(item) { + if (item.name == key ) { + found = true; + item.timeout = val; + } + }); + if (!found) { + config.iSpindels.push({"name":key, "timeout":val}); + } + fs.writeFileSync(this._configFileName, JSON.stringify(config)); +}; + +Configuration.prototype.newIspindel = function (name) { + var config = this._configuration; + var defaultTimeout = defaultConfigValues().iSpindels[0].timeout; + config.iSpindels.push({"name":name, "timeout":defaultTimeout}); + fs.writeFileSync(this._configFileName, JSON.stringify(config)); + this.loadConfigFromFile(); +}; + /******** atest = new Configuration(); var config = atest.getConfiguration(); diff --git a/src/scripts/modules/fhem.js b/src/scripts/modules/fhem.js new file mode 100644 index 0000000..e38217c --- /dev/null +++ b/src/scripts/modules/fhem.js @@ -0,0 +1,222 @@ +/* + The only communication from an iSpindel device is its periodic data report. + We have no way of determining whether an iSpindel device is being used + (or multiple devices) other than the data reports. We therefore start out + assuming there are no iSPindel devices. As each device submits a data report, + we instantiate an fhemDevice object and add it to fhemDeviceList. + + The fhemDevice object maintains a record of the last received data report + along with the time it was received. If we haven't heard from a device for + some predermined time, it is removed from fhemDeviceList by deviceReaper() + which runs periodically (started when the first iSpindel is detected). + + Data reports are not frequent - the isPindel documentation mentions: + "With an update interval of 30min it was possible to achive a + battery lifetime of almost 3 months!" (sic). + Therefore when each data report is received, we emit "fhem_reading" so that + any interested modules can immediately do something with the newly received + data. +*/ +import querystring from "querystring"; +import { eventEmitter } from "./gpioworker"; +import Configuration from "./configuration"; + + +/* + FHEM arithmetic from + https://github.com/universam1/iSpindel/blob/master/docs/upload-FHEM_en.md +*/ +var correctPlato = function (plato, temp) { + var k; + + if (plato < 5) k = [56.084, -0.17885, -0.13063]; + else if (plato >= 5) k = [69.685, -1.367, -0.10621]; + else if (plato >= 10) k = [77.782, -1.7288, -0.10822]; + else if (plato >= 15) k = [87.895, -2.3601, -0.10285]; + else if (plato >= 20) k = [97.052, -2.7729, -0.10596]; + + var cPlato = k[0] + k[1] * temp + k[2] * temp*temp; + return plato - cPlato/100; +}; +var calcPlato = function (tilt, temp) { + // Get this from Excel Calibration at 20 Degrees + var plato=0.00438551*tilt*tilt + 0.13647658*tilt - 6.968821422; + + return correctPlato(plato, temp); +}; + +var fhemDeviceList = []; +var searchDeviceListByName = function (name) { + var result; + for (var i=0;i ispindel.waitTime ) { + clearInterval(ispindel.ispindelUpdateInterval); + console.log("Toooo long since last report"); + ispindel.removeOverlay(); + var x = document.getElementById(ispindel.elementName); + if (x) x.parentNode.removeChild(x); + + var itemToRemove = -1; + iSpindelDevices.forEach( function (item, index) { + if (item.name == ispindel.name) { + itemToRemove = index; + } + }); + console.log("Removing item " + itemToRemove + " from " + JSON.stringify(iSpindelDevices)); + if (itemToRemove > -1 ) iSpindelDevices.splice(itemToRemove,1); + console.log("Changed iSpindelDevices: " + JSON.stringify(iSpindelDevices)); + return; + } + } + + showOverlay () { + var el = document.getElementById(this.elementName); + + // Size of enclosing DIV (css isp_sensor_update) + var els = {"w":128, "h":64}; + // Size of overlay (css .isp_sol) + var ols = {"w":168, "h":152}; + + this.overlay = document.createElement("DIV"); + this.overlay.id = this.elementName + "_overlay"; + this.overlay.className = "isp_sol unselectable"; + this.overlay.style.position = "fixed"; + this.overlay.style.top = getOffsetRect(el).top - (ols.h - els.h)/2 + 'px'; + this.overlay.style.left = getOffsetRect(el).left - (ols.w - els.w)/2 + 'px'; + document.body.appendChild(this.overlay); + + // Overlay contents + var olTitle = document.createElement("DIV"); + olTitle.id = this.elementName + "_overlay_title"; + olTitle.className = "isp_sol_title unselectable"; + olTitle.textContent = this.name; + this.overlay.appendChild(olTitle); + + var olTilt = document.createElement("DIV"); + olTilt.id = this.elementName + "_overlay_tilt"; + olTilt.className = "isp_sol_detail unselectable"; + var olTiltKey = document.createElement("DIV"); + olTiltKey.id = this.elementName + "_overlay_tilt_key"; + olTiltKey.className = "isp_sol_key unselectable"; + olTiltKey.textContent = "Tilt:"; + var olTiltVal = document.createElement("DIV"); + olTiltVal.id = this.elementName + "_overlay_tilt_val"; + olTiltVal.className = "isp_sol_val unselectable"; + olTiltVal.textContent = this.tilt + "\u00B0"; + olTilt.appendChild(olTiltKey); + olTilt.appendChild(olTiltVal); + this.overlay.appendChild(olTilt); + + var olTemp = document.createElement("DIV"); + olTemp.id = this.elementName + "_overlay_temp"; + olTemp.className = "isp_sol_detail unselectable"; + var olTempKey = document.createElement("DIV"); + olTempKey.id = this.elementName + "_overlay_temp_key"; + olTempKey.className = "isp_sol_key unselectable"; + olTempKey.textContent = "Temp:"; + var olTempVal = document.createElement("DIV"); + olTempVal.id = this.elementName + "_overlay_temp_val"; + olTempVal.className = "isp_sol_val unselectable"; + olTempVal.textContent = this.temp + "\u00B0"; + olTemp.appendChild(olTempKey); + olTemp.appendChild(olTempVal); + this.overlay.appendChild(olTemp); + + var olGrav = document.createElement("DIV"); + olGrav.id = this.elementName + "_overlay_grav"; + olGrav.className = "isp_sol_detail unselectable"; + var olGravKey = document.createElement("DIV"); + olGravKey.id = this.elementName + "_overlay_grav_key"; + olGravKey.className = "isp_sol_key unselectable"; + olGravKey.textContent = "Grav:"; + var olGravVal = document.createElement("DIV"); + olGravVal.id = this.elementName + "_overlay_grav_val"; + olGravVal.className = "isp_sol_val unselectable"; + olGravVal.textContent = this.grav.toFixed(2); + olGrav.appendChild(olGravKey); + olGrav.appendChild(olGravVal); + this.overlay.appendChild(olGrav); + + /* + var olPlato = document.createElement("DIV"); + olPlato.id = this.elementName + "_overlay_plato"; + olPlato.className = "isp_sol_detail unselectable"; + var olPlatoKey = document.createElement("DIV"); + olPlatoKey.id = this.elementName + "_overlay_plato_key"; + olPlatoKey.className = "isp_sol_key unselectable"; + olPlatoKey.textContent = "Plato:"; + var olPlatoVal = document.createElement("DIV"); + olPlatoVal.id = this.elementName + "_overlay_plato_val"; + olPlatoVal.className = "isp_sol_val unselectable"; + olPlatoVal.textContent = this.plato.toFixed(2); + olPlato.appendChild(olPlatoKey); + olPlato.appendChild(olPlatoVal); + this.overlay.appendChild(olPlato); + */ + + var olBatt = document.createElement("DIV"); + olBatt.id = this.elementName + "_overlay_batt"; + olBatt.className = "isp_sol_detail unselectable"; + var olBattKey = document.createElement("DIV"); + olBattKey.id = this.elementName + "_overlay_batt_key"; + olBattKey.className = "isp_sol_key unselectable"; + olBattKey.textContent = "Batt:"; + var olBattVal = document.createElement("DIV"); + olBattVal.id = this.elementName + "_overlay_batt_val"; + olBattVal.className = "isp_sol_val unselectable"; + olBattVal.textContent = this.batt + "v"; + olBatt.appendChild(olBattKey); + olBatt.appendChild(olBattVal); + this.overlay.appendChild(olBatt); + + var olLast = document.createElement("DIV"); + olLast.id = this.elementName + "_overlay_last"; + olLast.className = "isp_sol_detail unselectable"; + var olLastKey = document.createElement("DIV"); + olLastKey.id = this.elementName + "_overlay_last_key"; + olLastKey.className = "isp_sol_key unselectable"; + olLastKey.textContent = "Last:"; + var olLastVal = document.createElement("DIV"); + olLastVal.id = this.elementName + "_overlay_last_val"; + olLastVal.className = "isp_sol_val unselectable"; + olLastVal.textContent = forHumans(parseInt((new Date() - new Date(this.stamp))/1000)); + olLast.appendChild(olLastKey); + olLast.appendChild(olLastVal); + this.overlay.appendChild(olLast); + + this.overlayUpdateInterval = setInterval(this.checkOverlayTimeout, 1000, this); + + this.overlay.addEventListener("mouseout", function () { + this.removeOverlay(); + }.bind(this)); + + this.overlay.addEventListener("dblclick", function (e) { + console.log("Pressed button " + e.button + " at " + this.name); + this.configureIspindel(this); + }.bind(this)); + + } + + configureIspindel (ispindel) { + console.log("configureIspindel() " + ispindel.name); + ispindel.removeOverlay(); + location.href = '#content_4'; + } + + checkOverlayTimeout (ispindel) { + //console.log("checkOverlayTimeout() " + ispindel.stamp); + + var elapsed = new Date() - new Date(ispindel.stamp); + if (elapsed > ispindel.waitTime ) { + console.log("Too long since last report"); + ispindel.removeOverlay(); + var x = document.getElementById(ispindel.elementName); + if (x) x.parentNode.removeChild(x); + return; + } + + var olLastVal = document.getElementById(ispindel.elementName + "_overlay_last_val"); + if (olLastVal) { + olLastVal.textContent = forHumans(parseInt((new Date() - new Date(ispindel.stamp))/1000)); + } + } + removeOverlay () { + var x = document.getElementById(this.elementName + "_overlay"); + if (x) document.body.removeChild(x); + if (this.overlayUpdateInterval) clearInterval(this.overlayUpdateInterval); + } +} // main() //domReady( function(){ @@ -546,6 +824,7 @@ window.onload = function () { socket.send(message.data); }; + function live_update(data) { var sensor_state = data.sensor_state; var relay_state = data.relay_state; @@ -583,6 +862,8 @@ window.onload = function () { var tempScale = el.getAttribute('tempScale'); el.textContent = 'Temp. (' + tempScale + '):'; + var isp; + var existingIspNames; for (i=0;i