diff --git a/controller/Equipment.ts b/controller/Equipment.ts index 6edf087c..85cfe168 100755 --- a/controller/Equipment.ts +++ b/controller/Equipment.ts @@ -1188,7 +1188,11 @@ export class Valve extends EqItem { public get isIntake(): boolean { return utils.makeBool(this.data.isIntake); } public set isIntake(val: boolean) { this.setDataVal('isIntake', val); } public get isReturn(): boolean { return utils.makeBool(this.data.isReturn); } - public set isReturn(val: boolean) { this.setDataVal('isReturn', val); } + public set isReturn(val: boolean) { this.setDataVal('isVirtual', val); } + public get isVirtual(): boolean { return utils.makeBool(this.data.isReturn); } + public set isVirtual(val: boolean) { this.setDataVal('isVirtual', val); } + public get pinId(): number { return this.data.pinId || 0; } + public set pinId(val: number) { this.setDataVal('pinId', val); } public get isActive(): boolean { return this.data.isActive; } public set isActive(val: boolean) { this.setDataVal('isActive', val); } } diff --git a/controller/State.ts b/controller/State.ts index a8b6d5a3..400678dc 100755 --- a/controller/State.ts +++ b/controller/State.ts @@ -20,7 +20,7 @@ import * as extend from 'extend'; import * as util from 'util'; import { setTimeout } from 'timers'; import { logger } from '../logger/Logger'; -import { Timestamp, ControllerType } from './Constants'; +import { Timestamp, ControllerType, utils } from './Constants'; import { webApp } from '../web/Server'; import { sys, ChemController } from './Equipment'; import { InvalidEquipmentIdError } from './Errors'; @@ -1063,13 +1063,33 @@ export class ValveState extends EqState { public set id(val: number) { this.data.id = val; } public get name(): string { return this.data.name; } public set name(val: string) { this.setDataVal('name', val); } - public get type(): number { return typeof this.data.type !== 'undefined' ? this.data.type : -1; } + public get type(): number { return typeof this.data.type !== 'undefined' ? this.data.type.val : -1; } public set type(val: number) { if (this.type !== val) { this.data.type = sys.board.valueMaps.valveTypes.transform(val); this.hasChanged = true; } } + public get isDiverted(): boolean { return utils.makeBool(this.data.isDiverted); } + public set isDiverted(val: boolean) { this.setDataVal('isDiverted', val); } + public getExtended(): any { + let valve = sys.valves.getItemById(this.id); + let vstate = this.get(true); + if(valve.circuit !== 256) vstate.circuit = state.circuits.getInterfaceById(valve.circuit).get(true); + vstate.isIntake = utils.makeBool(valve.isIntake); + vstate.isReturn = utils.makeBool(valve.isReturn); + vstate.isVirtual = utils.makeBool(valve.isVirtual); + vstate.isActive = utils.makeBool(valve.isActive); + vstate.pinId = valve.pinId; + return vstate; + } + public emitEquipmentChange() { + if (typeof (webApp) !== 'undefined' && webApp) { + if (this.hasChanged) this.emitData(this.dataName, this.getExtended()); + this.hasChanged = false; + state._dirtyList.removeEqState(this); + } + } } export class CoverStateCollection extends EqStateCollection { public createItem(data: any): CoverState { return new CoverState(data); } diff --git a/controller/boards/IntelliCenterBoard.ts b/controller/boards/IntelliCenterBoard.ts index 42077a45..fdaa5817 100644 --- a/controller/boards/IntelliCenterBoard.ts +++ b/controller/boards/IntelliCenterBoard.ts @@ -2846,15 +2846,16 @@ class IntelliCenterHeaterCommands extends HeaterCommands { } class IntelliCenterValveCommands extends ValveCommands { - public async setValveAsync(obj?: any) : Promise { + public async setValveAsync(obj?: any): Promise { + if (obj.isVirtual) return super.setValveAsync(obj); + let id = parseInt(obj.id, 10); + if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Valve Id has not been defined', obj.id, 'Valve')); + let valve = sys.valves.getItemById(id); // [255, 0, 255][165, 63, 15, 16, 168, 20][9, 0, 9, 2, 86, 97, 108, 118, 101, 32, 70, 0, 0, 0, 0, 0, 0, 0, 0, 0][4, 55] // RKS: The valve messages are a bit unique since they are 0 based instead of 1s based. Our configuration includes // the ability to set these valves appropriately via the interface by subtracting 1 from the circuit and the valve id. In // shared body systems there is a gap for the additional intake/return valves that exist in i10d. return new Promise(function (resolve, reject) { - let id = parseInt(obj.id, 10); - if (isNaN(id)) reject(new InvalidEquipmentIdError('Valve Id has not been defined', obj.id, 'Valve')); - let valve = sys.valves.getItemById(id); let v = extend(true, valve.get(true), obj); let out = Outbound.create({ action: 168, diff --git a/controller/boards/SystemBoard.ts b/controller/boards/SystemBoard.ts index 367d3bac..e0944164 100644 --- a/controller/boards/SystemBoard.ts +++ b/controller/boards/SystemBoard.ts @@ -1540,6 +1540,7 @@ export class CircuitCommands extends BoardCommands { } } } + sys.board.valves.syncValveStates(); state.emitEquipmentChanges(); sys.board.virtualPumpControllers.start(); return Promise.resolve(state.circuits.getInterfaceById(circ.id)); @@ -1915,6 +1916,7 @@ export class FeatureCommands extends BoardCommands { let feature = sys.features.getItemById(id); let fstate = state.features.getItemById(feature.id, feature.isActive !== false); fstate.isOn = val; + sys.board.valves.syncValveStates(); sys.board.virtualPumpControllers.start(); state.emitEquipmentChanges(); return Promise.resolve(fstate.get(true)); @@ -1937,6 +1939,7 @@ export class FeatureCommands extends BoardCommands { } let sgrp = state.circuitGroups.getItemById(grp.id); sgrp.isOn = bIsOn && grp.isActive; + sys.board.valves.syncValveStates(); state.emitEquipmentChanges(); } } @@ -2378,14 +2381,50 @@ export class HeaterCommands extends BoardCommands { } export class ValveCommands extends BoardCommands { public async setValveAsync(obj: any): Promise { + let id = parseInt(obj.id, 10); + // The following code will make sure we do not encroach on any valves defined by the OCP. + obj.isVirtual = true; + if (isNaN(id) || id <= 0) id = Math.max(sys.valves.getMaxId(false), 49) + 1; return new Promise(function (resolve, reject) { - let id = parseInt(obj.id, 10); if (isNaN(id)) reject(new InvalidEquipmentIdError('Valve Id has not been defined', obj.id, 'Valve')); - let valve = sys.valves.getItemById(id, false); + if (id < 50) reject(new InvalidEquipmentDataError('Virtual valves must be defined with an id >= 50.', obj.id, 'Valve')); + let valve = sys.valves.getItemById(id, true); + obj.id = id; for (var s in obj) valve[s] = obj[s]; + sys.board.valves.syncValveStates(); + resolve(valve); + }); + } + public async deleteValveAsync(obj: any): Promise { + let id = parseInt(obj.id, 10); + // The following code will make sure we do not encroach on any valves defined by the OCP. + return new Promise(function (resolve, reject) { + if (isNaN(id)) reject(new InvalidEquipmentIdError('Valve Id has not been defined', obj.id, 'Valve')); + let valve = sys.valves.getItemById(id, false); + let vstate = state.valves.getItemById(id); + valve.isActive = false; + vstate.hasChanged = true; + vstate.emitEquipmentChange(); + sys.valves.removeItemById(id); + state.valves.removeItemById(id); + resolve(valve); }); } + + public syncValveStates() { + for (let i = 0; i < sys.valves.length; i++) { + // Run through all the valves to see whether they should be triggered or not. + let valve = sys.valves.getItemByIndex(i); + if (valve.circuit > 0) { + let circ = state.circuits.getInterfaceById(valve.circuit); + let vstate = state.valves.getItemById(valve.id, true); + vstate.type = valve.type; + vstate.name = valve.name; + vstate.isDiverted = utils.makeBool(circ.isOn); + } + } + } } export class ChemControllerCommands extends BoardCommands { public async setChemControllerAsync(data: any): Promise { diff --git a/controller/comms/messages/config/ExternalMessage.ts b/controller/comms/messages/config/ExternalMessage.ts index 283c5dd7..0dd8da77 100755 --- a/controller/comms/messages/config/ExternalMessage.ts +++ b/controller/comms/messages/config/ExternalMessage.ts @@ -90,6 +90,7 @@ export class ExternalMessage { let valve = sys.valves.getItemById(msg.extractPayloadByte(2) + 1); valve.circuit = msg.extractPayloadByte(3) + 1; valve.name = msg.extractPayloadString(4, 16); + valve.isVirtual = false; } public static processPool(msg: Inbound) { switch (msg.extractPayloadByte(2)) { diff --git a/controller/comms/messages/config/ValveMessage.ts b/controller/comms/messages/config/ValveMessage.ts index b639724f..2ec879cd 100755 --- a/controller/comms/messages/config/ValveMessage.ts +++ b/controller/comms/messages/config/ValveMessage.ts @@ -78,6 +78,7 @@ export class ValveMessage { // what is payload[0]? for (let ndx = 4, id = 1; id <= sys.equipment.maxValves;) { let valve = sys.valves.getItemById(id, true); + valve.isVirtual = false; valve.type = 0; if (id === 3){ valve.circuit = 6; // pool/spa -- fix diff --git a/controller/comms/messages/status/EquipmentStateMessage.ts b/controller/comms/messages/status/EquipmentStateMessage.ts index b6d9f3a7..2a0a1c4e 100644 --- a/controller/comms/messages/status/EquipmentStateMessage.ts +++ b/controller/comms/messages/status/EquipmentStateMessage.ts @@ -525,6 +525,7 @@ export class EquipmentStateMessage { // to be available on message 30-15 and 168-15. //EquipmentStateMessage.processFeatureState(msg); sys.board.circuits.syncVirtualCircuitStates(); + sys.board.valves.syncValveStates(); state.emitControllerChange(); state.emitEquipmentChanges(); break; @@ -537,6 +538,7 @@ export class EquipmentStateMessage { // This will toggle the group states depending on the state of the individual circuits. sys.board.features.syncGroupStates(); sys.board.circuits.syncVirtualCircuitStates(); + sys.board.valves.syncValveStates(); state.emitControllerChange(); state.emitEquipmentChanges(); break; diff --git a/defaultConfig.json b/defaultConfig.json index 8cec0adf..e15d916b 100755 --- a/defaultConfig.json +++ b/defaultConfig.json @@ -70,6 +70,18 @@ "host": "", "port": 3480 } + }, + "valveRelay": { + "name": "Valve Relays", + "enabled": false, + "fileName": "valveRelays.json", + "vars": { + "valveIds": [] + }, + "options": { + "host": "0.0.0.0", + "port": 8081 + } } } }, diff --git a/web/bindings/valveRelays.json b/web/bindings/valveRelays.json new file mode 100644 index 00000000..11b9634b --- /dev/null +++ b/web/bindings/valveRelays.json @@ -0,0 +1,20 @@ +{ + "context": { + "name": "valveRelay", + "options": { + "method": "GET", + "path": "/@bind=data.pinId;/@bind=data.isDiverted ? 'on' : 'off';", + "headers": { + "CONTENT-TYPE": "application/json" + } + }, + "vars": {} + }, + "events": [ + { + "name": "valve", + "filter": "@bind=data.isVirtual;", + "description": "Send commands to turn on or off the valve relay based upon the valve emit." + } + ] +} diff --git a/web/interfaces/baseInterface.ts b/web/interfaces/baseInterface.ts index eba52c39..1fa79480 100644 --- a/web/interfaces/baseInterface.ts +++ b/web/interfaces/baseInterface.ts @@ -67,6 +67,7 @@ export class BaseInterfaceBindings { export class InterfaceEvent { public name: string; public enabled: boolean = true; + public filter: string; public options: any = {}; public body: any = {}; public vars: any = {}; diff --git a/web/interfaces/httpInterface.ts b/web/interfaces/httpInterface.ts index 0fdaa361..8c9be2fa 100644 --- a/web/interfaces/httpInterface.ts +++ b/web/interfaces/httpInterface.ts @@ -50,6 +50,11 @@ export class HttpInterfaceBindings extends BaseInterfaceBindings { let e = evts[i]; if (typeof e.enabled !== 'undefined' && !e.enabled) continue; let opts = extend(true, baseOpts, e.options); + // Figure out whether we need to check the filter. + if (typeof e.filter !== 'undefined') { + this.buildTokens(e.filter, evt, toks, e, data[0]); + if (eval(this.replaceTokens(e.filter, toks)) === false) continue; + } // If we are still waiting on mdns then blow this off. if ((typeof opts.hostname === 'undefined' || !opts.hostname) && (typeof opts.host === 'undefined' || !opts.host || opts.host === '*')) { @@ -63,7 +68,7 @@ export class HttpInterfaceBindings extends BaseInterfaceBindings { //case 'application/json': //case 'json': default: - sbody = JSON.stringify(e.body); + sbody = typeof e.body !== 'undefined' ? JSON.stringify(e.body) : ''; break; // We may need an XML output and can add transforms for that // later. There isn't a native xslt processor in node and most @@ -87,7 +92,7 @@ export class HttpInterfaceBindings extends BaseInterfaceBindings { } if (typeof opts.path !== 'undefined') opts.path = encodeURI(opts.path); // Encode the data just in case we have spaces. // opts.headers["CONTENT-LENGTH"] = Buffer.byteLength(sbody || ''); - logger.verbose(`Sending [${ evt }] request to ${ this.cfg.name }: ${ JSON.stringify(opts) }`); + logger.verbose(`Sending [${evt}] request to ${this.cfg.name}: ${JSON.stringify(opts)}`); let req: http.ClientRequest; // We should now have all the tokens. Put together the request. if (typeof sbody !== 'undefined') { diff --git a/web/services/config/Config.ts b/web/services/config/Config.ts index f666524d..244f1dde 100755 --- a/web/services/config/Config.ts +++ b/web/services/config/Config.ts @@ -224,6 +224,15 @@ export class ConfigRoute { } catch (err) { next(err); } }); + app.delete('/config/valve', async (req, res, next) => { + // Update a valve. + try { + let valve = await sys.board.valves.deleteValveAsync(req.body); + return res.status(200).send((valve).get(true)); + } + catch (err) { next(err); } + }); + app.put('/config/body', async (req, res, next) => { // Change the body attributes. try {