diff --git a/README.md b/README.md index 414a7a6d..592aaddf 100755 --- a/README.md +++ b/README.md @@ -93,21 +93,19 @@ Ready for 6.0; * [Vera Home Automation Hub](https://github.com/rstrouse/nodejs-poolController-veraPlugin) - A plugin that integrates with nodejs-poolController. * [SmartThings/Hubitat](https://github.com/bsileo/hubitat_poolcontroller) by @bsileo (prev help from @johnny2678, @donkarnag, @arrmo) * [Homebridge/Siri/EVE](https://github.com/gadget-monk/homebridge-poolcontroller) by @gadget-monk, adopted from @leftyflip +* InfluxDB Need to be updated: * [Another SmartThings Controller](https://github.com/dhop90/pentair-pool-controller/blob/master/README.md) by @dhop90 * [ISY](src/integrations/socketISY.js). Original credit to @blueman2, enhancements by @mayermd * [ISY Polyglot NodeServer](https://github.com/brianmtreese/nodejs-pool-controller-polyglotv2) created by @brianmtreese * [MQTT](https://github.com/crsherman/nodejs-poolController-mqtt) created by @crsherman. -* InfluxDB # Support 1. For discussions, recommendations, designs, and clarifications, we recommend you join our [Gitter Chat room](https://gitter.im/pentair_pool/Lobby). 1. Check the [wiki](https://github.com/tagyoureit/nodejs-poolController/wiki) for tips, tricks and additional documentation. 1. For bug reports you can open a [github issue](https://github.com/tagyoureit/nodejs-poolController/issues/new), - - ### Virtual Controller v6 adds all new configuration and support for virtual pumps, chlorinators (and soon, Intellichem) @@ -115,7 +113,6 @@ v6 adds all new configuration and support for virtual pumps, chlorinators (and s * [Virtual Chlorinator Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Virtual-Chlorinator-Controller-v6) - # Changed/dropped since 5.3 1. Ability to load different config.json files 1. Automatic upgrade of config.json files (tbd) diff --git a/config/Config.ts b/config/Config.ts index e94c39ea..0e94a6f4 100755 --- a/config/Config.ts +++ b/config/Config.ts @@ -1,19 +1,19 @@ -/* nodejs-poolController. An application to control pool equipment. -Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -*/ +/* nodejs-poolController. An application to control pool equipment. +Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ import * as path from "path"; import * as fs from "fs"; const extend = require("extend"); @@ -22,15 +22,31 @@ class Config { private cfgPath: string; private _cfg: any; private _isInitialized: boolean=false; + private _fileTime: Date = new Date(0); + private _isLoading: boolean = false; constructor() { + let self=this; this.cfgPath = path.posix.join(process.cwd(), "/config.json"); // RKS 05-18-20: This originally had multiple points of failure where it was not in the try/catch. try { + this._isLoading = true; this._cfg = fs.existsSync(this.cfgPath) ? JSON.parse(fs.readFileSync(this.cfgPath, "utf8")) : {}; const def = JSON.parse(fs.readFileSync(path.join(process.cwd(), "/defaultConfig.json"), "utf8").trim()); const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), "/package.json"), "utf8").trim()); this._cfg = extend(true, {}, def, this._cfg, { appVersion: packageJson.version }); this._isInitialized = true; + fs.watch(this.cfgPath, (event, fileName) => { + if (fileName && event === 'change') { + if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once. + const stats = fs.statSync(self.cfgPath); + if (stats.mtime.valueOf() === self._fileTime.valueOf()) return; + this._cfg = fs.existsSync(this.cfgPath) ? JSON.parse(fs.readFileSync(this.cfgPath, "utf8")) : {}; + this._cfg = extend(true, {}, def, this._cfg, { appVersion: packageJson.version }); + logger.init(); // only reload logger for now; possibly expand to other areas of app + logger.info(`Reloading app config: ${fileName}`); + } + }); + this._isLoading = false; } catch (err) { console.log(`Error reading configuration information. Aborting startup: ${ err }`); // Rethrow this error so we exit the app with the appropriate pause in the console. @@ -41,10 +57,12 @@ class Config { // Don't overwrite the configuration if we failed during the initialization. try { if (!this._isInitialized) return; + this._isLoading = true; fs.writeFileSync( this.cfgPath, JSON.stringify(this._cfg, undefined, 2) ); + setTimeout(()=>{this._isLoading = false;}, 2000); } catch (err) { logger.error("Error writing configuration file %s", err); diff --git a/controller/State.ts b/controller/State.ts index a0b50ff1..d1091b16 100755 --- a/controller/State.ts +++ b/controller/State.ts @@ -1,19 +1,19 @@ -/* nodejs-poolController. An application to control pool equipment. -Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -*/ +/* nodejs-poolController. An application to control pool equipment. +Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ import * as path from 'path'; import * as fs from 'fs'; import * as extend from 'extend'; @@ -1008,7 +1008,7 @@ export class VirtualCircuitStateCollection extends EqStateCollection { public createItem(data: any): CircuitState { return new CircuitState(data); } - public setCircuitState(id: number, val: boolean) { return sys.board.circuits.setCircuitStateAsync(id, val); } + public setCircuitStateAsync(id: number, val: boolean):Promise { return sys.board.circuits.setCircuitStateAsync(id, val); } public async toggleCircuitStateAsync(id: number) { return sys.board.circuits.toggleCircuitStateAsync(id); } public async setLightThemeAsync(id: number, theme: number) { return sys.board.circuits.setLightThemeAsync(id, theme); } public getInterfaceById(id: number, add?: boolean): ICircuitState { @@ -1301,7 +1301,7 @@ export class ChemControllerState extends EqState { let chem = sys.chemControllers.getItemById(this.id); let obj = this.get(true); obj.saturationIndex = this.saturationIndex || 0; - obj.alkalinty = chem.alkalinity; + obj.alkalinity = chem.alkalinity; obj.body = sys.board.valueMaps.bodies.transform(chem.body); obj.calciumHardness = chem.calciumHardness; obj.cyanuricAcid = chem.cyanuricAcid; diff --git a/controller/boards/EasyTouchBoard.ts b/controller/boards/EasyTouchBoard.ts index 769c3939..26354163 100644 --- a/controller/boards/EasyTouchBoard.ts +++ b/controller/boards/EasyTouchBoard.ts @@ -862,7 +862,7 @@ class TouchCircuitCommands extends CircuitCommands { sys.board.virtualPumpControllers.start(); // sys.board.virtualPumpControllers.setTargetSpeed(); state.emitEquipmentChanges(); - resolve(cstate.get(true)); + resolve(cstate); } } }); @@ -883,6 +883,9 @@ class TouchCircuitCommands extends CircuitCommands { public async setLightGroupStateAsync(id: number, val: boolean): Promise { return this.setCircuitGroupStateAsync(id, val); } public async toggleCircuitStateAsync(id: number) { let cstate = state.circuits.getInterfaceById(id); + if (cstate instanceof LightGroupState) { + return this.setLightGroupThemeAsync(id, sys.board.valueMaps.lightThemes.getValue(cstate.isOn?'off':'on')); + } return this.setCircuitStateAsync(id, !cstate.isOn); } private createLightGroupMessages(group: LightGroup) { @@ -1044,17 +1047,16 @@ class TouchCircuitCommands extends CircuitCommands { if (err) reject(err); else { try { - /* for (let i = 0; i < sys.intellibrite.circuits.length; i++) { - let c = sys.intellibrite.circuits.getItemByIndex(i); - let cstate = state.circuits.getItemById(c.circuit); - if (!cstate.isOn) await sys.board.circuits.setCircuitStateAsync(c.circuit, true); - } */ // Let everyone know we turned these on. The theme messages will come later. for (let i = 0; i < grp.circuits.length; i++) { let c = grp.circuits.getItemByIndex(i); let cstate = state.circuits.getItemById(c.circuit); - if (!cstate.isOn) await sys.board.circuits.setCircuitStateAsync(c.circuit, true); + // if theme is 'off' light groups should not turn on + if (cstate.isOn && sys.board.valueMaps.lightThemes.getName(theme) === 'off') + await sys.board.circuits.setCircuitStateAsync(c.circuit, false); + else if (!cstate.isOn && sys.board.valueMaps.lightThemes.getName(theme) !== 'off') await sys.board.circuits.setCircuitStateAsync(c.circuit, true); } + sgrp.isOn = sys.board.valueMaps.lightThemes.getName(theme) === 'off' ? false: true; switch (theme) { case 0: // off case 1: // on diff --git a/controller/boards/SystemBoard.ts b/controller/boards/SystemBoard.ts index 1b037c16..38451e89 100644 --- a/controller/boards/SystemBoard.ts +++ b/controller/boards/SystemBoard.ts @@ -1535,7 +1535,7 @@ export class CircuitCommands extends BoardCommands { } state.emitEquipmentChanges(); sys.board.virtualPumpControllers.start(); - return Promise.resolve(circ); + return Promise.resolve(state.circuits.getInterfaceById(circ.id)); } public toggleCircuitStateAsync(id: number): Promise { diff --git a/logger/Logger.ts b/logger/Logger.ts index 3b4b96e8..4dcbd312 100755 --- a/logger/Logger.ts +++ b/logger/Logger.ts @@ -1,19 +1,19 @@ -/* nodejs-poolController. An application to control pool equipment. -Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -*/ +/* nodejs-poolController. An application to control pool equipment. +Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ import * as path from 'path'; import * as fs from 'fs'; import * as winston from 'winston'; @@ -30,7 +30,6 @@ class Logger { this.pktPath = path.join(process.cwd(), '/logs', this.getPacketPath()); this.captureForReplayBaseDir = path.join(process.cwd(), '/logs/', this.getLogTimestamp()); /* this.captureForReplayPath = path.join(this.captureForReplayBaseDir, '/packetCapture.json'); */ - this.cfg = config.getSection('log'); this.pkts = []; } private cfg; @@ -61,6 +60,7 @@ class Logger { private _logger: winston.Logger; public init() { + this.cfg = config.getSection('log'); logger._logger = winston.createLogger({ format: winston.format.combine(winston.format.colorize(), winston.format.splat(), winston.format.simple()), transports: [this.transports.console] diff --git a/package.json b/package.json index 3b37f62f..c476c020 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nodejs-poolcontroller", - "version": "6.0.0", + "version": "6.0.1", "description": "nodejs-poolController", "main": "app.js", "author": { diff --git a/web/Server.ts b/web/Server.ts index 4e165275..58c3cdb7 100755 --- a/web/Server.ts +++ b/web/Server.ts @@ -1,19 +1,19 @@ -/* nodejs-poolController. An application to control pool equipment. -Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -*/ +/* nodejs-poolController. An application to control pool equipment. +Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ import * as path from "path"; import * as fs from "fs"; import express=require('express'); @@ -37,6 +37,7 @@ import * as ssdp from 'node-ssdp'; import * as os from 'os'; import { URL } from "url"; import { HttpInterfaceBindings } from './interfaces/httpInterface'; +import { InfluxInterfaceBindings } from './interfaces/influxInterface'; import {Timestamp} from '../controller/Constants'; import extend = require("extend"); import { ConfigSocket } from "./services/config/ConfigSocket"; @@ -45,6 +46,7 @@ import { ConfigSocket } from "./services/config/ConfigSocket"; // external interfaces as well as an internal dashboard. export class WebServer { private _servers: ProtoServer[]=[]; + private family='IPv4'; constructor() { } public init() { let cfg = config.getSection('web'); @@ -83,6 +85,11 @@ export class WebServer { int.init(c); this._servers.push(int); break; + case 'influx': + int = new InfluxInterfaceServer(); + int.init(c); + this._servers.push(int); + break; } } } @@ -103,16 +110,6 @@ export class WebServer { if (typeof this._servers[s].stop() === 'function') this._servers[s].stop(); } } -} -class ProtoServer { - // base class for all servers. - public isRunning: boolean=false; - public emitToClients(evt: string, ...data: any) { } - public emitToChannel(channel: string, evt: string, ...data: any) { } - public stop() { } - protected _dev: boolean=process.env.NODE_ENV !== 'production'; - // todo: how do we know if the client is using IPv4/IPv6? - private family='IPv4'; private getInterface() { const networkInterfaces = os.networkInterfaces(); // RKS: We need to get the scope-local nic. This has nothing to do with IP4/6 and is not necessarily named en0 or specific to a particular nic. We are @@ -130,13 +127,22 @@ class ProtoServer { } } } - protected ip() { + public ip() { return typeof this.getInterface() === 'undefined' ? '0.0.0.0' : this.getInterface().address; } - protected mac() { + public mac() { return typeof this.getInterface() === 'undefined' ? '00:00:00:00' : this.getInterface().mac; } } +class ProtoServer { + // base class for all servers. + public isRunning: boolean=false; + public emitToClients(evt: string, ...data: any) { } + public emitToChannel(channel: string, evt: string, ...data: any) { } + public stop() { } + protected _dev: boolean=process.env.NODE_ENV !== 'production'; + // todo: how do we know if the client is using IPv4/IPv6? +} export class Http2Server extends ProtoServer { public server: http2.Http2Server; public app: Express.Application; @@ -326,11 +332,11 @@ export class SsdpServer extends ProtoServer { let self = this; logger.info('Starting up SSDP server'); - var udn = 'uuid:806f52f4-1f35-4e33-9299-' + this.mac(); + var udn = 'uuid:806f52f4-1f35-4e33-9299-' + webApp.mac(); // todo: should probably check if http/https is enabled at this point var port = config.getSection('web').servers.http.port || 4200; //console.log(port); - let location = 'http://' + this.ip() + ':' + port + '/device'; + let location = 'http://' + webApp.ip() + ':' + port + '/device'; var SSDP = ssdp.Server; this.server = new SSDP({ logLevel: 'INFO', @@ -368,7 +374,7 @@ export class SsdpServer extends ProtoServer { https://github.com/tagyoureit/nodejs-poolController An application to control pool equipment. 0 - uuid:806f52f4-1f35-4e33-9299-${this.mac() } + uuid:806f52f4-1f35-4e33-9299-${webApp.mac() } `; @@ -419,7 +425,7 @@ export class MdnsServer extends ProtoServer { name: '_poolcontroller._tcp.local', type: 'A', ttl: 300, - data: self.ip() + data: webApp.ip() }, { name: 'api._poolcontroller._tcp.local', @@ -507,7 +513,66 @@ export class HttpInterfaceServer extends ProtoServer { }); } } + return true; + } + catch (err) { + logger.error(`Error initializing interface bindings: ${err}`); + } + return false; + } + public emitToClients(evt: string, ...data: any) { + if (this.isRunning) { + // Take the bindings and map them to the appropriate http GET, PUT, DELETE, and POST. + this.bindings.bindEvent(evt, ...data); + } + } +} +export class InfluxInterfaceServer extends ProtoServer { + public bindingsPath: string; + public bindings: InfluxInterfaceBindings; + private _fileTime: Date = new Date(0); + private _isLoading: boolean = false; + public init(cfg) { + if (cfg.enabled) { + if (cfg.fileName && this.initBindings(cfg)) this.isRunning = true; + } + } + public loadBindings(cfg): boolean { + this._isLoading = true; + if (fs.existsSync(this.bindingsPath)) { + try { + let bindings = JSON.parse(fs.readFileSync(this.bindingsPath, 'utf8')); + let ext = extend(true, {}, typeof cfg.context !== 'undefined' ? cfg.context.options : {}, bindings); + this.bindings = Object.assign(new InfluxInterfaceBindings(cfg), ext); + this.isRunning = true; + this._isLoading = false; + const stats = fs.statSync(this.bindingsPath); + this._fileTime = stats.mtime; + return true; + } + catch (err) { + logger.error(`Error reading interface bindings file: ${this.bindingsPath}. ${err}`); + this.isRunning = false; + this._isLoading = false; + } + } + return false; + } + public initBindings(cfg): boolean { + let self = this; + try { + this.bindingsPath = path.posix.join(process.cwd(), "/web/bindings") + '/' + cfg.fileName; + fs.watch(this.bindingsPath, (event, fileName) => { + if (fileName && event === 'change') { + if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once. + const stats = fs.statSync(self.bindingsPath); + if (stats.mtime.valueOf() === self._fileTime.valueOf()) return; + self.loadBindings(cfg); + logger.info(`Reloading ${cfg.name || ''} interface config: ${fileName}`); + } + }); + this.loadBindings(cfg); return true; } catch (err) { @@ -522,4 +587,5 @@ export class HttpInterfaceServer extends ProtoServer { } } } + export const webApp = new WebServer(); diff --git a/web/bindings/influxDB.json b/web/bindings/influxDB.json new file mode 100644 index 00000000..cf73a7a3 --- /dev/null +++ b/web/bindings/influxDB.json @@ -0,0 +1,512 @@ +{ + "context": { + "name": "InfluxDB", + "options": { + "tags": [ + { + "name": "sourceIP", + "value": "@bind=Server_1.webApp.ip();" + }, + { + "name": "sourceApp", + "value": "njspc" + } + ] + } + }, + "events": [ + { + "name": "temps", + "description": "Bind temperatures to measurements", + "points": [ + { + "measurement": "ambientTemps", + "tags": [ + { + "name": "units", + "value": "@bind=data.units.desc;" + } + ], + "fields": [ + { + "name": "airTemp", + "value": "@bind=data.air;", + "type": "int" + }, + { + "name": "solarTemp", + "value": "@bind=data.solar;", + "type": "int" + } + ] + }, + { + "measurement": "bodyTemps", + "tags": [ + { + "name": "units", + "value": "@bind=data.units.desc;" + }, + { + "name": "heatMode", + "value": "@bind=data.bodies[0].heatMode.desc;" + }, + { + "name": "heatStatus", + "value": "@bind=data.bodies[0].heatStatus.desc;" + }, + { + "name": "body", + "value": "@bind=data.bodies[0].name;" + } + ], + "fields": [ + { + "name": "@bind=data.bodies[0].name;", + "value": "@bind=data.bodies[0].temp;", + "type": "int" + } + ] + }, + { + "measurement": "bodyTemps", + "tags": [ + { + "name": "units", + "value": "@bind=data.units.desc;" + }, + { + "name": "heatMode", + "value": "@bind=data.bodies[1].heatMode.desc;" + }, + { + "name": "heatStatus", + "value": "@bind=data.bodies[1].heatStatus.desc;" + }, + { + "name": "body", + "value": "@bind=data.bodies[1].name;" + } + ], + "fields": [ + { + "name": "@bind=data.bodies[1].name;", + "value": "@bind=data.bodies[1].temp;", + "type": "int" + } + ] + } + ] + }, + { + "name": "body", + "description": "Bind bodies to measurements", + "points": [ + { + "measurement": "bodyTemps", + "tags": [ + { + "name": "heatMode", + "value": "@bind=data.heatMode.desc;" + }, + { + "name": "heatStatus", + "value": "@bind=data.heatStatus.desc;" + }, + { + "name": "body", + "value": "@bind=data.name;" + } + ], + "fields": [ + { + "name": "@bind=data.name;", + "value": "@bind=data.temp;", + "type": "int" + }, + { + "name": "@bind=data.name+'Setpoint';", + "value": "@bind=data.setPoint;", + "type": "int" + } + ] + } + ] + }, + { + "name": "chemController", + "description": "Bind chemController emit", + "points": [ + { + "measurement": "chemControllers", + "tags": [ + { + "name": "name", + "value": "@bind=data.name;" + }, + { + "name": "type", + "value": "@bind=data.type.desc;" + }, + { + "name": "status", + "value": "@bind=data.status.desc;" + }, + { + "name": "status1", + "value": "@bind=data.status1.desc;" + }, + { + "name": "status2", + "value": "@bind=data.status2.desc;" + } + ], + "fields": [ + { + "name": "pHLevel", + "value": "@bind=data.pHLevel;", + "type": "float" + }, + { + "name": "pHSetpoint", + "value": "@bind=data.pHSetpoint;", + "type": "float" + }, + { + "name": "orpLevel", + "value": "@bind=data.orpLevel;", + "type": "float" + }, + { + "name": "orpSetpoint", + "value": "@bind=data.orpSetpoint;", + "type": "float" + }, + { + "name": "acidTankLevel", + "value": "@bind=data.acidTankLevel;", + "type": "int" + }, + { + "name": "orpTankLevel", + "value": "@bind=data.orpTankLevel;", + "type": "int" + }, + { + "name": "saturationIndex", + "value": "@bind=data.saturationIndex;", + "type": "float" + }, + { + "name": "CYA", + "value": "@bind=data.cyanuricAcid;", + "type": "int" + }, + { + "name": "CH", + "value": "@bind=data.calciumHardness;", + "type": "int" + }, + { + "name": "Alk", + "value": "@bind=data.alkalinity;", + "type": "int" + }, + { + "name": "phDosingTime", + "value": "@bind=data.pHDosingTime;", + "type": "float" + }, + { + "name": "orpDosingTime", + "value": "@bind=data.orpDosingTime;", + "type": "float" + } + ] + } + ] + }, + { + "name": "circuit", + "description": "Bind circuit emit", + "points": [ + { + "measurement": "circuits", + "storePrevState": true, + "tags": [ + { + "name": "name", + "value": "@bind=data.name;" + }, + { + "name": "id", + "value": "@bind=data.id;" + }, + { + "name": "type", + "value": "@bind=data.type.desc;" + } + ], + "fields": [ + { + "name": "isOn", + "value": "@bind=data.isOn;", + "type": "boolean" + }, + { + "name": "isOnVal", + "value": "@bind=data.isOn?1:0;", + "type": "integer" + } + ] + } + ] + }, + { + "name": "feature", + "description": "Bind feature emit", + "points": [ + { + "measurement": "circuits", + "storePrevState": true, + "tags": [ + { + "name": "name", + "value": "@bind=data.name;" + }, + { + "name": "id", + "value": "@bind=data.id;" + }, + { + "name": "type", + "value": "@bind=data.type.desc;" + } + ], + "fields": [ + { + "name": "isOn", + "value": "@bind=data.isOn;", + "type": "boolean" + }, + { + "name": "isOnVal", + "value": "@bind=data.isOn?1:0;", + "type": "int" + } + ] + } + ] + }, + { + "name": "virtualCircuit", + "description": "Bind virtualCircuit emit", + "points": [ + { + "measurement": "circuits", + "storePrevState": true, + "tags": [ + { + "name": "name", + "value": "@bind=data.name;" + }, + { + "name": "id", + "value": "@bind=data.id;" + }, + { + "name": "type", + "value": "@bind=data.type.desc;" + } + ], + "fields": [ + { + "name": "isOn", + "value": "@bind=data.isOn;", + "type": "boolean" + }, + { + "name": "isOnVal", + "value": "@bind=data.isOn?1:0;", + "type": "int" + } + ] + } + ] + }, + { + "name": "lightGroup", + "description": "Bind lightGroup emit", + "points": [ + { + "measurement": "circuits", + "storePrevState": true, + "tags": [ + { + "name": "name", + "value": "@bind=data.name;" + }, + { + "name": "id", + "value": "@bind=data.id;" + }, + { + "name": "type", + "value": "@bind=data.type.desc;" + } + ], + "fields": [ + { + "name": "isOn", + "value": "@bind=data.isOn;", + "type": "boolean" + }, + { + "name": "isOnVal", + "value": "@bind=data.isOn?1:0;", + "type": "int" + } + ] + } + ] + }, + { + "name": "circuitGroup", + "description": "Bind circuitGroup emit", + "points": [ + { + "measurement": "circuits", + "storePrevState": true, + "tags": [ + { + "name": "name", + "value": "@bind=data.name;" + }, + { + "name": "id", + "value": "@bind=data.id;" + }, + { + "name": "type", + "value": "@bind=data.type.desc;" + } + ], + "fields": [ + { + "name": "isOn", + "value": "@bind=data.isOn;", + "type": "boolean" + }, + { + "name": "isOnVal", + "value": "@bind=data.isOn?1:0;", + "type": "int" + } + ] + } + ] + }, + { + "name": "pump", + "description": "Bind circuit emit", + "points": [ + { + "measurement": "pumps", + "tags": [ + { + "name": "name", + "value": "@bind=data.name;" + }, + { + "name": "id", + "value": "@bind=data.id;" + }, + { + "name": "type", + "value": "@bind=data.type.desc;" + }, + { + "name": "status", + "value": "@bind=data.status.desc;" + } + ], + "fields": [ + { + "name": "rpm", + "value": "@bind=data.rpm;", + "type": "int" + }, + { + "name": "gpm", + "value": "@bind=data.gpm;", + "type": "int" + }, + { + "name": "watts", + "value": "@bind=data.watts;", + "type": "int" + } + ] + } + ] + }, + { + "name": "chlorinator", + "description": "Bind circuit emit", + "points": [ + { + "measurement": "chlorinators", + "tags": [ + { + "name": "name", + "value": "@bind=data.name;" + }, + { + "name": "id", + "value": "@bind=data.id;" + }, + { + "name": "status", + "value": "@bind=data.status.desc;" + }, + { + "name": "superChlor", + "value": "@bind=data.superChlor;" + }, + { + "name": "superChlorHours", + "value": "@bind=data.superChlorHours;" + } + ], + "fields": [ + { + "name": "currentOutput", + "value": "@bind=data.currentOutput;", + "type": "int" + }, + { + "name": "poolSetpoint", + "value": "@bind=data.poolSetpoint;", + "type": "int" + }, + { + "name": "saltLevel", + "value": "@bind=data.saltLevel;", + "type": "int" + }, + { + "name": "spaSetpoint", + "value": "@bind=data.spaSetpoint;", + "type": "int" + } + ] + } + ] + }, + { + "name": "config", + "description": "Not used for updates", + "enabled": false + } + ] +} \ No newline at end of file diff --git a/web/interfaces/baseInterface.ts b/web/interfaces/baseInterface.ts new file mode 100644 index 00000000..a813bcab --- /dev/null +++ b/web/interfaces/baseInterface.ts @@ -0,0 +1,76 @@ +import extend = require("extend"); +import { logger } from "../../logger/Logger"; +import { sys } from "../../controller/Equipment"; +import { state } from "../../controller/State"; +import { webApp } from '../Server'; + +export class BaseInterfaceBindings { + constructor(cfg) { + this.cfg = cfg; + } + public context: InterfaceContext; + public cfg; + public events: InterfaceEvent[]; + public bindEvent(evt: string, ...data: any) { }; + protected buildTokens(input: string, eventName: string, toks: any, e: InterfaceEvent, data): any { + toks = toks || []; + let s = input; + let regx = /(?<=@bind\=\s*).*?(?=\;)/g; + let match; + let vars = extend(true, {}, this.cfg.vars, this.context.vars, typeof e !== 'undefined' && e.vars); + let sys1 = sys; + let state1 = state; + let webApp1 = webApp; + // Map all the returns to the token list. We are being very basic + // here an the object graph is simply based upon the first object occurrence. + // We simply want to eval against that object reference. + + while (match = regx.exec(s)) { + let bind = match[0]; + if (typeof toks[bind] !== 'undefined') continue; + let tok: any = {}; + toks[bind] = tok; + try { + // we may error out if data can't be found (eg during init) + tok.reg = new RegExp("@bind=" + this.escapeRegex(bind) + ";", "g"); + tok.value = eval(bind.replace(/sys./g, 'Equipment_1.sys.').replace(/state./g, 'State_1.state.')); + } + catch (err) { + // leave value undefined so it isn't sent to bindings + tok[bind] = null; + } + } + return toks; + + } + protected escapeRegex(reg: string) { + return reg.replace(/[-[\]{}()*+?.,\\^$]/g, '\\$&'); + } + protected replaceTokens(input: string, toks: any) { + let s = input; + for (let exp in toks) { + let tok = toks[exp]; + if (typeof tok.reg === 'undefined') continue; + tok.reg.lastIndex = 0; // Start over if we used this before. + if (typeof tok.value === 'string') s = s.replace(tok.reg, tok.value); + else if (typeof tok.value === 'undefined') s = s.replace(tok.reg, 'null'); + else s = s.replace(tok.reg, JSON.stringify(tok.value)); + } + return s; + } +} + +export class InterfaceEvent { + public name: string; + public enabled: boolean = true; + public options: any = {}; + public body: any = {}; + public vars: any = {}; +} +export class InterfaceContext { + public name: string; + public mdnsDiscovery: any; + public upnpDevice: any; + public options: any = {}; + public vars: any = {}; +} diff --git a/web/interfaces/httpInterface.ts b/web/interfaces/httpInterface.ts index cda32510..0fdaa361 100644 --- a/web/interfaces/httpInterface.ts +++ b/web/interfaces/httpInterface.ts @@ -22,16 +22,15 @@ import extend=require("extend"); import { logger } from "../../logger/Logger"; import { sys } from "../../controller/Equipment"; import { state } from "../../controller/State"; +import { InterfaceContext, InterfaceEvent, BaseInterfaceBindings } from "./baseInterface"; -export class HttpInterfaceBindings { +export class HttpInterfaceBindings extends BaseInterfaceBindings { constructor(cfg) { - this.cfg = cfg; + super(cfg); } - public context: HttpInterfaceContext; - public cfg; - public events: HttpInterfaceEvent[]; public bindEvent(evt: string, ...data: any) { - // Find the binding by first looking for the specific event name. If that doesn't exist then look for the "*" (all events). + // Find the binding by first looking for the specific event name. + // If that doesn't exist then look for the "*" (all events). if (typeof this.events !== 'undefined') { let evts = this.events.filter(elem => elem.name === evt); // If we don't have an explicitly defined event then see if there is a default. @@ -114,50 +113,5 @@ export class HttpInterfaceBindings { } } } - private buildTokens(input: string, eventName: string, toks: any, e: HttpInterfaceEvent, data): any { - toks = toks || []; - let s = input; - let regx = /(?<=@bind\=\s*).*?(?=\;)/g; - let match; - let vars = extend(true, {}, this.cfg.vars, this.context.vars, e.vars); - // Map all the returns to the token list. We are being very basic - // here an the object graph is simply based upon the first object occurrence. - // We simply want to eval against that object reference. - while (match = regx.exec(s)) { - let bind = match[0]; - if (typeof toks[bind] !== 'undefined') continue; - let tok: any = {}; - toks[bind] = tok; - tok.value = eval(bind); - tok.reg = new RegExp("@bind=" + this.escapeRegex(bind) + ";", "g"); - } - return toks; - } - private escapeRegex(reg: string) { - return reg.replace(/[-[\]{}()*+?.,\\^$]/g, '\\$&'); - } - private replaceTokens(input: string, toks: any) { - let s = input; - for (let exp in toks) { - let tok = toks[exp]; - tok.reg.lastIndex = 0; // Start over if we used this before. - if (typeof tok.value === 'string') s = s.replace(tok.reg, tok.value); - else if (typeof tok.value === 'undefined') s = s.replace(tok.reg, 'null'); - else s = s.replace(tok.reg, JSON.stringify(tok.value)); - } - return s; - } -} -export class HttpInterfaceEvent { - public name: string; - public enabled: boolean=true; - public options: any={}; - public body: any={}; - public vars: any={}; -} -export class HttpInterfaceContext { - public mdnsDiscovery: any; - public upnpDevice: any; - public options: any={}; - public vars: any={}; } + diff --git a/web/interfaces/influxInterface.ts b/web/interfaces/influxInterface.ts new file mode 100644 index 00000000..43030f06 --- /dev/null +++ b/web/interfaces/influxInterface.ts @@ -0,0 +1,181 @@ +/* nodejs-poolController. An application to control pool equipment. +Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +import extend = require("extend"); +import { logger } from "../../logger/Logger"; +import { InfluxDB, Point, HttpError, WritePrecision, ClientOptions } from '@influxdata/influxdb-client'; +import { InterfaceEvent, InterfaceContext, BaseInterfaceBindings } from "./baseInterface"; +import { utils } from '../../controller/Constants'; +import { state } from '../../controller/State'; +import { sys } from "../../controller/Equipment"; + + +export class InfluxInterfaceBindings extends BaseInterfaceBindings { + constructor(cfg) { + super(cfg); + } + private writeApi; + public context: InterfaceContext; + public cfg; + public events: InfluxInterfaceEvent[]; + private init = () => { + let baseOpts = extend(true, this.cfg.options, this.context.options); + if (typeof baseOpts.host === 'undefined' || !baseOpts.host) { + logger.warn(`Interface: ${this.cfg.name} has not resolved to a valid host.`); + return; + } + if (typeof baseOpts.database === 'undefined' || !baseOpts.database) { + logger.warn(`Interface: ${this.cfg.name} has not resolved to a valid database.`); + return; + } + // let opts = extend(true, baseOpts, e.options); + let url = 'http'; + if (typeof baseOpts.protocol !== 'undefined' && baseOpts.protocol) url = baseOpts.protocol; + url = `${url}://${baseOpts.host}:${baseOpts.port}`; + // TODO: add username/password + const bucket = `${baseOpts.database}/${baseOpts.retentionPolicy}`; + const clientOptions:ClientOptions = { + url, + token: `${baseOpts.username}:${baseOpts.password}`, + } + const influxDB = new InfluxDB(clientOptions); + this.writeApi = influxDB.getWriteApi('', bucket, 'ms' as WritePrecision); + + // set global tags from context + let baseTags = {} + baseOpts.tags.forEach(tag=> { + let toks = {}; + let sname = tag.name; + this.buildTokens(sname, undefined, toks, undefined, {}); + sname = this.replaceTokens(sname, toks); + let svalue = tag.value; + this.buildTokens(svalue, undefined, toks, {vars:{}} as any, {}); + svalue = this.replaceTokens(svalue, toks); + if (typeof sname !== 'undefined' && typeof svalue !== 'undefined' && !sname.includes('@bind') && !svalue.includes('@bind')) + baseTags[sname] = svalue; + }) + this.writeApi.useDefaultTags(baseTags); + } + public bindEvent(evt: string, ...data: any) { + + // if (state.status.value !== sys.board.valueMaps.controllerStatus.getValue('ready')) return; // miss values? or show errors? or? + if (typeof this.events !== 'undefined') { + if (typeof this.writeApi === 'undefined') this.init(); + let evts = this.events.filter(elem => elem.name === evt); + if (evts.length > 0) { + let toks = {}; + for (let i = 0; i < evts.length; i++) { + let e = evts[i]; + if (typeof e.enabled !== 'undefined' && !e.enabled) continue; + e.points.forEach(_point => { + // iterate through points array + let point = new Point(_point.measurement) + let point2 = new Point(_point.measurement); + _point.tags.forEach(_tag => { + let sname = _tag.name; + this.buildTokens(sname, evt, toks, e, data[0]); + sname = this.replaceTokens(sname, toks); + let svalue = _tag.value; + this.buildTokens(svalue, evt, toks, e, data[0]); + svalue = this.replaceTokens(svalue, toks); + if (typeof sname !== 'undefined' && typeof svalue !== 'undefined' && !sname.includes('@bind') && !svalue.includes('@bind') && svalue !== null){ + point.tag(sname, svalue); + if (typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.tag(sname, svalue); + } + else { + console.log(`failed on ${_tag.name}/${_tag.value}`); + } + }) + _point.fields.forEach(_field => { + let sname = _field.name; + this.buildTokens(sname, evt, toks, e, data[0]); + sname = this.replaceTokens(sname, toks); + let svalue = _field.value; + this.buildTokens(svalue, evt, toks, e, data[0]); + svalue = this.replaceTokens(svalue, toks); + if (typeof sname !== 'undefined' && typeof svalue !== 'undefined' && !sname.includes('@bind') && !svalue.includes('@bind') && svalue !== null) + switch (_field.type) { + case 'int': + case 'integer': + let int = parseInt(svalue, 10); + if (!isNaN(int)) point.intField(sname, int); + // if (!isNaN(int) && typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.intField(sname, int); + break; + case 'string': + point.stringField(sname, svalue); + // if (typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.stringField(sname, svalue); + break; + case 'boolean': + point.booleanField(sname, utils.makeBool(svalue)); + if (typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.booleanField(sname, !utils.makeBool(svalue)); + break; + case 'float': + let float = parseFloat(svalue); + if (!isNaN(float)) point.floatField(sname, float); + // if (!isNaN(float) && typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.intField(sname, int); + break; + } + else { + console.log(`failed on ${_field.name}/${_field.value}`); + + } + }) + point.timestamp(new Date()); + if (typeof _point.storePrevState !== 'undefined' && _point.storePrevState){ + // copy the point and subtract a second and keep inverse value + let ts = new Date(); + let sec = ts.getSeconds() - 1; + ts.setSeconds(sec); + point2.timestamp(ts); + this.writeApi.writePoint(point2); + } + if (typeof point.toLineProtocol() !== 'undefined'){ + this.writeApi.writePoint(point); + logger.info(`INFLUX: ${point.toLineProtocol()}`) + } + else { + logger.silly(`Skipping INFLUX write because some data is missing with ${e.name} event on measurement ${_point.measurement}.`) + } + }) + } + } + } + } + public close = () => { + + } +} + +class InfluxInterfaceEvent extends InterfaceEvent { + public points: IPoint[]; +} + +export interface IPoint { + measurement: string; + tags: ITag[]; + fields: IFields[]; + storePrevState?: boolean; +} +export interface ITag { + name: string; + value: string; +} +export interface IFields { + name: string; + value: string; + type: string; +} diff --git a/web/services/state/State.ts b/web/services/state/State.ts index b72f95fa..c4d1b425 100755 --- a/web/services/state/State.ts +++ b/web/services/state/State.ts @@ -1,19 +1,19 @@ -/* nodejs-poolController. An application to control pool equipment. -Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as -published by the Free Software Foundation, either version 3 of the -License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . -*/ +/* nodejs-poolController. An application to control pool equipment. +Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ import * as express from "express"; import { state, ICircuitState, LightGroupState, ICircuitGroupState } from "../../../controller/State"; import { sys } from "../../../controller/Equipment"; @@ -28,8 +28,8 @@ export class StateRoute { }); app.put('/state/chemController', async (req, res, next) => { try { - let chem = await sys.board.chemControllers.setChemControllerAsync(req.body); - return res.status(200).send(state.chemControllers.getItemById(chem.id).getExtended()); + let schem = await sys.board.chemControllers.setChemControllerAsync(req.body); + return res.status(200).send(state.chemControllers.getItemById(schem.id).getExtended()); } catch (err) { next(err); } }); @@ -46,47 +46,46 @@ export class StateRoute { }); app.put('/state/circuit/setState', async (req, res, next) => { try { - console.log(`request: ${JSON.stringify(req.body)}... id: ${req.body.id} state: ${req.body.state} isOn: ${req.body.isOn}`); // Do some work to allow the legacy state calls to work. For some reason the state value is generic while all of the // circuits are actually binary states. While this may need to change in the future it seems like a distant plan // that circuits would have more than 2 states. Not true for other equipment but certainly true for individual circuits/features/groups. let isOn = utils.makeBool(typeof req.body.isOn !== 'undefined' ? req.body.isOn : req.body.state); //state.circuits.setCircuitState(parseInt(req.body.id, 10), utils.makeBool(req.body.state)); - let circuit = await sys.board.circuits.setCircuitStateAsync(parseInt(req.body.id, 10), isOn); - return res.status(200).send((circuit as ICircuitState).get(true)); + let cstate = await sys.board.circuits.setCircuitStateAsync(parseInt(req.body.id, 10), isOn); + return res.status(200).send(cstate.get(true)); } catch (err) { next(err); } }); app.put('/state/circuitGroup/setState', async (req, res, next) => { console.log(`request: ${JSON.stringify(req.body)}... id: ${req.body.id} state: ${req.body.state} isOn: ${req.body.isOn}`); let isOn = utils.makeBool(typeof req.body.isOn !== 'undefined' ? req.body.isOn : req.body.state); - let circuit = await sys.board.circuits.setCircuitGroupStateAsync(parseInt(req.body.id, 10), isOn); - return res.status(200).send((circuit as ICircuitGroupState).get(true)); + let cstate = await sys.board.circuits.setCircuitGroupStateAsync(parseInt(req.body.id, 10), isOn); + return res.status(200).send(cstate.get(true)); }); app.put('/state/lightGroup/setState', async (req, res, next) => { console.log(`request: ${JSON.stringify(req.body)}... id: ${req.body.id} state: ${req.body.state} isOn: ${req.body.isOn}`); let isOn = utils.makeBool(typeof req.body.isOn !== 'undefined' ? req.body.isOn : req.body.state); - let circuit = await sys.board.circuits.setLightGroupStateAsync(parseInt(req.body.id, 10), isOn); - return res.status(200).send((circuit as ICircuitGroupState).get(true)); + let cstate = await sys.board.circuits.setLightGroupStateAsync(parseInt(req.body.id, 10), isOn); + return res.status(200).send(cstate.get(true)); }); app.put('/state/circuit/toggleState', async (req, res, next) => { try { let cstate = await sys.board.circuits.toggleCircuitStateAsync(parseInt(req.body.id, 10)); - return res.status(200).send(cstate); + return res.status(200).send(cstate.get(true)); } catch (err) {next(err);} }); app.put('/state/feature/toggleState', async (req, res, next) => { try { let fstate = await sys.board.features.toggleFeatureStateAsync(parseInt(req.body.id, 10)); - return res.status(200).send(fstate); + return res.status(200).send(fstate.get(true)); } catch (err) {next(err);} }); app.put('/state/circuit/setTheme', async (req, res, next) => { try { let theme = await state.circuits.setLightThemeAsync(parseInt(req.body.id, 10), parseInt(req.body.theme, 10)); - return res.status(200).send(theme); + return res.status(200).send(theme.get(true)); } catch (err) { next(err); } }); @@ -105,8 +104,8 @@ export class StateRoute { }); app.put('/state/circuit/setDimmerLevel', async (req, res, next) => { try { - let circuit = await sys.board.circuits.setDimmerLevelAsync(parseInt(req.body.id, 10), parseInt(req.body.level, 10)); - return res.status(200).send(circuit); + let cstate = await sys.board.circuits.setDimmerLevelAsync(parseInt(req.body.id, 10), parseInt(req.body.level, 10)); + return res.status(200).send(cstate.get(true)); } catch (err) { next(err); } }); @@ -114,7 +113,7 @@ export class StateRoute { try { let isOn = utils.makeBool(typeof req.body.isOn !== 'undefined' ? req.body.isOn : req.body.state); let fstate = await state.features.setFeatureStateAsync(req.body.id, isOn); - return res.status(200).send(fstate); + return res.status(200).send(fstate.get(true)); } catch (err){ next(err); } }); @@ -134,7 +133,7 @@ export class StateRoute { let body = sys.bodies.findByObject(req.body); if (typeof body === 'undefined') return next(new ServiceParameterError(`Cannot set body heatMode. You must supply a valid id, circuit, name, or type for the body`, 'body', 'id', req.body.id)); let tbody = await sys.board.bodies.setHeatModeAsync(body, mode); - return res.status(200).send(tbody); + return res.status(200).send(tbody.get(true)); } catch (err) { next(err); } }); app.put('/state/body/setPoint', async (req, res, next) => { @@ -143,51 +142,51 @@ export class StateRoute { let body = sys.bodies.findByObject(req.body); if (typeof body === 'undefined') return next(new ServiceParameterError(`Cannot set body setPoint. You must supply a valid id, circuit, name, or type for the body`, 'body', 'id', req.body.id)); let tbody = await sys.board.bodies.setHeatSetpointAsync(body, parseInt(req.body.setPoint, 10)); - return res.status(200).send(tbody); + return res.status(200).send(tbody.get(true)); } catch (err) { next(err); } }); app.put('/state/chlorinator', async (req, res, next) => { try { - let chlor = await sys.board.chlorinator.setChlorAsync(req.body); - return res.status(200).send(chlor); + let schlor = await sys.board.chlorinator.setChlorAsync(req.body); + return res.status(200).send(schlor.get(true)); } catch (err) { next(err); } }); // this ../setChlor should really be EOL for PUT /state/chlorinator above app.put('/state/chlorinator/setChlor', async (req, res, next) => { try { - let chlor = await sys.board.chlorinator.setChlorAsync(req.body); - return res.status(200).send(chlor); + let schlor = await sys.board.chlorinator.setChlorAsync(req.body); + return res.status(200).send(schlor.get(true)); } catch (err) { next(err); } }); app.put('/state/chlorinator/poolSetpoint', async (req, res, next) => { try { let obj = { id: req.body.id, poolSetpoint: parseInt(req.body.setPoint, 10) } - let chlor = await sys.board.chlorinator.setChlorAsync(obj); - return res.status(200).send(chlor); + let schlor = await sys.board.chlorinator.setChlorAsync(obj); + return res.status(200).send(schlor.get(true)); } catch (err) { next(err); } }); app.put('/state/chlorinator/spaSetpoint', async (req, res, next) => { try { let obj = { id: req.body.id, spaSetpoint: parseInt(req.body.setPoint, 10) } - let chlor = await sys.board.chlorinator.setChlorAsync(obj); - return res.status(200).send(chlor); + let schlor = await sys.board.chlorinator.setChlorAsync(obj); + return res.status(200).send(schlor.get(true)); } catch (err) { next(err); } }); app.put('/state/chlorinator/superChlorHours', async (req, res, next) => { try { let obj = { id: req.body.id, superChlorHours: parseInt(req.body.hours, 10) } - let chlor = await sys.board.chlorinator.setChlorAsync(obj); - return res.status(200).send(chlor); + let schlor = await sys.board.chlorinator.setChlorAsync(obj); + return res.status(200).send(schlor.get(true)); } catch (err) { next(err); } }); app.put('/state/chlorinator/superChlorinate', async (req, res, next) => { try { let obj = { id: req.body.id, superChlorinate: utils.makeBool(req.body.superChlorinate) } - let chlor = await sys.board.chlorinator.setChlorAsync(obj); - return res.status(200).send(chlor); + let schlor = await sys.board.chlorinator.setChlorAsync(obj); + return res.status(200).send(schlor.get(true)); } catch (err) { next(err); } });