From 49d553b881f421c26fee17dbc3b2fb530c8cb982 Mon Sep 17 00:00:00 2001 From: Russell Goldin Date: Sun, 22 Nov 2020 09:57:07 -0800 Subject: [PATCH] filter equipment type; sample rules manager --- controller/Equipment.ts | 35 +++-- controller/State.ts | 82 ++++++----- controller/boards/SystemBoard.ts | 224 ++++++++++--------------------- defaultConfig.json | 15 ++- web/Server.ts | 68 ++++++++++ web/bindings/rulesManager.json | 65 +++++++++ web/services/config/Config.ts | 43 +++--- 7 files changed, 307 insertions(+), 225 deletions(-) create mode 100644 web/bindings/rulesManager.json diff --git a/controller/Equipment.ts b/controller/Equipment.ts index 42921b68..c21920a8 100755 --- a/controller/Equipment.ts +++ b/controller/Equipment.ts @@ -44,6 +44,7 @@ interface IPoolSystem { features: FeatureCollection; pumps: PumpCollection; chlorinators: ChlorinatorCollection; + filters: FilterCollection; valves: ValveCollection; heaters: HeaterCollection; covers: CoverCollection; @@ -52,10 +53,7 @@ interface IPoolSystem { eggTimers: EggTimerCollection; security: Security; chemControllers: ChemControllerCollection; - //intellichem: IntelliChem; board: SystemBoard; - // virtualChlorinatorControllers: VirtualChlorinatorControllerCollection; - // virtualPumpControllers: VirtualPumpControllerCollection; updateControllerDateTimeAsync( hour: number, min: number, @@ -96,13 +94,11 @@ export class PoolSystem implements IPoolSystem { this.security = new Security(this.data, 'security'); this.customNames = new CustomNameCollection(this.data, 'customNames'); this.eggTimers = new EggTimerCollection(this.data, 'eggTimers'); - //this.intellichem = new IntelliChem(this.data, 'intellichem'); this.chemControllers = new ChemControllerCollection(this.data, 'chemControllers'); + this.filters = new FilterCollection(this.data, 'filters'); this.data.appVersion = state.appVersion.installed = this.appVersion = JSON.parse(fs.readFileSync(path.posix.join(process.cwd(), '/package.json'), 'utf8')).version; versionCheck.compare(); // if we installed a new version, reset the flag so we don't show an outdated message for up to 2 days this.board = BoardFactory.fromControllerType(this.controllerType, this); - // this.intellibrite = new LightGroup(this.data, 'intellibrite', { id: 0, isActive: false, type: 3 }); - //console.log(utils.uuid()); } // This performs a safe load of the config file. If the file gets corrupt or actually does not exist // it will not break the overall system and allow hardened recovery. @@ -163,11 +159,8 @@ export class PoolSystem implements IPoolSystem { this.security.clear(); this.valves.clear(); this.chemControllers.clear(); - //if (typeof this.data.intelliBrite !== 'undefined') this.intellibrite.clear(); + this.filters.clear(); if (typeof this.data.eggTimers !== 'undefined') this.eggTimers.clear(); - //this.intellichem.clear(); - //console.log(this.configVersion); - } public async stopAsync() { if (this._timerChanges) clearTimeout(this._timerChanges); @@ -230,6 +223,7 @@ export class PoolSystem implements IPoolSystem { public security: Security; public customNames: CustomNameCollection; public chemControllers: ChemControllerCollection; + public filters: FilterCollection; public appVersion: string; public get dirty(): boolean { return this._isDirty; } public set dirty(val) { @@ -1654,4 +1648,25 @@ export class ChemController extends EqItem { return chem; } } +export class FilterCollection extends EqItemCollection { + constructor(data: any, name?: string) { super(data, name || "filters"); } + public createItem(data: any): Filter { return new Filter(data); } +} +export class Filter extends EqItem { + public dataName='filterConfig'; + public get id(): number { return this.data.id; } + public set id(val: number) { this.setDataVal('id', val); } + public get filterType(): number | any { return this.data.filterType; } + public set filterType(val: number | any) { this.setDataVal('filterType', sys.board.valueMaps.filterTypes.encode(val)); } + public get body(): number | any { return this.data.body; } + public set body(val: number | any) { this.setDataVal('body', sys.board.valueMaps.bodies.encode(val)); } + public get isActive(): boolean { return this.data.isActive; } + public set isActive(val: boolean) { this.setDataVal('isActive', val); } + public get name(): string { return this.data.name; } + public set name(val: string) { this.setDataVal('name', val); } + public get lastCleanDate(): Timestamp { return this.data.lastCleanDate; } + public set lastCleanDate(val: Timestamp) { this.setDataVal('lastCleanDate', val); } + public get needsCleaning(): number { return this.data.needsCleaning; } + public set needsCleaning(val: number) { this.setDataVal('needsCleaning', val); } +} export let sys = new PoolSystem(); \ No newline at end of file diff --git a/controller/State.ts b/controller/State.ts index f0d107f6..69bd09d8 100755 --- a/controller/State.ts +++ b/controller/State.ts @@ -103,6 +103,7 @@ export class State implements IState { _state.lightGroups = this.lightGroups.getExtended(); _state.virtualCircuits = this.virtualCircuits.getExtended(); _state.covers = this.covers.getExtended(); + _state.filters = this.filters.getExtended(); _state.schedules = this.schedules.getExtended(); _state.chemControllers = this.chemControllers.getExtended(); return _state; @@ -214,6 +215,9 @@ export class State implements IState { for (let i = 0; i < state.covers.length; i++) { state.covers.getItemByIndex(i).hasChanged = true; } + for (let i = 0; i < state.filters.length; i++) { + state.filters.getItemByIndex(i).hasChanged = true; + } for (let i = 0; i < state.schedules.length; i++) { state.schedules.getItemByIndex(i).hasChanged = true; } @@ -337,6 +341,7 @@ export class State implements IState { this.virtualCircuits = new VirtualCircuitStateCollection(this.data, 'virtualCircuits'); this.chemControllers = new ChemControllerStateCollection(this.data, 'chemControllers'); this.covers = new CoverStateCollection(this.data, 'covers'); + this.filters = new FilterStateCollection(this.data, 'filters'); this.comms = new CommsState(); this.heliotrope = new Heliotrope(); this.appVersion = new AppVersionState(this.data, 'appVersion'); @@ -356,7 +361,7 @@ export class State implements IState { this.schedules.clear(); this.valves.clear(); this.virtualCircuits.clear(); - this.covers.clear(); + this.filters.clear(); this.chemControllers.clear(); } @@ -373,6 +378,7 @@ export class State implements IState { public lightGroups: LightGroupStateCollection; public virtualCircuits: VirtualCircuitStateCollection; public covers: CoverStateCollection; + public filters: FilterStateCollection; public chemControllers: ChemControllerStateCollection; public comms: CommsState; public appVersion: AppVersionState; @@ -405,6 +411,7 @@ interface IState { circuitGroups: CircuitGroupStateCollection; virtualCircuits: VirtualCircuitStateCollection; chemControllers: ChemControllerStateCollection; + filters: FilterStateCollection; comms: CommsState; } export interface ICircuitState { @@ -1369,44 +1376,20 @@ export class ChemControllerState extends EqState { } } + public get pHSetpoint(): number { return this.data.pHSetpoint; } + public set pHSetpoint(val: number) { this.setDataVal('pHSetpoint', val); } public get pHLevel(): number { return this.data.pHLevel; } public set pHLevel(val: number) { this.setDataVal('pHLevel', val); } + public get orpSetpoint(): number { return this.data.orpSetpoint; } + public set orpSetpoint(val: number) { this.setDataVal('orpSetpoint', val); } public get orpLevel(): number { return this.data.orpLevel; } public set orpLevel(val: number) { this.setDataVal('orpLevel', val); } public get saltLevel(): number { return this.data.saltLevel; } public set saltLevel(val: number) { this.setDataVal('saltLevel', val); } - /* public get waterFlow(): number { return this.data.waterFlow; } - public set waterFlow(val: number) { - if (this.waterFlow !== val) { - this.data.waterFlow = sys.board.valueMaps.chemControllerWaterFlow.transform(val); - this.hasChanged = true; - } - } */ - public get acidTankLevel(): number { return this.data.acidTankLevel || 0; } + public get acidTankLevel(): number { return this.data.acidTankLevel; } public set acidTankLevel(val: number) { this.setDataVal('acidTankLevel', val); } public get orpTankLevel(): number { return this.data.orpTankLevel || 0; } public set orpTankLevel(val: number) { this.setDataVal('orpTankLevel', val); } - /* public get status1(): number { return this.data.status1; } - public set status1(val: number) { - if (this.status1 !== val) { - this.data.status1 = sys.board.valueMaps.intelliChemStatus1.transform(val); - this.hasChanged = true; - } - } */ - /* public get status2(): number { return this.data.status2; } - public set status2(val: number) { - if (this.status2 !== val) { - this.data.status2 = sys.board.valueMaps.intelliChemStatus2.transform(val); - this.hasChanged = true; - } - } */ - /* public get alarms(): number { return typeof (this.data.alarms) !== 'undefined' ? this.data.alarms.val : undefined; } - public set alarms(val: number) { - if (this.alarms !== val) { - this.data.alarms = sys.board.valueMaps.chemControllerAlarms.transform(val); - this.hasChanged = true; - } - } */ public get pHDosingStatus(): number { return typeof (this.data.pHDosingStatus) !== 'undefined' ? this.data.pHDosingStatus.val : undefined; } public set pHDosingStatus(val: number) { if (this.pHDosingStatus !== val) { @@ -1421,13 +1404,6 @@ export class ChemControllerState extends EqState { this.hasChanged = true; } } - /* public get warnings(): number { return typeof (this.data.warnings) !== 'undefined' ? this.data.warnings.val : undefined; } - public set warnings(val: number) { - if (this.dosingStatus !== val) { - this.data.dosingStatus = sys.board.valueMaps.chemControllerWarnings.transform(val); - this.hasChanged = true; - } - } */ public get warnings(): ChemControllerStateWarnings { return new ChemControllerStateWarnings(this.data, 'warnings'); } public get alarms(): ChemControllerStateAlarms { return new ChemControllerStateAlarms(this.data, 'alarms'); } public get pHDosingTime(): number { return this.data.pHDosingTime; } @@ -1609,5 +1585,37 @@ export class AppVersionState extends EqState{ export class CommsState { public keepAlives: number; } +export class FilterStateCollection extends EqStateCollection { + public createItem(data: any): FilterState { return new FilterState(data); } +} +export class FilterState extends EqState { + public dataName: string = 'filter'; + public get id(): number { return this.data.id; } + 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 body(): number { return typeof (this.data.body) !== 'undefined' ? this.data.body.val : -1; } + public set body(val: number) { + if (this.body !== val) { + this.data.body = sys.board.valueMaps.bodies.transform(val); + this.hasChanged = true; + } + } + public get filterType(): number { return typeof this.data.filterType === 'undefined' ? undefined : this.data.filterType.val; } + public set filterType(val: number) { + if (this.filterType !== val) { + this.data.filterType = sys.board.valueMaps.filterTypes.transform(val); + this.hasChanged = true; + } + } + public get psi(): number { return this.data.psi; } + public set psi(val: number) { this.setDataVal('psi', val); } + public get filterPsi(): number { return this.data.filterPsi; } // do not exceed value. + public set filterPsi(val: number) { this.setDataVal('filterPsi', val); } + public get lastCleanDate(): Timestamp { return this.data.lastCleanDate; } + public set lastCleanDate(val: Timestamp) { this.setDataVal('lastCleanDate', val); } + public get needsCleaning(): number { return this.data.needsCleaning; } + public set needsCleaning(val: number) { this.setDataVal('needsCleaning', val); } +} export var state = new State(); \ No newline at end of file diff --git a/controller/boards/SystemBoard.ts b/controller/boards/SystemBoard.ts index f07fd387..d4b82fe9 100644 --- a/controller/boards/SystemBoard.ts +++ b/controller/boards/SystemBoard.ts @@ -20,9 +20,9 @@ import { webApp } from '../../web/Server'; import { conn } from '../comms/Comms'; import { Message, Outbound, Protocol } from '../comms/messages/Messages'; import { utils, Heliotrope } from '../Constants'; -import { Body, ChemController, Chlorinator, Circuit, CircuitGroup, CircuitGroupCircuit, ConfigVersion, CustomName, CustomNameCollection, EggTimer, Feature, General, Heater, ICircuit, LightGroup, LightGroupCircuit, Location, Options, Owner, PoolSystem, Pump, Schedule, sys, Valve, ControllerType, TempSensorCollection } from '../Equipment'; +import { Body, ChemController, Chlorinator, Circuit, CircuitGroup, CircuitGroupCircuit, ConfigVersion, CustomName, CustomNameCollection, EggTimer, Feature, General, Heater, ICircuit, LightGroup, LightGroupCircuit, Location, Options, Owner, PoolSystem, Pump, Schedule, sys, Valve, ControllerType, TempSensorCollection, Filter } from '../Equipment'; import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, ParameterOutOfRangeError } from '../Errors'; -import { BodyTempState, ChemControllerState, ChlorinatorState, ICircuitGroupState, ICircuitState, LightGroupState, PumpState, state, TemperatureState, VirtualCircuitState, HeaterState, ScheduleState } from '../State'; +import { BodyTempState, ChemControllerState, ChlorinatorState, ICircuitGroupState, ICircuitState, LightGroupState, PumpState, state, TemperatureState, VirtualCircuitState, HeaterState, ScheduleState, FilterState } from '../State'; export class byteValueMap extends Map { public transform(byte: number, ext?: number) { return extend(true, { val: byte || 0 }, this.get(byte) || this.get(0)); } @@ -493,24 +493,18 @@ export class byteValueMaps { [1, { name: 'monitoring', desc: 'Monitoring' }], [2, { name: 'mixing', desc: 'Mixing' }] ]); - /* ---- TO GET RID OF ----- */ - // public chemControllerWaterFlow: byteValueMap = new byteValueMap([ - // [0, { name: 'ok', desc: 'Ok' }], - // [1, { name: 'alarm', desc: 'Alarm - No Water Flow' }] - // ]); - // public intelliChemStatus1: byteValueMap = new byteValueMap([ - // // need to be verified - and combined with below? - // [37, { name: 'dosingAuto', desc: 'Dosing - Auto' }], - // [69, { name: 'dosingManual', desc: 'Dosing Acid - Manual' }], - // [85, { name: 'mixing', desc: 'Mixing' }], - // [101, { name: 'monitoring', desc: 'Monitoring' }] - // ]); - // public intelliChemStatus2: byteValueMap = new byteValueMap([ - // // need to be verified - // [20, { name: 'ok', desc: 'Ok' }], - // [22, { name: 'dosingManual', desc: 'Dosing Chlorine - Manual' }] + public filterTypes: byteValueMap = new byteValueMap([ + [0, { name: 'sand', desc: 'Sand Filter', hasBackwash: true }], + [1, { name: 'cartridge', desc: 'Cartridge Filter', hasBackwash: false }], + [2, { name: 'de', desc: 'DE Filter', hasBackwash: true }], + [3, { name: 'unknown', desc: 'unknown' }] + ]); + // public filterPSITargetTypes: byteValueMap = new byteValueMap([ + // [0, { name: 'none', desc: 'Do not use filter PSI' }], + // [1, { name: 'value', desc: 'Change filter at value' }], + // [2, { name: 'percent', desc: 'Change filter with % increase' }], + // [3, { name: 'increase', desc: 'Change filter with psi increase' }] // ]); - /* ---- TO GET RID OF END ----- */ public countries: byteValueMap = new byteValueMap([ [1, { name: 'US', desc: 'United States' }], [2, { name: 'CA', desc: 'Canada' }], @@ -619,6 +613,7 @@ export class SystemBoard { public features: FeatureCommands = new FeatureCommands(this); public chlorinator: ChlorinatorCommands = new ChlorinatorCommands(this); public heaters: HeaterCommands = new HeaterCommands(this); + public filters: FilterCommands = new FilterCommands(this); public chemControllers: ChemControllerCommands = new ChemControllerCommands(this); public schedules: ScheduleCommands = new ScheduleCommands(this); @@ -1194,122 +1189,6 @@ export class PumpCommands extends BoardCommands { let spump = state.pumps.getItemById(pump.id); spump.emitData('pumpExt', spump.getExtended()); } - /* public setPumpCircuit(pump: Pump, pumpCircuitDeltas: any) { - const origValues = extend(true, {}, pumpCircuitDeltas); - let { pumpCircuitId, circuit, rate, units } = pumpCircuitDeltas; - - let failed = false; - let succeeded = false; - // STEP 1 - Make a copy of the existing circuit - let shadowPumpCircuit: PumpCircuit = pump.circuits.getItemById(pumpCircuitId); - - // if pumpCircuitId === 0, do we have an available circuit - if (pumpCircuitId === 0 || typeof pumpCircuitId === 'undefined') { - pumpCircuitId = pump.nextAvailablePumpCircuit(); - // no circuits available - if (pumpCircuitId === 0) failed = true; - succeeded = true; - } - // if we get a bad pumpCircuitId then fail. Only idiots will get here. - else if (pumpCircuitId < 1 || pumpCircuitId > 8) failed = true; - if (failed) return { result: 'FAILED', reason: { pumpCircuitId: pumpCircuitId } }; - - // STEP 1A: Validate Circuit - // first check if we are missing both a new circuitId or existing circuitId - if (typeof circuit !== 'undefined') { - let _circuit = sys.circuits.getInterfaceById(circuit); - if (_circuit.isActive === false || typeof _circuit.type === 'undefined') { - // not a good circuit, fail - return { result: 'FAILED', reason: { circuit: circuit } }; - } - shadowPumpCircuit.circuit = circuit; - succeeded = true; - } - // if we don't have a circuit, fail - if (typeof shadowPumpCircuit.circuit === 'undefined') return { result: 'FAILED', reason: { circuit: 0 } }; - - // STEP 1B: Validate Rate/Units - let type = sys.board.valueMaps.pumpTypes.transform(pump.type).name; - switch (type) { - case 'vs': - // if VS, need rate only - // in fact, ignoring units - if (typeof rate === 'undefined') rate = shadowPumpCircuit.speed; - shadowPumpCircuit.units = sys.board.valueMaps.pumpUnits.getValue('rpm'); - shadowPumpCircuit.speed = pump.checkOrMakeValidRPM(rate); - shadowPumpCircuit.flow = undefined; - succeeded = true; - break; - case 'vf': - // if VF, need rate only - // in fact, ignoring units - if (typeof rate === 'undefined') rate = shadowPumpCircuit.flow; - shadowPumpCircuit.units = sys.board.valueMaps.pumpUnits.getValue('gpm'); - shadowPumpCircuit.flow = pump.checkOrMakeValidGPM(rate); - shadowPumpCircuit.speed = undefined; - succeeded = true; - break; - case 'vsf': - // if VSF, we can take either rate or units or both and make a valid pumpCircuit - if ((typeof rate !== 'undefined') && (typeof units !== 'undefined')) { - // do we have a valid combo of units and rate? -- do we need to check that or assume it will be passed in correctly? - if (sys.board.valueMaps.pumpUnits.getName(units) === 'rpm') { - shadowPumpCircuit.speed = pump.checkOrMakeValidRPM(rate); - shadowPumpCircuit.units = sys.board.valueMaps.pumpUnits.getValue('rpm'); - shadowPumpCircuit.flow = undefined; - succeeded = true; - } - else { - shadowPumpCircuit.flow = pump.checkOrMakeValidGPM(rate); - shadowPumpCircuit.units = sys.board.valueMaps.pumpUnits.getValue('gpm'); - shadowPumpCircuit.speed = undefined; - succeeded = true; - } - } - else if (typeof rate === 'undefined') { - // only have units; set default rate or use existing rate - if (sys.board.valueMaps.pumpUnits.getName(units) === 'rpm') { - shadowPumpCircuit.speed = pump.checkOrMakeValidRPM(shadowPumpCircuit.speed); - shadowPumpCircuit.flow = undefined; - succeeded = true; - } - else { - shadowPumpCircuit.flow = pump.checkOrMakeValidGPM(shadowPumpCircuit.flow); - shadowPumpCircuit.speed = undefined; - succeeded = true; - } - } - else if (typeof units === 'undefined') { - let rateType = pump.isRPMorGPM(rate); - if (rateType !== 'gpm') { - // default to speed if None - shadowPumpCircuit.flow = rate; - shadowPumpCircuit.speed = undefined; - } - else { - shadowPumpCircuit.speed = rate || 1000; - shadowPumpCircuit.flow = undefined; - } - shadowPumpCircuit.units = sys.board.valueMaps.pumpUnits.getValue(rateType); - succeeded = true; - } - break; - } - - if (!succeeded) return { result: 'FAILED', reason: origValues }; - // STEP 2: Copy values to real circuit -- if we get this far, we have a real circuit - let pumpCircuit: PumpCircuit = pump.circuits.getItemById(pumpCircuitId, true); - pumpCircuit.circuit = shadowPumpCircuit.circuit; - pumpCircuit.units = shadowPumpCircuit.units; - pumpCircuit.speed = shadowPumpCircuit.speed; - pumpCircuit.flow = shadowPumpCircuit.flow; - let spump = state.pumps.getItemById(pump.id); - spump.emitData('pumpExt', spump.getExtended()); - sys.emitEquipmentChange(); - // sys.board.virtualPumpControllers.setTargetSpeed(); - return { result: 'OK' }; - - } */ public setType(pump: Pump, pumpType: number) { // if we are changing pump types, need to clear out circuits @@ -1372,22 +1251,6 @@ export class PumpCommands extends BoardCommands { _availCircuits.push({ type: 'none', id: 255, name: 'Remove' }); return _availCircuits; } - // ping the pump and see if we get a response - /* public async initPump(pump: Pump) { - try { - await this.setPumpToRemoteControl(pump, true); - await this.requestPumpStatus(pump); - logger.info(`found pump ${ pump.id }`); - let spump = sys.pumps.getItemById(pump.id, true); - spump.type = pump.type; - pump.circuits.clear(); - await this.setPumpToRemoteControl(pump, false); - } - catch (err) { - logger.warn(`Init pump cannot find pump: ${ err.message }. Removing Pump.`); - if (pump.id > 1) { sys.pumps.removeItemById(pump.id); } - } - } */ public run(pump: Pump) { let spump = state.pumps.getItemById(pump.id); @@ -2300,7 +2163,7 @@ export class ScheduleCommands extends BoardCommands { for (let i = 0; i < schedules.length; i++) { let sched = schedules[i]; // check if the id's, min, hour match - if (sched.circuit === cbody.circuit && sched.isActive && Math.floor(sched.startTime / 60) === state.time.hours && sched.startTime % 60 === state.time.minutes) { + if (sched.circuit === cbody.circuit && sched.isActive && Math.floor(sched.startTime / 60) === state.time.hours && sched.startTime % 60 === state.time.minutes) { // check day match next as we need to iterate another array // let days = sys.board.valueMaps.scheduleDays.transform(sched.scheduleDays); // const days = sys.board.valueMaps.scheduleDays.transform(sched.scheduleDays); @@ -2871,8 +2734,8 @@ export class ChemControllerCommands extends BoardCommands { _reject(err); } else { - chem.pHSetpoint = _ph / 100; - chem.orpSetpoint = _orp; + schem.pHSetpoint = chem.pHSetpoint = _ph / 100; + schem.orpSetpoint = chem.orpSetpoint = _orp; chem.calciumHardness = _ch; chem.alkalinity = _alk; schem.acidTankLevel = Math.max(typeof data.acidTankLevel !== 'undefined' ? parseInt(data.acidTankLevel, 10) : schem.acidTankLevel, 0); @@ -3377,4 +3240,57 @@ export class VirtualChemController extends BoardCommands { else schem.virtualControllerStatus = -1; } } +} + +export class FilterCommands extends BoardCommands { + public setFilter(data: any): any { + let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); + if (id <= 0) id = sys.filters.length + 1; // set max filters? + if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid filter id: ${data.id}`, data.id, 'Filter')); + let filter = sys.filters.getItemById(id, id > 0); + let sfilter = state.filters.getItemById(id, id > 0); + let filterType = typeof data.filterType !== 'undefined' ? parseInt(data.filterType,10) : filter.filterType; + if (typeof filterType === 'undefined') filterType = sys.board.valueMaps.filterTypes.getValue('unknown'); + + if (typeof data.isActive !== 'undefined') { + if (utils.makeBool(data.isActive) === false) { + sys.filters.removeItemById(id); + state.filters.removeItemById(id); + return; + } + } + + let body = typeof data.body !== 'undefined' ? data.body : filter.body; + let name = typeof data.name !== 'undefined' ? data.name : filter.name; + let psi = typeof data.psi !== 'undefined' ? parseFloat(data.psi) : sfilter.psi; + let lastCleanDate = typeof data.lastCleanDate !== 'undefined' ? data.lastCleanDate : sfilter.lastCleanDate; + let filterPsi = typeof data.filterPsi !== 'undefined' ? parseInt(data.filterPsi, 10) : sfilter.filterPsi; + let needsCleaning = typeof data.needsCleaning !== 'undefined' ? data.needsCleaning : sfilter.needsCleaning; + + // Ensure all the defaults. + if (isNaN(psi)) psi = 0; + if (typeof body === 'undefined') body = 32; + + // At this point we should have all the data. Validate it. + if (!sys.board.valueMaps.filterTypes.valExists(filterType)) return Promise.reject(new InvalidEquipmentDataError(`Invalid filter type; ${filterType}`, 'Filter', filterType)); + + filter.filterType = sfilter.filterType = filterType; + filter.body = sfilter.body = body; + filter.filterType = sfilter.filterType = filterType; + filter.name = sfilter.name = name; + sfilter.psi = psi; + sfilter.filterPsi = filterPsi; + filter.needsCleaning = sfilter.needsCleaning = needsCleaning; + filter.lastCleanDate = sfilter.lastCleanDate = lastCleanDate; + sfilter.emitEquipmentChange(); + return sfilter; + } + + public deleteFilter(data: any): any { + let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10); + if (isNaN(id)) return; + sys.filters.removeItemById(id); + state.filters.removeItemById(id); + return state.filters.getItemById(id); + } } \ No newline at end of file diff --git a/defaultConfig.json b/defaultConfig.json index 25db228a..2083a4f7 100755 --- a/defaultConfig.json +++ b/defaultConfig.json @@ -90,7 +90,7 @@ }, "options": { "host": "0.0.0.0", - "port": 8081 + "port": 8081 } }, "influxDB": { @@ -99,7 +99,7 @@ "enabled": false, "fileName": "influxDB.json", "options": { - "protocol": "http", + "protocol": "http", "host": "192.168.0.1", "port": 32770, "username": "", @@ -143,6 +143,15 @@ "reconnection": true, "reconnectionDelayMax": 20000 } + }, + "equipmentManager": { + "name": "Equipment Manager", + "type": "equipmentManager", + "enabled": false, + "fileName": "equipmentManager.json", + "globals": {}, + "options": {}, + "uuid": "27d91217-bd91-4024-a39b-6d9be9bb3926" } } }, @@ -218,4 +227,4 @@ } }, "appVersion": "0.0.1" -} +} \ No newline at end of file diff --git a/web/Server.ts b/web/Server.ts index 19ced80f..19d473b6 100755 --- a/web/Server.ts +++ b/web/Server.ts @@ -41,6 +41,7 @@ import { URL } from "url"; import { HttpInterfaceBindings } from './interfaces/httpInterface'; import { InfluxInterfaceBindings } from './interfaces/influxInterface'; import { MqttInterfaceBindings } from './interfaces/mqttInterface'; +import { EquipmentManagerInterfaceBindings } from './interfaces/equipmentManagerInterface'; import { Timestamp } from '../controller/Constants'; import extend = require("extend"); import { ConfigSocket } from "./services/config/ConfigSocket"; @@ -115,6 +116,11 @@ export class WebServer { int.init(c); this._servers.push(int); break; + case 'equipmentManager': + int = new EquipmentManagerInterfaceServer(c.name, type); + int.init(c); + this._servers.push(int); + break; } } } @@ -782,6 +788,68 @@ export class MqttInterfaceServer extends ProtoServer { } } +export class EquipmentManagerInterfaceServer extends ProtoServer { + public bindingsPath: string; + public bindings: EquipmentManagerInterfaceBindings; + private _fileTime: Date = new Date(0); + private _isLoading: boolean = false; + public get isConnected() { return this.isRunning && this.bindings.events.length > 0; } + public init(cfg) { + this.uuid = cfg.uuid; + 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 EquipmentManagerInterfaceBindings(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; + let fileTime = new Date(0).valueOf(); + 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) { + 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 REMInterfaceServer extends ProtoServer { public init(cfg) { this.cfg = cfg; diff --git a/web/bindings/rulesManager.json b/web/bindings/rulesManager.json new file mode 100644 index 00000000..1bd1606f --- /dev/null +++ b/web/bindings/rulesManager.json @@ -0,0 +1,65 @@ +{ + "context": { + "name": "Rules Manager", + "vars": {}, + "options": { + "method": "PUT", + "headers": { + "CONTENT-TYPE": "application/json" + } + } + }, + "events": [ + { + "name": "controller", + "description": "Turn on needsCleaning flag for filter. Evaluate on a controller socket emit. True when the pool is on, pump is not priming, and filter psi > 15", + "enabled": true, + "filter": "@bind=data.id;===1 && @bind=state.filters.getItemById(1).psi;>15 && @bind=data.status.name;!=='priming'", + "options": { + "path": "/config/filter" + }, + "body": "{\"needsCleaning\":true}" + }, + { + "name": "controller", + "description": "Turn on needsCleaning flag for filter. Evaluate on a controller socket emit. True when the pool is on, pump is not priming, and filter psi > 15", + "enabled": true, + "filter": "@bind=data.id;===1 && @bind=state.filters.getItemById(1).psi;>15 && @bind=data.status.name;!=='priming'", + "options": { + "path": "/config/filter" + }, + "body": {"needsCleaning":true} + }, + { + "name": "controller", + "description": "Turn on needsCleaning flag for filter. Evaluate on a controller/time socket emit. Eval on the first day of every 3rd month when psi is >15 at any point", + "enabled": true, + "filter": "@bind=(new Date(data.time)).getMonth()%3;===0 && @bind=(new Date(data.time)).getDate();===1 && @bind=state.filters.getItemById(1).psi;>15", + "options": { + "path": "/config/filter" + }, + "body": {"needsCleaning":true} + }, + { + "name": "chemController", + "description": "Sets chlorinator to 50% if ORP is low", + "enabled": true, + "filter": "@bind=data.orpLevel; < @bind=sys.chemControllers.getItemById(1).orpSetpoint;", + "options": { + "path": "/config/chlorinator" + }, + "body": {"id":1, "poolSetpoint":50} + }, + { + "name": "chemController", + "description": "Sets chlorinator to 5% if ORP is in range", + "enabled": true, + "filter": "@bind=data.orpLevel; > @bind=sys.chemControllers.getItemById(1).orpSetpoint;", + "options": { + "path": "/config/chlorinator" + }, + "body": {"id":1, "poolSetpoint":5} + } + + ] +} \ No newline at end of file diff --git a/web/services/config/Config.ts b/web/services/config/Config.ts index e6103e41..891caf28 100755 --- a/web/services/config/Config.ts +++ b/web/services/config/Config.ts @@ -245,6 +245,14 @@ export class ConfigRoute { }; return res.status(200).send(opts); }); + app.get('/config/options/filters', (req, res) => { + let opts = { + types: sys.board.valueMaps.filterTypes.toArray(), + bodies: sys.board.bodies.getBodyAssociations(), + filters: sys.filters.get(), + }; + return res.status(200).send(opts); + }); /******* END OF CONFIGURATION PICK LISTS/REFERENCES AND VALIDATION ***********/ /******* ENDPOINTS FOR MODIFYING THE OUTDOOR CONTROL PANEL SETTINGS **********/ app.put('/config/tempSensors', async (req, res, next) => { @@ -258,6 +266,20 @@ export class ConfigRoute { } catch (err) { next(err); } }); + app.put('/config/filter', async (req, res, next) => { + try { + let sfilter = sys.board.filters.setFilter(req.body); + return res.status(200).send(sfilter.get(true)); + } + catch (err) { next(err); } + }); + app.delete('/config/filter', async (req, res, next) => { + try { + let sfilter = sys.board.filters.deleteFilter(req.body); + return res.status(200).send(sfilter.get(true)); + } + catch (err) { next(err); } + }); app.put('/config/general', async (req, res, next) => { // Change the options for the pool. try { @@ -443,27 +465,6 @@ export class ConfigRoute { } catch (err) { next(err); } }); - /* - app.put('/config/schedule/:id', (req, res) => { - let schedId = parseInt(req.params.id || '0', 10); - let eggTimer = sys.eggTimers.getItemById(schedId); - let sched = sys.schedules.getItemById(schedId); - if (eggTimer.circuit) eggTimer.set(req.body); - else if (sched.circuit) sched.set(req.body); - else return res.status(500).send('Not a valid id'); - return res.status(200).send('OK'); - }); - app.delete('/config/schedule/:id', (req, res) => { - let schedId = parseInt(req.params.id || '0', 10); - let eggTimer = sys.eggTimers.getItemById(schedId); - let sched = sys.schedules.getItemById(schedId); - if (eggTimer.circuit) eggTimer.delete(); - else if (sched.circuit) sched.delete(); - else return res.status(500).send('Not a valid id'); - return res.status(200).send('OK'); - }); - */ - /***** END OF ENDPOINTS FOR MODIFYINC THE OUTDOOR CONTROL PANEL SETTINGS *****/