/* nodejs-poolController. An application to control pool equipment. Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022. 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 extend from 'extend'; import { ncp } from "../nixie/Nixie"; import { NixieHeaterBase } from "../nixie/heaters/Heater"; import { Timestamp, utils } from '../Constants'; import {SystemBoard, byteValueMap, BodyCommands, FilterCommands, PumpCommands, SystemCommands, CircuitCommands, FeatureCommands, ValveCommands, HeaterCommands, ChlorinatorCommands, ChemControllerCommands, EquipmentIdRange} from './SystemBoard'; import { logger } from '../../logger/Logger'; import { state, CircuitState, ICircuitState, ICircuitGroupState, LightGroupState, ValveState, FilterState, BodyTempState, FeatureState } from '../State'; import { sys, Equipment, General, PoolSystem, CircuitGroupCircuit, CircuitGroup, ChemController, Circuit, Feature, Valve, ICircuit, Heater, LightGroup, LightGroupCircuit, ControllerType, Filter } from '../Equipment'; import { BoardProcessError, EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, ServiceParameterError } from '../Errors'; import { delayMgr } from '../Lockouts'; import { webApp } from "../../web/Server"; import { setTimeout } from 'timers/promises'; import { setTimeout as setTimeoutSync } from 'timers'; const addrsPentairPump = Object.freeze([96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111]); const addrsRegalModbusPump = Object.freeze( Array.from({ length: ((0xF7 - 0x15) / 2) + 1 }, (_, i) => 0x15 + i * 2) // Odd numbers fro 0x15 through 0xF7 ); const addrsNeptuneModbusPump = Object.freeze( Array.from({ length: 247 }, (_, i) => i + 1) // Modbus slave IDs 1..247 ); export class NixieBoard extends SystemBoard { constructor (system: PoolSystem){ super(system); this._statusInterval = 3000; this.equipmentIds.circuits = new EquipmentIdRange(1, function () { return this.start + sys.equipment.maxCircuits - 1; }); this.equipmentIds.features = new EquipmentIdRange(function () { return 129; }, function () { return this.start + sys.equipment.maxFeatures - 1; }); this.equipmentIds.circuitGroups = new EquipmentIdRange(function () { return this.start; }, function () { return this.start + sys.equipment.maxCircuitGroups - 1; }); this.equipmentIds.virtualCircuits = new EquipmentIdRange(function () { return this.start; }, function () { return 277; }); this.equipmentIds.features.start = 129; this.equipmentIds.circuitGroups.start = 193; this.equipmentIds.virtualCircuits.start = 237; this.valueMaps.equipmentMaster = new byteValueMap([ [1, { val: 1, name: 'ncp', desc: 'Nixie Control Panel' }], [2, { val: 2, name: 'ext', desc: 'External Control Panel'}] ]); this.valueMaps.panelModes = new byteValueMap([ [0, { name: 'auto', desc: 'Auto' }], [1, { name: 'service', desc: 'Service' }], [128, { name: 'timeout', desc: 'Timeout' }], [255, { name: 'error', desc: 'System Error' }] ]); this.valueMaps.featureFunctions = new byteValueMap([ [0, { name: 'generic', desc: 'Generic' }], [1, { name: 'spillway', desc: 'Spillway' }], [2, { name: 'spadrain', desc: 'Spa Drain' }] ]); this.valueMaps.circuitFunctions = new byteValueMap([ [0, { name: 'generic', desc: 'Generic' }], [1, { name: 'spillway', desc: 'Spillway' }], [2, { name: 'mastercleaner', desc: 'Master Cleaner', body: 1 }], [3, { name: 'chemrelay', desc: 'Chem Relay' }], [4, { name: 'light', desc: 'Light', isLight: true }], [5, { name: 'intellibrite', desc: 'Intellibrite', isLight: true, theme: 'intellibrite' }], [6, { name: 'globrite', desc: 'GloBrite', isLight: true, theme: 'intellibrite' }], [7, { name: 'globritewhite', desc: 'GloBrite White', isLight: true }], [8, { name: 'magicstream', desc: 'Magicstream', isLight: true, theme: 'magicstream' }], [9, { name: 'dimmer', desc: 'Dimmer', isLight: true }], [10, { name: 'colorcascade', desc: 'ColorCascade', isLight: true, theme: 'intellibrite' }], [11, { name: 'mastercleaner2', desc: 'Master Cleaner 2', body: 2 }], [12, { name: 'pool', desc: 'Pool', hasHeatSource: true, body: 1 }], [13, { name: 'spa', desc: 'Spa', hasHeatSource: true, body: 2 }], [14, { name: 'colorlogic', desc: 'ColorLogic', isLight: true, theme: 'colorlogic' }], [15, { name: 'spadrain', desc: 'Spa Drain' }], [16, { name: 'pooltone', desc: 'Pool Tone', isLight: true, theme: 'pooltone' }], [17, { name: 'watercolors', desc: 'WaterColors', isLight: true, theme: 'watercolors' }], ]); this.valueMaps.pumpTypes = new byteValueMap([ [1, { name: 'ss', desc: 'Single Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 1, relays: [{ id: 1, name: 'Pump On/Off' }], addresses: []}], [2, { name: 'ds', desc: 'Two Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 2, relays: [{ id: 1, name: 'Low Speed' }, { id: 2, name: 'High Speed' }], addresses: []}], [3, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true, addresses: addrsPentairPump }], [4, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true, addresses: addrsPentairPump }], [5, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true, addresses: addrsPentairPump }], [6, { name: 'hwvs', desc: 'Hayward Eco/TriStar VS', minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true, addresses: addrsPentairPump }], [7, { name: 'hwrly', desc: 'Hayward Relay VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, maxSpeeds: 8, relays: [{ id: 1, name: 'Step #1' }, { id: 2, name: 'Step #2'}, { id: 3, name: 'Step #3' }, { id: 4, name: 'Pump On' }], addresses: [] }], [100, { name: 'sf', desc: 'SuperFlo VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1, maxSpeeds: 4, relays: [{ id: 1, name: 'Program #1' }, { id: 2, name: 'Program #2' }, { id: 3, name: 'Program #3' }, { id: 4, name: 'Program #4' }], addresses: [] }], [200, { name: 'regalmodbus', desc: 'Regal Modbus', minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true, addresses: addrsRegalModbusPump}], [201, { name: 'neptunemodbus', desc: 'Neptune Modbus', minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true, addresses: addrsNeptuneModbusPump }], ]); // RSG - same as systemBoard definition; can delete. this.valueMaps.heatModes = new byteValueMap([ [0, { name: 'off', desc: 'Off' }], [3, { name: 'heater', desc: 'Heater' }], [5, { name: 'solar', desc: 'Solar Only' }], [12, { name: 'solarpref', desc: 'Solar Preferred' }] ]); this.valueMaps.scheduleDays = new byteValueMap([ [1, { name: 'mon', desc: 'Monday', dow: 1, bitval: 1 }], [2, { name: 'tue', desc: 'Tuesday', dow: 2, bitval: 2 }], [3, { name: 'wed', desc: 'Wednesday', dow: 3, bitval: 4 }], [4, { name: 'thu', desc: 'Thursday', dow: 4, bitval: 8 }], [5, { name: 'fri', desc: 'Friday', dow: 5, bitval: 16 }], [6, { name: 'sat', desc: 'Saturday', dow: 6, bitval: 32 }], [7, { name: 'sun', desc: 'Sunday', dow: 0, bitval: 64 }] ]); /** * groupCircuitStates value map: * 1: 'on' - Circuit should be ON when group is ON, OFF when group is OFF. * 2: 'off' - Circuit should be OFF when group is ON, ON when group is OFF. * 3: 'ignore' - Circuit is ignored by group state changes. * 4: 'on+ignore' - Circuit should be ON when group is ON, ignored when group is OFF. * 5: 'off+ignore' - Circuit should be OFF when group is ON, ignored when group is OFF. */ this.valueMaps.groupCircuitStates = new byteValueMap([ [1, { name: 'on', desc: 'On/Off' }], // 1: ON when group ON, OFF when group OFF [2, { name: 'off', desc: 'Off/On' }], // 2: OFF when group ON, ON when group OFF [3, { name: 'ignore', desc: 'Ignore' }], // 3: Ignored by group state [4, { name: 'on+ignore', desc: 'On/Ignore' }], // 4: ON when group ON, ignored when group OFF [5, { name: 'off+ignore', desc: 'Off/Ignore' }] // 5: OFF when group ON, ignored when group OFF ]); this.valueMaps.chlorinatorModel = new byteValueMap([ [0, { name: 'unknown', desc: 'unknown', capacity: 0, chlorinePerDay: 0, chlorinePerSec: 0 }], [1, { name: 'intellichlor--15', desc: 'IntelliChlor IC15', capacity: 15000, chlorinePerDay: 0.60, chlorinePerSec: 0.60 / 86400 }], [2, { name: 'intellichlor--20', desc: 'IntelliChlor IC20', capacity: 20000, chlorinePerDay: 0.70, chlorinePerSec: 0.70 / 86400 }], [3, { name: 'intellichlor--40', desc: 'IntelliChlor IC40', capacity: 40000, chlorinePerDay: 1.40, chlorinePerSec: 1.4 / 86400 }], [4, { name: 'intellichlor--60', desc: 'IntelliChlor IC60', capacity: 60000, chlorinePerDay: 2, chlorinePerSec: 2 / 86400 }], [5, { name: 'aquarite-t15', desc: 'AquaRite T15', capacity: 40000, chlorinePerDay: 1.47, chlorinePerSec: 1.47 / 86400 }], [6, { name: 'aquarite-t9', desc: 'AquaRite T9', capacity: 30000, chlorinePerDay: 0.98, chlorinePerSec: 0.98 / 86400 }], [7, { name: 'aquarite-t5', desc: 'AquaRite T5', capacity: 20000, chlorinePerDay: 0.735, chlorinePerSec: 0.735 / 86400 }], [8, { name: 'aquarite-t3', desc: 'AquaRite T3', capacity: 15000, chlorinePerDay: 0.53, chlorinePerSec: 0.53 / 86400 }], [9, { name: 'aquarite-925', desc: 'AquaRite 925', capacity: 25000, chlorinePerDay: 0.98, chlorinePerSec: 0.98 / 86400 }], [10, { name: 'aquarite-940', desc: 'AquaRite 940', capacity: 40000, chlorinePerDay: 1.47, chlorinePerSec: 1.47 / 86400 }] ]); // Keep this around for now so I can fart with the custom names array. //this.valueMaps.customNames = new byteValueMap( // sys.customNames.get().map((el, idx) => { // return [idx + 200, { name: el.name, desc: el.name }]; // }) //); this.valueMaps.scheduleDays.toArray = function () { let arrKeys = Array.from(this.keys()); let arr = []; for (let i = 0; i < arrKeys.length; i++) arr.push(extend(true, { val: arrKeys[i] }, this.get(arrKeys[i]))); return arr; } this.valueMaps.scheduleDays.transform = function (byte) { let days = []; let b = byte & 0x007F; for (let bit = 6; bit >= 0; bit--) { if ((byte & (1 << bit)) > 0) days.push(extend(true, {}, this.get(bit + 1))); } return { val: b, days: days }; }; this.valueMaps.expansionBoards = new byteValueMap([ [0, { name: 'nxp', part: 'NXP', desc: 'Nixie Single Body', bodies: 1, valves: 0, single: true, shared: false, dual: false }], [1, { name: 'nxps', part: 'NXPS', desc: 'Nixie Shared Body', bodies: 2, valves: 2, shared: true, dual: false, chlorinators: 1, chemControllers: 1 }], [2, { name: 'nxpd', part: 'NXPD', desc: 'Nixie Dual Body', bodies: 2, valves: 0, shared: false, dual: true, chlorinators: 2, chemControllers: 2 }], [255, { name: 'nxnb', part: 'NXNB', desc: 'Nixie No Body', bodies: 0, valves: 0, shared: false, dual: false, chlorinators: 0, chemControllers: 0 }] ]); this.valueMaps.virtualCircuits = new byteValueMap([ [237, { name: 'heatBoost', desc: 'Heat Boost' }], [238, { name: 'heatEnable', desc: 'Heat Enable' }], [239, { name: 'pumpSpeedUp', desc: 'Pump Speed +' }], [240, { name: 'pumpSpeedDown', desc: 'Pump Speed -' }], [244, { name: 'poolHeater', desc: 'Pool Heater' }], [245, { name: 'spaHeater', desc: 'Spa Heater' }], [246, { name: 'freeze', desc: 'Freeze' }], [247, { name: 'poolSpa', desc: 'Pool/Spa' }], [251, { name: 'heater', desc: 'Heater' }], [252, { name: 'solar', desc: 'Solar' }], [253, { name: 'solar1', desc: 'Solar Body 1' }], [254, { name: 'solar2', desc: 'Solar Body 2' }], [255, { name: 'solar3', desc: 'Solar Body 3' }], [256, { name: 'solar4', desc: 'Solar Body 4' }], [257, { name: 'poolHeatEnable', desc: 'Pool Heat Enable' }], [258, { name: 'anyHeater', desc: 'Any Heater' }], [259, { name: 'heatpump', desc: 'Heat Pump'}] ]); this.valueMaps.scheduleTimeTypes.merge([ [1, { name: 'sunrise', desc: 'Sunrise' }], [2, { name: 'sunset', desc: 'Sunset' }] ]); this.valueMaps.lightThemes = new byteValueMap([ // IntelliBrite Themes [0, { name: 'white', desc: 'White', types: ['intellibrite', 'magicstream'], sequence: 11 }], [1, { name: 'green', desc: 'Green', types: ['intellibrite', 'magicstream'], sequence: 9 }], [2, { name: 'blue', desc: 'Blue', types: ['intellibrite', 'magicstream'], sequence: 8 }], [3, { name: 'magenta', desc: 'Magenta', types: ['intellibrite', 'magicstream'], sequence: 12 }], [4, { name: 'red', desc: 'Red', types: ['intellibrite', 'magicstream'], sequence: 10 }], [5, { name: 'sam', desc: 'SAm Mode', types: ['intellibrite', 'magicstream'], sequence: 1 }], [6, { name: 'party', desc: 'Party', types: ['intellibrite', 'magicstream'], sequence: 2 }], [7, { name: 'romance', desc: 'Romance', types: ['intellibrite', 'magicstream'], sequence: 3 }], [8, { name: 'caribbean', desc: 'Caribbean', types: ['intellibrite', 'magicstream'], sequence: 4 }], [9, { name: 'american', desc: 'American', types: ['intellibrite', 'magicstream'], sequence: 5 }], [10, { name: 'sunset', desc: 'Sunset', types: ['intellibrite', 'magicstream'], sequence: 6 }], [11, { name: 'royal', desc: 'Royal', types: ['intellibrite', 'magicstream'], sequence: 7 }], // ColorLogic Themes [20, { name: 'cloudwhite', desc: 'Cloud White', types: ['colorlogic'], sequence: 7 }], [21, { name: 'deepsea', desc: 'Deep Sea', types: ['colorlogic'], sequence: 2 }], [22, { name: 'royalblue', desc: 'Royal Blue', types: ['colorlogic'], sequence: 3 }], [23, { name: 'afternoonskies', desc: 'Afternoon Skies', types: ['colorlogic'], sequence: 4 }], [24, { name: 'aquagreen', desc: 'Aqua Green', types: ['colorlogic'], sequence: 5 }], [25, { name: 'emerald', desc: 'Emerald', types: ['colorlogic'], sequence: 6 }], [26, { name: 'warmred', desc: 'Warm Red', types: ['colorlogic'], sequence: 8 }], [27, { name: 'flamingo', desc: 'Flamingo', types: ['colorlogic'], sequence: 9 }], [28, { name: 'vividviolet', desc: 'Vivid Violet', types: ['colorlogic'], sequence: 10 }], [29, { name: 'sangria', desc: 'Sangria', types: ['colorlogic'], sequence: 11 }], [30, { name: 'voodoolounge', desc: 'Voodoo Lounge', types: ['colorlogic'], sequence: 1 }], [31, { name: 'twilight', desc: 'Twilight', types: ['colorlogic'], sequence: 12 }], [32, { name: 'tranquility', desc: 'Tranquility', types: ['colorlogic'], sequence: 13 }], [33, { name: 'gemstone', desc: 'Gemstone', types: ['colorlogic'], sequence: 14 }], [34, { name: 'usa', desc: 'USA', types: ['colorlogic'], sequence: 15 }], [35, { name: 'mardigras', desc: 'Mardi Gras', types: ['colorlogic'], sequence: 16 }], [36, { name: 'coolcabaret', desc: 'Cabaret', types: ['colorlogic'], sequence: 17 }], // Sunseeker PoolTone Themes [40, { name: 'eveningsea', desc: 'Evening Sea', types: ['pooltone'], sequence: 1 }], [41, { name: 'eveningrivers', desc: 'Evening Rivers', types: ['pooltone'], sequence: 2 }], [42, { name: 'riviera', desc: 'Riviera', types: ['pooltone'], sequence: 3 }], [43, { name: 'neutralwhite', desc: 'Neutral White', types: ['pooltone'], sequence: 4 }], [44, { name: 'rainbow', desc: 'Rainbow', types: ['pooltone'], sequence: 5 }], [45, { name: 'colorriver', desc: 'Color River', types: ['pooltone'], sequence: 6 }], [46, { name: 'disco', desc: 'Disco', types: ['pooltone'], sequence: 7 }], [47, { name: 'fourseasons', desc: 'Four Seasons', types: ['pooltone'], sequence: 8 }], [48, { name: 'Party', desc: 'Party', types: ['pooltone'], sequence: 9 }], [49, { name: 'sunwhite', desc: 'Sun White', types: ['pooltone'], sequence: 10 }], [50, { name: 'red', desc: 'Red', types: ['pooltone'], sequence: 11 }], [51, { name: 'green', desc: 'Green', types: ['pooltone'], sequence: 12 }], [52, { name: 'blue', desc: 'Blue', types: ['pooltone'], sequence: 13 }], [53, { name: 'greenblue', desc: 'Green-Blue', types: ['pooltone'], sequence: 14 }], [54, { name: 'redgreen', desc: 'Red-Green', types: ['pooltone'], sequence: 15 }], [55, { name: 'bluered', desc: 'Blue-red', types: ['pooltone'], sequence: 16 }], // Jandy Pro Series WaterColors Themes [56, { name: 'alpinewhite', desc: 'Alpine White', types: ['watercolors'], sequence: 1 }], [57, { name: 'skyblue', desc: 'Sky Blue', types: ['watercolors'], sequence: 2 }], [58, { name: 'cobaltblue', desc: 'Cobalt Blue', types: ['watercolors'], sequence: 3 }], [59, { name: 'caribbeanblue', desc: 'Caribbean Blue', types: ['watercolors'], sequence: 4 }], [60, { name: 'springgreen', desc: 'Spring Green', types: ['watercolors'], sequence: 5 }], [61, { name: 'emeraldgreen', desc: 'Emerald Green', types: ['watercolors'], sequence: 6 }], [62, { name: 'emeraldrose', desc: 'Emerald Rose', types: ['watercolors'], sequence: 7 }], [63, { name: 'magenta', desc: 'Magenta', types: ['watercolors'], sequence: 8 }], [64, { name: 'violet', desc: 'Violet', types: ['watercolors'], sequence: 9 }], [65, { name: 'slowcolorsplash', desc: 'Slow Color Splash', types: ['watercolors'], sequence: 10 }], [66, { name: 'fastcolorsplash', desc: 'Fast Color Splash', types: ['watercolors'], sequence: 11 }], [67, { name: 'americathebeautiful', desc: 'America Beautiful', types: ['watercolors'], sequence: 12 }], [68, { name: 'fattuesday', desc: 'Fat Tuesday', types: ['watercolors'], sequence: 13 }], [69, { name: 'discotech', desc: 'Disco Tech', types: ['watercolors'], sequence: 14 }], [255, { name: 'none', desc: 'None' }] ]); this.valueMaps.lightColors = new byteValueMap([ [0, { name: 'white', desc: 'White' }], [16, { name: 'lightgreen', desc: 'Light Green' }], [32, { name: 'green', desc: 'Green' }], [48, { name: 'cyan', desc: 'Cyan' }], [64, { name: 'blue', desc: 'Blue' }], [80, { name: 'lavender', desc: 'Lavender' }], [96, { name: 'magenta', desc: 'Magenta' }], [112, { name: 'lightmagenta', desc: 'Light Magenta' }] ]); this.valueMaps.heatSources = new byteValueMap([ [1, { name: 'off', desc: 'Off' }], [2, { name: 'heater', desc: 'Heater' }], [3, { name: 'solar', desc: 'Solar Only' }], [4, { name: 'solarpref', desc: 'Solar Preferred' }], [5, { name: 'ultratemp', desc: 'Ultratemp Only' }], [6, { name: 'ultratemppref', desc: 'Ultratemp Pref' }], [9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heatpump Pref' }], [32, { name: 'nochange', desc: 'No Change' }] ]); this.valueMaps.heatStatus = new byteValueMap([ [0, { name: 'off', desc: 'Off' }], [1, { name: 'heater', desc: 'Heater' }], [2, { name: 'solar', desc: 'Solar' }], [3, { name: 'cooling', desc: 'Cooling' }], [6, { name: 'mtheat', desc: 'Heater' }], [4, { name: 'hpheat', desc: 'Heating' }], [8, { name: 'hpcool', desc: 'Cooling' }], [128, {name: 'cooldown', desc: 'Cooldown'}] ]); this.valueMaps.scheduleTypes = new byteValueMap([ [0, { name: 'runonce', desc: 'Run Once', startDate: true, startTime: true, endTime: true, days: false, heatSource: true, heatSetpoint: true }], [128, { name: 'repeat', desc: 'Repeats', startDate: false, startTime: true, endTime: true, days: 'multi', heatSource: true, heatSetpoint: true }] ]); this.valueMaps.remoteTypes = new byteValueMap([ [0, { name: 'none', desc: 'Not Installed', maxButtons: 0 }], [1, { name: 'is4', desc: 'iS4 Spa-Side Remote', maxButtons: 4 }], [2, { name: 'is10', desc: 'iS10 Spa-Side Remote', maxButtons: 10 }], [3, { name: 'quickTouch', desc: 'Quick Touch Remote', maxButtons: 4 }], [4, { name: 'spaCommand', desc: 'Spa Command', maxButtons: 10 }] ]); } public async closeAsync() { logger.info(`Closing Nixie Board`); await ncp.closeAsync(); } public async checkConfiguration() { state.status = sys.board.valueMaps.controllerStatus.transform(0, 0); state.emitControllerChange(); // Set all the schedule data based upon the config. for (let i = 0; i < sys.schedules.length; i++) { let sched = sys.schedules.getItemByIndex(i); let ssched = state.schedules.getItemById(sched.id, true); ssched.circuit = sched.circuit; ssched.scheduleDays = sched.scheduleDays; ssched.scheduleType = sched.scheduleType; ssched.changeHeatSetpoint = sched.changeHeatSetpoint; ssched.heatSetpoint = sched.heatSetpoint; ssched.coolSetpoint = sched.coolSetpoint; ssched.heatSource = sched.heatSource; ssched.startTime = sched.startTime; ssched.endTime = sched.endTime; ssched.startTimeType = sched.startTimeType; ssched.endTimeType = sched.endTimeType; ssched.startDate = sched.startDate; ssched.isActive = sched.isActive = true; sched.disabled = sched.disabled; ssched.display = sched.display; } state.status = sys.board.valueMaps.controllerStatus.transform(1, 100); state.emitControllerChange(); } public async initNixieBoard() { try { this.killStatusCheck(); let self = this; sys.general.options.clockSource = 'server'; state.status = sys.board.valueMaps.controllerStatus.transform(0, 0); // First lets clear out all the messages. state.equipment.messages.removeItemByCode('EQ') // Set up all the default information for the controller. This should be done // for the startup of the system. The equipment installed at module 0 is the main // system descriptor. let mod = sys.equipment.modules.getItemById(0, true); mod.master = 1; //[0, { name: 'nxp', part: 'NXP', desc: 'Nixie Single Body', bodies: 1, valves: 2, shared: false, dual: false }], //[1, { name: 'nxps', part: 'NXPS', desc: 'Nixie Shared Body', bodies: 2, valves: 4, shared: true, dual: false, chlorinators: 1, chemControllers: 1 }], //[2, { name: 'nxpd', part: 'NXPD', desc: 'Nixe Dual Body', bodies: 2, valves: 2, shared: false, dual: true, chlorinators: 2, chemControllers: 2 }], //[255, { name: 'nxu', part: 'Unspecified', desc: 'Nixie No Body', bodies: 0, valves: 0, shared: false, dual: false, chlorinators: 0, chemControllers: 0 }] let type = typeof mod.type !== 'undefined' ? this.valueMaps.expansionBoards.transform(mod.type) : this.valueMaps.expansionBoards.transform(0); logger.info(`Initializing Nixie Control Panel for ${type.desc}`); state.equipment.shared = sys.equipment.shared = type.shared; state.equipment.dual = sys.equipment.dual = type.dual; state.equipment.single = sys.equipment.single = sys.equipment.shared === false && sys.equipment.dual === false; sys.equipment.controllerFirmware = '1.0.0'; mod.type = type.val; mod.part = type.part; let md = mod.get(); md['bodies'] = type.bodies; md['part'] = type.part; md['valves'] = type.valves; mod.name = type.name; sys.equipment.model = mod.desc = type.desc; state.equipment.maxValves = sys.equipment.maxValves = 32; state.equipment.maxCircuits = sys.equipment.maxCircuits = 40; state.equipment.maxFeatures = sys.equipment.maxFeatures = 32; state.equipment.maxHeaters = sys.equipment.maxHeaters = 16; state.equipment.maxLightGroups = sys.equipment.maxLightGroups = 16; state.equipment.maxCircuitGroups = sys.equipment.maxCircuitGroups = 16; state.equipment.maxSchedules = sys.equipment.maxSchedules = 100; state.equipment.maxPumps = sys.equipment.maxPumps = 16; state.equipment.controllerType = sys.controllerType; sys.equipment.maxCustomNames = 0; state.equipment.model = type.desc; state.equipment.maxBodies = sys.equipment.maxBodies = type.bodies; let bodyUnits = sys.general.options.units === 0 ? 1 : 2; sys.equipment.single = typeof type.single !== 'undefined' ? type.single : false; if (typeof state.temps.units === 'undefined' || state.temps.units < 0) state.temps.units = sys.general.options.units; if (type.bodies > 0) { let pool = sys.bodies.getItemById(1, true); let sbody = state.temps.bodies.getItemById(1, true); if (typeof pool.type === 'undefined') pool.type = 0; if (typeof pool.name === 'undefined') pool.name = type.dual ? 'Body 1' : 'Pool'; if (typeof pool.capacity === 'undefined') pool.capacity = 0; if (typeof pool.setPoint === 'undefined') pool.setPoint = 0; pool.circuit = 6; pool.isActive = true; pool.master = 1; pool.capacityUnits = bodyUnits; sbody.name = pool.name; sbody.setPoint = pool.setPoint; sbody.circuit = pool.circuit; sbody.type = pool.type; // We need to add in a circuit for 6. let circ = sys.circuits.getItemById(6, true, { name: pool.name, showInFeatures: false }); let scirc = state.circuits.getItemById(6, true); //[12, { name: 'pool', desc: 'Pool', hasHeatSource: true }], //[13, { name: 'spa', desc: 'Spa', hasHeatSource: true }] circ.type = 12; if (typeof circ.showInFeatures === 'undefined') circ.showInFeatures = false; circ.isActive = true; circ.master = 1; scirc.showInFeatures = circ.showInFeatures; scirc.type = circ.type; scirc.name = circ.name; if (type.shared || type.dual) { // We are going to add two bodies and prune off the othergood ls. let spa = sys.bodies.getItemById(2, true); if (typeof spa.type === 'undefined') spa.type = type.dual ? 0 : 1; if (typeof spa.name === 'undefined') spa.name = type.dual ? 'Body 2' : 'Spa'; if (typeof spa.capacity === 'undefined') spa.capacity = 0; if (typeof spa.setPoint === 'undefined') spa.setPoint = 0; circ = sys.circuits.getItemById(1, true, {name: spa.name, showInFeatures: false }); circ.type = type.dual ? 12 : 13; circ.isActive = true; circ.master = 1; spa.circuit = 1; spa.isActive = true; spa.master = 1; sbody = state.temps.bodies.getItemById(2, true); sbody.name = spa.name; sbody.setPoint = spa.setPoint; sbody.circuit = spa.circuit; sbody.type = spa.type; spa.capacityUnits = bodyUnits; scirc = state.circuits.getItemById(1, true); scirc.showInFeatures = circ.showInFeatures; scirc.type = circ.type; scirc.name = circ.name; } else { // Remove the items that are not part of our board. sys.bodies.removeItemById(2); state.temps.bodies.removeItemById(2); sys.circuits.removeItemById(1); state.circuits.removeItemById(1); } } else { sys.bodies.removeItemById(1); sys.bodies.removeItemById(2); state.temps.bodies.removeItemById(1); state.temps.bodies.removeItemById(2); sys.circuits.removeItemById(1); state.circuits.removeItemById(1); sys.circuits.removeItemById(6); state.circuits.removeItemById(6); } sys.equipment.setEquipmentIds(); sys.board.bodies.initFilters(); state.status = sys.board.valueMaps.controllerStatus.transform(2, 0); // Add up all the stuff we need to initialize. let total = sys.bodies.length; total += sys.circuits.length; total += sys.heaters.length; total += sys.chlorinators.length; total += sys.chemControllers.length; total += sys.filters.length; total += sys.pumps.length; total += sys.valves.length; total += sys.schedules.length; this.initValves(); sys.board.heaters.initTempSensors(); await this.verifySetup(); await ncp.initAsync(sys); sys.board.heaters.updateHeaterServices(); state.cleanupState(); logger.info(`${sys.equipment.model} control board initialized`); //state.status = sys.board.valueMaps.controllerStatus.transform(1, 100); state.mode = sys.board.valueMaps.panelModes.encode('auto'); // At this point we should have the start of a board so lets check to see if we are ready or if we are stuck initializing. await setTimeout(5000); state.status = sys.board.valueMaps.controllerStatus.transform(1, 100); await self.processStatusAsync(); } catch (err) { state.status = 255; logger.error(`Error Initializing Nixie Control Panel ${err.message}`); } } public initValves() { logger.info(`Initializing Intake/Return valves`); let iv = sys.valves.find(elem => elem.isIntake === true); let rv = sys.valves.find(elem => elem.isReturn === true); if (sys.equipment.shared) { if (typeof iv === 'undefined') iv = sys.valves.getItemById(sys.valves.getMaxId(false, 0) + 1, true); iv.isIntake = true; iv.isReturn = false; iv.type = 0; iv.name = 'Intake'; iv.circuit = 247; iv.isActive = true; iv.master = 1; if (typeof rv === 'undefined') rv = sys.valves.getItemById(sys.valves.getMaxId(false, 0) + 1, true); rv.isIntake = false; rv.isReturn = true; rv.name = 'Return'; rv.type = 0; rv.circuit = 247; rv.isActive = true; rv.master = 1; } else { if (typeof iv !== 'undefined') { sys.valves.removeItemById(iv.id); state.valves.removeItemById(iv.id); } if (typeof rv !== 'undefined') { sys.valves.removeItemById(rv.id); state.valves.removeItemById(rv.id); } } } public async verifySetup() { try { // In here we are going to attempt to check all the nixie relays. We will not check the other equipment just the items // that make up a raw pool like the circuits. The other stuff is the stuff of the equipment control. let circs = sys.circuits.toArray().filter((val) => { return val.controller === 1; }); for (let i = 0; i < circs.length; i++) { let circ = circs[i]; // Make sure we have a circuit identified in the ncp if it is controlled by Nixie. let c = await ncp.circuits.initCircuitAsync(circ); // Now we should have the circuit from nixie so check the status to see if it can be // controlled. i.e. The comms are up. await c.validateSetupAsync(circ, state.circuits.getItemById(circ.id)) } // Now we need to validate the heaters. Some heaters will be connected via a relay. If they have comms we will check it. let heaters = sys.heaters.toArray().filter((val) => { return val.controller === 1 }); for (let i = 0; i < heaters.length; i++) { let heater = heaters[i]; let h = await ncp.heaters.initHeaterAsync(heater); } // If we have relay based pumps, init them here... ss, ds, superflo let pumps = sys.heaters.toArray().filter((val) => { return val.controller === 1 }); for (let i = 0; i < pumps.length; i++) { let pump = pumps[i]; if (pump.type === 65){ // how are we defining ss and superflo? await ncp.pumps.initPumpAsync(pump); } } } catch (err) { logger.error(`Error verifying setup`); } } public equipmentMaster = 1; public system: NixieSystemCommands = new NixieSystemCommands(this); public circuits: NixieCircuitCommands = new NixieCircuitCommands(this); public features: NixieFeatureCommands = new NixieFeatureCommands(this); //public chlorinator: NixieChlorinatorCommands = new NixieChlorinatorCommands(this); public bodies: NixieBodyCommands = new NixieBodyCommands(this); public filters: NixieFilterCommands = new NixieFilterCommands(this); public pumps: NixiePumpCommands = new NixiePumpCommands(this); //public schedules: NixieScheduleCommands = new NixieScheduleCommands(this); public heaters: NixieHeaterCommands = new NixieHeaterCommands(this); public valves: NixieValveCommands = new NixieValveCommands(this); public chemControllers: NixieChemControllerCommands = new NixieChemControllerCommands(this); public async setControllerType(obj): Promise { try { if (typeof obj.model === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Nixie: Controller model not supplied`, 'model', obj.model)); let mt = this.valueMaps.expansionBoards.findItem(obj.model); if (typeof mt === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Nixie: A valid Controller model not supplied ${obj.model}`, 'model', obj.model)); this.killStatusCheck(); let mod = sys.equipment.modules.getItemById(0, true); mod.type = mt.val; await this.initNixieBoard(); state.emitControllerChange(); return sys.equipment; } catch (err) { logger.error(`Error setting Nixie controller type.`); } } } export class NixieBodyCommands extends BodyCommands { } export class NixieFilterCommands extends FilterCommands { public async setFilterStateAsync(filter: Filter, fstate: FilterState, isOn: boolean) { try { await ncp.filters.setFilterStateAsync(fstate, isOn); } catch (err) { return Promise.reject(new BoardProcessError(`Nixie: Error setFiterStateAsync ${err.message}`, 'setFilterStateAsync')); } } } export class NixieSystemCommands extends SystemCommands { protected _modeTimer: NodeJS.Timeout; public cancelDelay(): Promise { delayMgr.cancelPumpValveDelays(); delayMgr.cancelHeaterCooldownDelays(); delayMgr.cancelHeaterStartupDelays(); delayMgr.cancelCleanerStartDelays(); delayMgr.cancelManualPriorityDelays(); state.delay = sys.board.valueMaps.delay.getValue('nodelay'); return Promise.resolve(state.data.delay); } public setManualOperationPriority(id: number): Promise { let cstate = state.circuits.getInterfaceById(id); delayMgr.setManualPriorityDelay(cstate); return Promise.resolve(cstate); } public setDateTimeAsync(obj: any): Promise { return Promise.resolve(); } public getDOW() { return this.board.valueMaps.scheduleDays.toArray(); } public async setGeneralAsync(obj: any): Promise { let general = sys.general.get(); if (typeof obj.alias === 'string') sys.general.alias = obj.alias; if (typeof obj.options !== 'undefined') await sys.board.system.setOptionsAsync(obj.options); if (typeof obj.location !== 'undefined') await sys.board.system.setLocationAsync(obj.location); if (typeof obj.owner !== 'undefined') await sys.board.system.setOwnerAsync(obj.owner); return new Promise(function (resolve, reject) { resolve(sys.general); }); } public async setModelAsync(obj: any) { try { // First things first. } catch (err) { return logger.error(`Error setting Nixie Model: ${err.message}`); } } public async setPanelModeAsync(data: any): Promise { let mode = sys.board.valueMaps.panelModes.findItem(data.mode); let timeout = parseInt(data.timeout, 10); if (typeof mode === 'undefined') return Promise.reject(new ServiceParameterError(`Invalid mode value cannot set mode`, 'setPanelModeAsync', 'mode', data.mode)); switch (mode.name) { case 'timeout': if (isNaN(timeout) || timeout <= 0) return Promise.reject(new ServiceParameterError(`Invalid timeout value cannot set mode`, 'setPanelModeAsync', 'timeout', data.timeout)); await this.initServiceMode(mode, timeout); break; case 'service': await this.initServiceMode(mode); break; case 'auto': // Ok we are switching back to auto. // 1. Kill the timeout timer if it exists. // 2. Set the mode to auto. if (this._modeTimer) clearTimeout(this._modeTimer); this._modeTimer = null; state.mode = 0; webApp.emitToClients('panelMode', { mode: mode, remaining: 0 }); break; } } private checkServiceTimeout(mode: any, start: number, timeout: number, interval?: number) { if (this._modeTimer) clearTimeout(this._modeTimer); this._modeTimer = null; // The timeout is in seconds so we will need to deal with that. let elapsed = (new Date().getTime() - start) / 1000; let remaining = timeout - elapsed; logger.info(`Timeout: ${timeout} Elapsed: ${elapsed}`); if (remaining > 0) { webApp.emitToClients('panelMode', { mode: mode, remaining: remaining, elapsed: elapsed, timeout: timeout }); this._modeTimer = setTimeoutSync(() => { this.checkServiceTimeout(mode, start, timeout, interval || 1000); }, interval || 1000); } else { webApp.emitToClients('panelMode', { mode: sys.board.valueMaps.panelModes.transform(0), remaining: 0 }); state.mode = 0; } } public async initServiceMode(mode, timeout?: number) { if (this._modeTimer) clearTimeout(this._modeTimer); for (let i = 0; i < sys.circuits.length; i++) { let circ = sys.circuits.getItemByIndex(i); if (circ.master === 1) { let cstate = state.circuits.getItemById(circ.id); if (cstate.isOn) await sys.board.circuits.setCircuitStateAsync(circ.id, false, true); } } delayMgr.clearAllDelays(); state.mode = mode.val; // Shut everything down. await ncp.setServiceModeAsync(); if (timeout > 0) { let start = new Date().getTime(); this.checkServiceTimeout(mode, start, timeout, 1000); webApp.emitToClients('panelMode', { mode: mode, remaining: timeout, elapsed: 0, timeout: timeout }); } else { webApp.emitToClients('panelMode', { mode: mode }); } } } export class NixieCircuitCommands extends CircuitCommands { // This is our poll loop for circuit relay states. public async syncCircuitRelayStates() { try { if (state.mode !== 0) return; for (let i = 0; i < sys.circuits.length; i++) { // Run through all the controlled circuits to see whether they should be triggered or not. let circ = sys.circuits.getItemByIndex(i); if (circ.master === 1 && circ.isActive) { let cstate = state.circuits.getItemById(circ.id); if (cstate.isOn) await ncp.circuits.setCircuitStateAsync(cstate, cstate.isOn); } } } catch (err) { logger.error(`syncCircuitRelayStates: Error synchronizing circuit relays ${err.message}`); } } public async setCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { sys.board.suspendStatus(true); try { // We need to do some routing here as it is now critical that circuits, groups, and features // have their own processing. The virtual controller used to only deal with one circuit. if (sys.board.equipmentIds.circuitGroups.isInRange(id)) return await sys.board.circuits.setCircuitGroupStateAsync(id, val); else if (sys.board.equipmentIds.features.isInRange(id)) return await sys.board.features.setFeatureStateAsync(id, val); let circuit: ICircuit = sys.circuits.getInterfaceById(id, false, { isActive: false }); if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit or Feature id ${id} not valid`, id, 'Circuit')); let circ = state.circuits.getInterfaceById(id, circuit.isActive !== false); if (state.mode !== 0) return circ; if (circ.stopDelay) { // Send this off so that the relays are properly set. In the end we cannot change right now. If this // happens to be a body circuit then the relay state will be skipped anyway. await ncp.circuits.setCircuitStateAsync(circ, circ.isOn); return circ; } let newState = utils.makeBool(val); let ctype = sys.board.valueMaps.circuitFunctions.getName(circ.type); // Filter out any special circuit types. switch (ctype) { case 'pool': case 'spa': await this.setBodyCircuitStateAsync(id, newState, ignoreDelays); break; case 'mastercleaner': case 'mastercleaner2': await this.setCleanerCircuitStateAsync(id, newState, ignoreDelays); break; case 'spillway': await this.setSpillwayCircuitStateAsync(id, newState, ignoreDelays); break; case 'spadrain': await this.setDrainCircuitStateAsync(id, newState, ignoreDelays); break; default: await ncp.circuits.setCircuitStateAsync(circ, newState); break; } // Let the main nixie controller set the circuit state and affect the relays if it needs to. return state.circuits.getInterfaceById(circ.id); } catch (err) { logger.error(`Nixie: setCircuitState ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setCircuitStateAsync ${err.message}`, 'setCircuitState')); } finally { state.emitEquipmentChanges(); ncp.pumps.syncPumpStates(); sys.board.suspendStatus(false); await sys.board.processStatusAsync(); } } protected async setCleanerCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { try { let cstate = state.circuits.getItemById(id); let circuit = sys.circuits.getItemById(id); // We know which body the cleaner belongs to by an attribute on the circuit function. let ctype = sys.board.valueMaps.circuitFunctions.get(circuit.type); let bstate = state.temps.bodies.getItemById(ctype.body || 1); // Cleaner lockout should occur when // 1. The body circuit is off. // 2. The spillway mode is running. // Optional modes include // 1. The current body is heating with solar. // Lockouts are cleared when // 1. The above conditions are no longer true. // 2. The user requests the circuit to be off. if (!val) { // We can always turn a cleaner circuit off. Even if a delay is underway. delayMgr.clearCleanerStartDelays(bstate.id); await ncp.circuits.setCircuitStateAsync(cstate, false); } else if (val) { logger.info(`Setting cleaner circuit ${cstate.name} to ${val}`); // Alright we are turning the cleaner on. // To turn on the cleaner circuit we must first ensure the body is on. If it is not then we abort. if (!bstate.isOn) { logger.info(`Cannot turn on cleaner circuit ${cstate.name}. ${bstate.name} is not running`); await ncp.circuits.setCircuitStateAsync(cstate, false); return cstate; } // If there is a drain circuit going shut that thing off. await this.turnOffDrainCircuits(ignoreDelays); // If solar is currently on and the cleaner solar delay is set then we need to calculate a delay // to turn on the cleaner. let delayTime = 0; let dtNow = new Date().getTime(); if (typeof ignoreDelays === 'undefined' || !ignoreDelays) { if (sys.general.options.cleanerSolarDelay && sys.general.options.cleanerSolarDelayTime > 0) { let circBody = state.circuits.getItemById(bstate.circuit); // If the body has not been on or the solar heater has not been on long enough then we need to delay the startup. if (sys.board.valueMaps.heatStatus.getName(bstate.heatStatus) === 'solar') { // Check for the solar delay. We need to know when the heater first kicked in. A cleaner and solar // heater can run at the same time but the heater must be on long enough for the timer to expire. // The reasoning behind this is so that the booster pump can be assured that there is sufficient pressure // for it to start and any air from the solar has had time to purge through the system. let heaters = sys.heaters.getSolarHeaters(bstate.id); let startTime = 0; for (let i = 0; i < heaters.length; i++) { let heater = heaters.getItemByIndex(i); let hstate = state.heaters.getItemById(heater.id); startTime = Math.max(startTime, hstate.startTime.getTime()); } // Lets see if we have a solar start delay. delayTime = Math.max(Math.round(((sys.general.options.cleanerSolarDelayTime * 1000) - (dtNow - startTime))) / 1000, delayTime); } } if (sys.general.options.cleanerStartDelay && sys.general.options.cleanerStartDelayTime) { let bcstate = state.circuits.getItemById(bstate.circuit); let stime = typeof bcstate.startTime === 'undefined' ? dtNow : (dtNow - bcstate.startTime.getTime()); // So we should be started. Lets determine whethere there should be any delay. delayTime = Math.max(Math.round(((sys.general.options.cleanerStartDelayTime * 1000) - stime) / 1000), delayTime); logger.info(`Cleaner delay time calculated to ${delayTime}`); } } if (delayTime > 5) delayMgr.setCleanerStartDelay(cstate, bstate.id, delayTime); else await ncp.circuits.setCircuitStateAsync(cstate, true); } return cstate; } catch (err) { return Promise.reject(new BoardProcessError(`Nixie: Error setting cleaner circuit state: ${err.message}`, 'setCleanerCircuitStateAsync')); } } protected async setBodyCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { try { let cstate = state.circuits.getItemById(id); let circuit = sys.circuits.getItemById(id); let bstate = state.temps.bodies.getBodyByCircuitId(id); if (cstate.isOn === val) return; // If body is already in desired state, don't do anything. // https://github.com/tagyoureit/nodejs-poolController/issues/361#issuecomment-1186087763 if (val) { // We are turning on a body circuit. logger.verbose(`Turning on a body circuit ${bstate.name}`); if (sys.equipment.shared === true) { // If we are turning on and this is a shared system it means that we need to turn off // the other circuit. let delayPumps = false; await this.turnOffDrainCircuits(ignoreDelays); if (bstate.id === 2) await this.turnOffSpillwayCircuits(); if (sys.general.options.pumpDelay === true && ignoreDelays !== true) { // Now that this is off check the valve positions. If they are not currently in the correct position we need to delay any attached pump // so that it does not come on while the valve is rotating. Default 30 seconds. let iValves = sys.valves.getIntake(); for (let i = 0; i < iValves.length && !delayPumps; i++) { let vstate = state.valves.getItemById(iValves[i].id); if (vstate.isDiverted === true && circuit.type === 12) delayPumps = true; else if (vstate.isDiverted === false && circuit.type === 13) delayPumps = true; } if (!delayPumps) { let rValves = sys.valves.getReturn(); for (let i = 0; i < rValves.length && !delayPumps; i++) { let vstate = state.valves.getItemById(rValves[i].id); if (vstate.isDiverted === true && circuit.type === 12) delayPumps = true; else if (vstate.isDiverted === false && circuit.type === 13) delayPumps = true; } } } // If we are shared we need to turn off the other circuit. let offType = circuit.type === 12 ? 13 : 12; let off = sys.circuits.get().filter(elem => elem.type === offType); let delayCooldown = false; // Turn the circuits off that are part of the shared system. We are going back to the board // just in case we got here for a circuit that isn't on the current defined panel. for (let i = 0; i < off.length; i++) { let coff = off[i]; let bsoff = state.temps.bodies.getBodyByCircuitId(coff.id); let csoff = state.circuits.getItemById(coff.id); // Ensure the cleaner circuits for this body are off. await this.turnOffCleanerCircuits(bsoff); if (csoff.isOn) { logger.verbose(`Turning off shared body ${coff.name} circuit`); delayMgr.clearBodyStartupDelay(bsoff); if (bsoff.heaterCooldownDelay && ignoreDelays !== true) { // In this condition we are requesting that the shared body start when the cooldown delay // has finished. This will add this request to the cooldown delay code. The setHeaterCooldownDelay // code is expected to be re-entrant and checks the id so that it does not clear // the original request if it is asked for again. // NOTE: There is room for improvement here. For instance, if the result // of turning on the circuit is that the heater(s) requiring cooldown will result in being on // then why not cancel the current cooldown cycle and let the user get on with it. // Consider: // 1. Check each heater attached to the off body to see if it is also attached to the on body. // 2. If the heater is attached check to see if there is any cooldown time left on it. // 3. If the above conditions are true cancel the cooldown cycle. logger.verbose(`${bsoff.name} is already in Cooldown mode`); delayMgr.setHeaterCooldownDelay(bsoff, bstate); delayCooldown = true; } else { // We need to deal with heater cooldown delays here since you cannot turn off the body while the heater is // cooling down. This means we need to check to see if the heater requires cooldown then set a delay for it // if it does. The delay manager will shut the body off and start the new body when it is done. let heaters = sys.board.heaters.getHeatersByCircuitId(circuit.id); let cooldownTime = 0; if (ignoreDelays !== true) { for (let j = 0; j < heaters.length; j++) { let nheater = ncp.heaters.find(x => x.id === heaters[j].id) as NixieHeaterBase; cooldownTime = Math.max(nheater.getCooldownTime(), cooldownTime); } } if (cooldownTime > 0) { // We need do start a cooldown cycle for the body. If there is already // a cooldown underway this will append the on to it. delayMgr.setHeaterCooldownDelay(bsoff, bstate, cooldownTime * 1000); delayCooldown = true; } else { await ncp.circuits.setCircuitStateAsync(csoff, false); bsoff.isOn = false; } } } } if (delayCooldown) return cstate; if (delayPumps === true) sys.board.pumps.setPumpValveDelays([id, bstate.circuit]); } // Now we need to set the startup delay for all the heaters. This is true whether // the system is shared or not so lets get a list of all the associated heaters for the body in question. if (sys.general.options.heaterStartDelay && sys.general.options.heaterStartDelayTime > 0) { let heaters = sys.board.heaters.getHeatersByCircuitId(circuit.id); for (let j = 0; j < heaters.length; j++) { let hstate = state.heaters.getItemById(heaters[j].id); delayMgr.setHeaterStartupDelay(hstate); } } await ncp.circuits.setCircuitStateAsync(cstate, val); bstate.isOn = val; } else if (!val) { // Alright we are turning off a circuit that will result in a body shutting off. If this // circuit is already under delay it should have been processed out earlier. delayMgr.cancelPumpValveDelays(); delayMgr.cancelHeaterStartupDelays(); sys.board.heaters.clearPrevHeaterOffTemp(); if (cstate.startDelay) delayMgr.clearBodyStartupDelay(bstate); await this.turnOffCleanerCircuits(bstate); if (sys.equipment.shared && bstate.id === 2) await this.turnOffDrainCircuits(ignoreDelays); logger.verbose(`Turning off a body circuit ${circuit.name}`); if (cstate.isOn) { // Check to see if we have any heater cooldown delays that need to take place. let heaters = sys.board.heaters.getHeatersByCircuitId(circuit.id); let cooldownTime = 0; for (let j = 0; j < heaters.length; j++) { let nheater = ncp.heaters.find(x => x.id === heaters[j].id) as NixieHeaterBase; cooldownTime = Math.max(nheater.getCooldownTime(), cooldownTime); } if (cooldownTime > 0) { logger.info(`Starting a Cooldown Delay ${cooldownTime}sec`); // We need do start a cooldown cycle for the body. delayMgr.setHeaterCooldownDelay(bstate, undefined, cooldownTime * 1000); } else { await ncp.circuits.setCircuitStateAsync(cstate, val); bstate.isOn = val; } } else { bstate.isOn = val; } } return cstate; } catch (err) { logger.error(`Nixie: Error setBodyCircuitStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setBodyCircuitStateAsync ${err.message}`, 'setBodyCircuitStateAsync')); } } protected async setSpillwayCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { try { let cstate = state.circuits.getItemById(id); let delayPumps = false; if (cstate.isOn !== val) { if (sys.equipment.shared === true) { // First we need to check to see if the pool is on. if (val) { let spastate = state.circuits.getItemById(1); if (spastate.isOn) { logger.warn(`Cannot turn ${cstate.name} on because ${spastate.name} is on`); return cstate; } // If there are any drain circuits or features that are currently engaged we need to turn them off. await this.turnOffDrainCircuits(ignoreDelays); if (sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([6, id]); } else if (!val && !ignoreDelays) { // If we are turning off and there is another circuit that ties to the same pumps then we need set a valve delay. This means // that if the pool circuit is on then we need to delay the pumps. However, if there is no other circuit that needs // the pump to be on, then no harm no foul a delay in the pump won't mean anything. // Conditions where this should not delay. // 1. Another spillway circuit or feature is on. // 2. There is no other running circuit that will affect the intake or return. let arrIds = sys.board.valves.getBodyValveCircuitIds(true); if (arrIds.length > 1) { if (sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) { sys.board.pumps.setPumpValveDelays([6, id]); } } } } } logger.verbose(`Turning ${val ? 'on' : 'off'} a spillway circuit ${cstate.name}`); await ncp.circuits.setCircuitStateAsync(cstate, val); return cstate; } catch (err) { logger.error(`Nixie: Error setSpillwayCircuitStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setSpillwayCircuitStateAsync ${err.message}`, 'setBodyCircuitStateAsync')); } } protected async setDrainCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { try { // Drain circuits can be very bad. This is because they can be turned on then never turned off // we may want to create some limits are to how long they can be on or even force them off // if for instance the spa is not on. // RULES FOR DRAIN CIRCUITS: // 1. All spillway circuits must be off. let cstate = state.circuits.getItemById(id); let delayPumps = false; if (cstate.isOn !== val) { if (sys.equipment.shared === true) { let spastate = state.temps.bodies.getItemById(2); let poolstate = state.temps.bodies.getItemById(1); // First we need to check to see if the pool is on. if (val) { if (spastate.isOn || spastate.startDelay || poolstate.isOn || poolstate.startDelay) { logger.warn(`Cannot turn ${cstate.name} on because a body is on`); return cstate; } // If there are any spillway circuits or features that are currently engaged we need to turn them off. await this.turnOffSpillwayCircuits(true); // If there are any cleaner circuits on for the main body turn them off. await this.turnOffCleanerCircuits(state.temps.bodies.getItemById(1), true); if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 1, 6]); } else if (!val && !ignoreDelays) { if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 1, 6]); } } } logger.verbose(`Turning ${val ? 'on' : 'off'} a drain circuit ${cstate.name}`); await ncp.circuits.setCircuitStateAsync(cstate, val); return cstate; } catch (err) { logger.error(`Nixie: Error setDrainCircuitStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setDrainCircuitStateAsync ${err.message}`, 'setDrainCircuitStateAsync')); } } public toggleCircuitStateAsync(id: number): Promise { let circ = state.circuits.getInterfaceById(id); return this.setCircuitStateAsync(id, !(circ.isOn || false)); } public async setLightThemeAsync(id: number, theme: number) { if (sys.board.equipmentIds.circuitGroups.isInRange(id)) { await this.setLightGroupThemeAsync(id, theme); return Promise.resolve(state.lightGroups.getItemById(id)); } let cstate = state.circuits.getItemById(id); if (state.mode !== 0) return cstate; let circ = sys.circuits.getItemById(id); let thm = sys.board.valueMaps.lightThemes.findItem(theme); if (typeof thm !== 'undefined' && typeof thm.sequence !== 'undefined' && circ.master === 1) { logger.info(`Setting light theme for ${circ.name} to ${thm.name} [${thm.sequence}]`); await ncp.circuits.setLightThemeAsync(id, thm); } cstate.lightingTheme = theme; return Promise.resolve(cstate as ICircuitState); } public setDimmerLevelAsync(id: number, level: number): Promise { let circ = state.circuits.getItemById(id); circ.level = level; return Promise.resolve(circ as ICircuitState); } public getCircuitReferences(includeCircuits?: boolean, includeFeatures?: boolean, includeVirtual?: boolean, includeGroups?: boolean) { let arrRefs = []; if (includeCircuits) { // RSG: converted this to getItemByIndex because hasHeatSource isn't actually stored as part of the data for (let i = 0; i < sys.circuits.length; i++) { let c = sys.circuits.getItemByIndex(i); arrRefs.push({ id: c.id, name: c.name, type: c.type, equipmentType: 'circuit', nameId: c.nameId, hasHeatSource: c.hasHeatSource }); } } if (includeFeatures) { let features = sys.features.get(); for (let i = 0; i < sys.features.length; i++) { let c = features[i]; arrRefs.push({ id: c.id, name: c.name, type: c.type, equipmentType: 'feature', nameId: c.nameId }); } } if (includeVirtual) { let vcs = sys.board.valueMaps.virtualCircuits.toArray(); for (let i = 0; i < vcs.length; i++) { let c = vcs[i]; arrRefs.push({ id: c.val, name: c.desc, equipmentType: 'virtual', assignableToPumpCircuit: c.assignableToPumpCircuit }); } } if (includeGroups) { let groups = sys.circuitGroups.get(); for (let i = 0; i < groups.length; i++) { let c = groups[i]; arrRefs.push({ id: c.id, name: c.name, equipmentType: 'circuitGroup', nameId: c.nameId }); } groups = sys.lightGroups.get(); for (let i = 0; i < groups.length; i++) { let c = groups[i]; arrRefs.push({ id: c.id, name: c.name, equipmentType: 'lightGroup', nameId: c.nameId }); } } arrRefs.sort((a, b) => { return a.id > b.id ? 1 : a.id === b.id ? 0 : -1; }); return arrRefs; } public getLightReferences() { let circuits = sys.circuits.get(); let arrRefs = []; for (let i = 0; i < circuits.length; i++) { let c = circuits[i]; let type = sys.board.valueMaps.circuitFunctions.transform(c.type); if (type.isLight) arrRefs.push({ id: c.id, name: c.name, type: c.type, equipmentType: 'circuit', nameId: c.nameId }); } return arrRefs; } public getLightThemes(type?: number) { let tobj = (typeof type === 'undefined') ? sys.board.valueMaps.circuitFunctions.transformByName('intellibrite') : sys.board.valueMaps.circuitFunctions.transform(type); let arrThemes = sys.board.valueMaps.lightThemes.toArray(); let arr = []; for (let i = 0; i < arrThemes.length; i++) { if (tobj.name === arrThemes[i].type) arr.push(arrThemes[i]); } return arr; } public getCircuitFunctions() { return super.getCircuitFunctions(); } public getCircuitNames() { return [...sys.board.valueMaps.circuitNames.toArray(), ...sys.board.valueMaps.customNames.toArray()]; } public async setCircuitAsync(data: any): Promise { try { let id = parseInt(data.id, 10); if (id <= 0 || isNaN(id)) { // You can add any circuit so long as it isn't 1 or 6. id = sys.circuits.getNextEquipmentId(sys.board.equipmentIds.circuits, [1, 6]); } if (isNaN(id) || !sys.board.equipmentIds.circuits.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id: ${data.id}`, data.id, 'Circuit')); let circuit = sys.circuits.getItemById(id, true); let scircuit = state.circuits.getItemById(id, true); scircuit.isActive = circuit.isActive = true; circuit.master = 1; if (data.name) circuit.name = scircuit.name = data.name; else if (!circuit.name && !data.name) circuit.name = scircuit.name = Circuit.getIdName(id); if (typeof data.type !== 'undefined' || typeof circuit.type === 'undefined') circuit.type = scircuit.type = parseInt(data.type, 10) || 0; this.assertSinglePoolSpaType(id, circuit.type); if (typeof data.freeze !== 'undefined' || typeof circuit.freeze === 'undefined') circuit.freeze = utils.makeBool(data.freeze) || false; if (typeof data.showInFeatures !== 'undefined' || typeof data.showInFeatures === 'undefined') circuit.showInFeatures = scircuit.showInFeatures = utils.makeBool(data.showInFeatures); if (typeof data.dontStop !== 'undefined' && utils.makeBool(data.dontStop) === true) data.eggTimer = 1440; if (typeof data.eggTimer !== 'undefined' || typeof circuit.eggTimer === 'undefined') circuit.eggTimer = parseInt(data.eggTimer, 10) || 0; if (typeof data.connectionId !== 'undefined') circuit.connectionId = data.connectionId; if (typeof data.deviceBinding !== 'undefined') circuit.deviceBinding = data.deviceBinding; circuit.dontStop = circuit.eggTimer === 1440; // update end time in case egg timer is changed while circuit is on sys.board.circuits.setEndTime(circuit, scircuit, scircuit.isOn, true); sys.emitEquipmentChange(); state.emitEquipmentChanges(); ncp.circuits.setCircuitAsync(circuit, data); return circuit; } catch (err) { logger.error(`Error setting circuit data ${err.message}`); } } public async setCircuitGroupAsync(obj: any): Promise { let group: CircuitGroup = null; let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1; if (id <= 0) { // We are adding a circuit group so we need to get the next equipment id. For circuit groups and light groups, they share ids. let range = sys.board.equipmentIds.circuitGroups; for (let i = range.start; i <= range.end; i++) { if (!sys.lightGroups.find(elem => elem.id === i) && !sys.circuitGroups.find(elem => elem.id === i)) { id = i; break; } } } if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max circuit group id exceeded`, id, 'CircuitGroup')); if (isNaN(id) || !sys.board.equipmentIds.circuitGroups.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit group id: ${obj.id}`, obj.id, 'CircuitGroup')); group = sys.circuitGroups.getItemById(id, true); let sgroup = state.circuitGroups.getItemById(id, true); return new Promise((resolve, reject) => { if (typeof obj.name !== 'undefined') group.name = sgroup.name = obj.name; if (typeof obj.nameId !== 'undefined') sgroup.nameId = group.nameId =obj.nameId; if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440; if (typeof obj.eggTimer !== 'undefined') group.eggTimer = Math.min(Math.max(parseInt(obj.eggTimer, 10), 0), 1440); if (typeof obj.showInFeatures !== 'undefined') sgroup.showInFeatures = group.showInFeatures = utils.makeBool(obj.showInFeatures); sgroup.type = group.type; group.dontStop = group.eggTimer === 1440; group.isActive = sgroup.isActive = true; if (typeof obj.circuits !== 'undefined') { for (let i = 0; i < obj.circuits.length; i++) { let c = group.circuits.getItemByIndex(i, true, { id: i + 1 }); let cobj = obj.circuits[i]; if (typeof cobj.circuit !== 'undefined') c.circuit = cobj.circuit; if (typeof cobj.desiredState !== 'undefined') c.desiredState = parseInt(cobj.desiredState, 10); else if (typeof cobj.desiredStateOn !== 'undefined') { // Shim for prior interfaces that send desiredStateOn. c.desiredState = utils.makeBool(cobj.desiredStateOn) ? 0 : 1; //c.desiredStateOn = utils.makeBool(cobj.desiredStateOn); } //RKS: 09-26-20 There is no such thing as a lighting theme on a circuit group circuit. That is what lighGroups are for. //if (typeof cobj.lightingTheme !== 'undefined') c.lightingTheme = parseInt(cobj.lightingTheme, 10); } group.circuits.length = obj.circuits.length; // RSG - removed as this will delete circuits that were not changed } // update end time in case group is changed while circuit is on sys.board.circuits.setEndTime(group, sgroup, sgroup.isOn, true); resolve(group); }); } public async setLightGroupAsync(obj: any): Promise { let group: LightGroup = null; let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1; if (id <= 0) { // We are adding a circuit group so we need to get the next equipment id. For circuit groups and light groups, they share ids. let range = sys.board.equipmentIds.circuitGroups; for (let i = range.start; i <= range.end; i++) { if (!sys.lightGroups.find(elem => elem.id === i) && !sys.circuitGroups.find(elem => elem.id === i)) { id = i; break; } } } if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max circuit light group id exceeded`, id, 'LightGroup')); if (isNaN(id) || !sys.board.equipmentIds.circuitGroups.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit group id: ${obj.id}`, obj.id, 'LightGroup')); group = sys.lightGroups.getItemById(id, true); let sgroup = state.lightGroups.getItemById(id, true); return new Promise((resolve, reject) => { if (typeof obj.name !== 'undefined') sgroup.name = group.name = obj.name; if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440; if (typeof obj.eggTimer !== 'undefined') group.eggTimer = Math.min(Math.max(parseInt(obj.eggTimer, 10), 0), 1440); group.dontStop = group.eggTimer === 1440; group.isActive = true; if (typeof obj.circuits !== 'undefined') { for (let i = 0; i < obj.circuits.length; i++) { let cobj = obj.circuits[i]; let c: LightGroupCircuit; if (typeof cobj.id !== 'undefined') c = group.circuits.getItemById(parseInt(cobj.id, 10), true); else if (typeof cobj.circuit !== 'undefined') c = group.circuits.getItemByCircuitId(parseInt(cobj.circuit, 10), true); else c = group.circuits.getItemByIndex(i, true, { id: i + 1 }); if (typeof cobj.circuit !== 'undefined') c.circuit = cobj.circuit; if (typeof cobj.lightingTheme !== 'undefined') c.lightingTheme = parseInt(cobj.lightingTheme, 10); if (typeof cobj.color !== 'undefined') c.color = parseInt(cobj.color, 10); if (typeof cobj.swimDelay !== 'undefined') c.swimDelay = parseInt(cobj.swimDelay, 10); if (typeof cobj.position !== 'undefined') c.position = parseInt(cobj.position, 10); } // RKS: 09-25-21 - This has to be here. Not sure the goal of not setting the entire circuit array when saving the group. // group.circuits.length = obj.circuits.length; // RSG - removed as this will delete circuits that were not changed group.circuits.length = obj.circuits.length; sgroup.emitEquipmentChange(); } resolve(group); }); } public async deleteCircuitGroupAsync(obj: any): Promise { let id = parseInt(obj.id, 10); if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid group id: ${obj.id}`, obj.id, 'CircuitGroup')); if (!sys.board.equipmentIds.circuitGroups.isInRange(id)) return; if (typeof obj.id !== 'undefined') { let group = sys.circuitGroups.getItemById(id, false); let sgroup = state.circuitGroups.getItemById(id, false); sys.circuitGroups.removeItemById(id); state.circuitGroups.removeItemById(id); group.isActive = false; sgroup.isOn = false; sgroup.isActive = false; sgroup.emitEquipmentChange(); return new Promise((resolve, reject) => { resolve(group); }); } else return Promise.reject(new InvalidEquipmentIdError('Group id has not been defined', id, 'CircuitGroup')); } public async deleteLightGroupAsync(obj: any): Promise { let id = parseInt(obj.id, 10); if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid group id: ${obj.id}`, obj.id, 'LightGroup')); if (!sys.board.equipmentIds.circuitGroups.isInRange(id)) return; if (typeof obj.id !== 'undefined') { let group = sys.lightGroups.getItemById(id, false); let sgroup = state.lightGroups.getItemById(id, false); sys.lightGroups.removeItemById(id); state.lightGroups.removeItemById(id); group.isActive = false; sgroup.isOn = false; sgroup.isActive = false; sgroup.emitEquipmentChange(); return new Promise((resolve, reject) => { resolve(group); }); } else return Promise.reject(new InvalidEquipmentIdError('Group id has not been defined', id, 'LightGroup')); } public async deleteCircuitAsync(data: any): Promise { let id = parseInt(data.id, 10); if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id: ${data.id}`, data.id, 'Circuit')); if (!sys.board.equipmentIds.circuits.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id: ${data.id}`, data.id, 'Circuit')); let circuit = sys.circuits.getInterfaceById(id); let cstate = state.circuits.getInterfaceById(id); if (circuit instanceof Circuit) { sys.circuits.removeItemById(circuit.id); state.circuits.removeItemById(circuit.id); cstate.isActive = circuit.isActive = false; } if (circuit instanceof Feature) { sys.features.removeItemById(circuit.id); state.features.removeItemById(circuit.id); cstate.isActive = circuit.isActive = false; } cstate.emitEquipmentChange(); return new Promise((resolve, reject) => { resolve(circuit); }); } public deleteCircuit(data: any) { if (typeof data.id !== 'undefined') { let circuit = sys.circuits.getInterfaceById(data.id); if (circuit instanceof Circuit) { sys.circuits.removeItemById(circuit.id); state.circuits.removeItemById(circuit.id); return; } if (circuit instanceof Feature) { sys.features.removeItemById(data.id); state.features.removeItemById(data.id); return; } } } public getNameById(id: number) { if (id < 200) return sys.board.valueMaps.circuitNames.transform(id).desc; else return sys.customNames.getItemById(id - 200).name; } public async setLightGroupThemeAsync(id: number, theme: number): Promise { const grp = sys.lightGroups.getItemById(id); const sgrp = state.lightGroups.getItemById(id); //grp.lightingTheme = sgrp.lightingTheme = theme; let thm = sys.board.valueMaps.lightThemes.transform(theme); sgrp.action = sys.board.valueMaps.circuitActions.getValue('lighttheme'); try { // Go through and set the theme for all lights in the group. for (let i = 0; i < grp.circuits.length; i++) { let c = grp.circuits.getItemByIndex(i); //let cstate = state.circuits.getItemById(c.circuit); await sys.board.circuits.setLightThemeAsync(c.circuit, theme); await sys.board.circuits.setCircuitStateAsync(c.circuit, false); } await setTimeout(5000); // Turn the circuits all back on again. for (let i = 0; i < grp.circuits.length; i++) { let c = grp.circuits.getItemByIndex(i); //let cstate = state.circuits.getItemById(c.circuit); await sys.board.circuits.setCircuitStateAsync(c.circuit, true); } sgrp.lightingTheme = theme; return sgrp; } catch (err) { return Promise.reject(err); } finally { sgrp.action = 0; sgrp.emitEquipmentChange(); } } public async setLightGroupAttribsAsync(group: LightGroup): Promise { let grp = sys.lightGroups.getItemById(group.id); try { grp.circuits.clear(); for (let i = 0; i < group.circuits.length; i++) { let circuit = grp.circuits.getItemByIndex(i); grp.circuits.add({ id: i, circuit: circuit.circuit, color: circuit.color, position: i, swimDelay: circuit.swimDelay }); } let sgrp = state.lightGroups.getItemById(group.id); sgrp.hasChanged = true; // Say we are dirty but we really are pure as the driven snow. return Promise.resolve(grp); } catch (err) { return Promise.reject(err); } } //public async runLightCommandAsync(id: number, command: string): Promise { // let circ = sys.circuits.getItemById(id); // try { // let type = sys.board.valueMaps.circuitFunctions.transform(circ.type); // let cmd = sys.board.valueMaps.lightCommands.findItem(command); // if (typeof cmd === 'undefined') return Promise.reject(new InvalidOperationError(`Light command ${command} does not exist`, 'runLightCommandAsync')); // if (typeof cmd.sequence !== 'undefined' && circ.master === 1) { // await sys.board.circuits.setCircuitStateAsync(id, true); // await ncp.circuits.sendOnOffSequenceAsync(id, cmd.sequence); // } // return state.circuits.getItemById(id); // } // catch (err) { return Promise.reject(`Error runLightCommandAsync ${err.message}`); } //} public async sequenceLightGroupAsync(id: number, operation: string): Promise { let sgroup = state.lightGroups.getItemById(id); if (state.mode !== 0) return sgroup; let grp = sys.lightGroups.getItemById(id); let nop = sys.board.valueMaps.circuitActions.getValue(operation); try { switch (operation) { case 'colorsync': sgroup.action = nop; sgroup.emitEquipmentChange(); for (let i = 0; i < grp.circuits.length; i++) { let c = grp.circuits.getItemByIndex(i); await sys.board.circuits.setCircuitStateAsync(c.circuit, false); } await setTimeout(10000); // Turn the circuits all back on again. for (let i = 0; i < grp.circuits.length; i++) { let c = grp.circuits.getItemByIndex(i); await sys.board.circuits.setCircuitStateAsync(c.circuit, true); } break; case 'colorset': sgroup.action = nop; sgroup.emitEquipmentChange(); await setTimeout(5000); break; case 'colorswim': sgroup.action = nop; sgroup.emitEquipmentChange(); await setTimeout(5000); break; } return sgroup; } catch (err) { return Promise.reject(err); } finally { sgroup.action = 0; sgroup.emitEquipmentChange(); } } public async setCircuitGroupStateAsync(id: number, val: boolean): Promise { let grp = sys.circuitGroups.getItemById(id, false, { isActive: false }); if (grp.dataName !== 'circuitGroupConfig') return await sys.board.circuits.setLightGroupStateAsync(id, val); let gstate = state.circuitGroups.getItemById(grp.id, grp.isActive !== false); if (state.mode !== 0) return gstate; let circuits = grp.circuits.toArray(); sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(gstate.id), gstate, val); gstate.isOn = val; let arr = []; for (let i = 0; i < circuits.length; i++) { let circuit:CircuitGroupCircuit = circuits[i]; // The desiredState will be as follows. // 1 = on, 2 = off, 3 = ignore. let cval = true; if (circuit.desiredState === 1) cval = val ? true : false; else if (circuit.desiredState === 2) cval = val ? false : true; else if (circuit.desiredState === 3) continue; else if (circuit.desiredState === 4){ // on/ignore if (val) cval = true; else continue; } else if (circuit.desiredState === 5){ // off/ignore if (val) cval = false; else continue; } await sys.board.circuits.setCircuitStateAsync(circuit.circuit, cval); //arr.push(sys.board.circuits.setCircuitStateAsync(circuit.circuit, cval)); } return state.circuitGroups.getItemById(grp.id, grp.isActive !== false); //return new Promise(async (resolve, reject) => { // await Promise.all(arr).catch((err) => { reject(err) }); // resolve(gstate); //}); } public async setLightGroupStateAsync(id: number, val: boolean): Promise { let grp = sys.circuitGroups.getItemById(id, false, { isActive: false }); if (grp.dataName === 'circuitGroupConfig') return await sys.board.circuits.setCircuitGroupStateAsync(id, val); let gstate = state.lightGroups.getItemById(grp.id, grp.isActive !== false); if (state.mode !== 0) return gstate; let circuits = grp.circuits.toArray(); sys.board.circuits.setEndTime(grp, gstate, val); gstate.isOn = val; let arr = []; for (let i = 0; i < circuits.length; i++) { let circuit = circuits[i]; // RSG 4/3/24 - This function was executing and returing the results to the array; not pushing the fn to the array. //arr.push(sys.board.circuits.setCircuitStateAsync(circuit.circuit, val)); arr.push(async () => { await sys.board.circuits.setCircuitStateAsync(circuit.circuit, val) }); } // return new Promise(async (resolve, reject) => { // await Promise.all(arr).catch((err) => { reject(err) }); // resolve(gstate); // }); return new Promise(async (resolve, reject) => { try { Promise.all(arr.map(async func => await func())); resolve(gstate); } catch (err) { reject(err); }; }); }; } export class NixieFeatureCommands extends FeatureCommands { public async setFeatureAsync(obj: any): Promise { let id = parseInt(obj.id, 10); if (id <= 0 || isNaN(id)) { id = sys.features.getNextEquipmentId(sys.board.equipmentIds.features); } if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${obj.id}`, obj.id, 'Feature')); if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Feature id out of range: ${id}: ${sys.board.equipmentIds.features.start} to ${sys.board.equipmentIds.features.end}`, obj.id, 'Feature')); let feature = sys.features.getItemById(id, true); let sfeature = state.features.getItemById(id, true); feature.isActive = true; sfeature.isOn = false; if (obj.nameId) { feature.nameId = sfeature.nameId = obj.nameId; feature.name = sfeature.name = sys.board.valueMaps.circuitNames.get(obj.nameId); } else if (obj.name) feature.name = sfeature.name = obj.name; else if (!feature.name && !obj.name) feature.name = sfeature.name = `feature${obj.id}`; if (typeof obj.type !== 'undefined') feature.type = sfeature.type = parseInt(obj.type, 10); else if (!feature.type && typeof obj.type !== 'undefined') feature.type = sfeature.type = 0; if (typeof obj.freeze !== 'undefined') feature.freeze = utils.makeBool(obj.freeze); if (typeof obj.showInFeatures !== 'undefined') feature.showInFeatures = sfeature.showInFeatures = utils.makeBool(obj.showInFeatures); if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440; if (typeof obj.eggTimer !== 'undefined') feature.eggTimer = parseInt(obj.eggTimer, 10); feature.dontStop = feature.eggTimer === 1440; // update end time in case feature is changed while circuit is on sys.board.circuits.setEndTime(feature, sfeature, sfeature.isOn, true); return new Promise((resolve, reject) => { resolve(feature); }); } public async deleteFeatureAsync(obj: any): Promise { let id = parseInt(obj.id, 10); if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${obj.id}`, obj.id, 'Feature')); if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${obj.id}`, obj.id, 'Feature')); if (typeof obj.id !== 'undefined') { let feature = sys.features.getItemById(id, false); let sfeature = state.features.getItemById(id, false); sys.features.removeItemById(id); state.features.removeItemById(id); feature.isActive = false; sfeature.isOn = false; sfeature.showInFeatures = false; sfeature.isActive = false; sfeature.emitEquipmentChange(); return new Promise((resolve, reject) => { resolve(feature); }); } else Promise.reject(new InvalidEquipmentIdError('Feature id has not been defined', undefined, 'Feature')); } public async setFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { try { if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature')); if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature')); let feature = sys.features.getItemById(id); let fstate = state.features.getItemById(feature.id, feature.isActive !== false); feature.master = 1; if (state.mode !== 0) return fstate; let ftype = sys.board.valueMaps.featureFunctions.getName(feature.type); if(val && !fstate.isOn) sys.board.circuits.setEndTime(feature, fstate, val); switch (ftype) { case 'spadrain': this.setDrainFeatureStateAsync(id, val, ignoreDelays); break; case 'spillway': this.setSpillwayFeatureStateAsync(id, val, ignoreDelays); break; default: fstate.isOn = val; break; } sys.board.valves.syncValveStates(); ncp.pumps.syncPumpStates(); if (!val){ if (fstate.manualPriorityActive) delayMgr.cancelManualPriorityDelay(fstate.id); fstate.manualPriorityActive = false; // if the delay was previously cancelled, still need to turn this off } state.emitEquipmentChanges(); return fstate; } catch (err) { return Promise.reject(new Error(`Error setting feature state ${err.message}`)); } } protected async setSpillwayFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { try { let cstate = state.features.getItemById(id); if (cstate.isOn !== val) { if (sys.equipment.shared === true) { let spastate = state.temps.bodies.getItemById(2); if (val) { if (spastate.isOn || spastate.startDelay) { logger.warn(`Cannot turn ${cstate.name} on because ${spastate.name} is on`); return cstate; } // If there are any drain circuits or features that are currently engaged we need to turn them off. await sys.board.circuits.turnOffDrainCircuits(ignoreDelays); if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 6]); } else if (!val) { let arrIds = sys.board.valves.getBodyValveCircuitIds(true); if (arrIds.length > 1) { if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 6]); } } } logger.verbose(`Turning ${val ? 'on' : 'off'} a spillway feature ${cstate.name}`); cstate.isOn = val; } return cstate; } catch (err) { logger.error(`Nixie: Error setSpillwayFeatureStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setSpillwayFeatureStateAsync ${err.message}`, 'setSpillwayFeatureStateAsync')); } } protected async setDrainFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise { try { // Drain circuits can be very bad. This is because they can be turned on then never turned off // we may want to create some limits are to how long they can be on or even force them off // if for instance the spa is not on. // RULES FOR DRAIN CIRCUITS: // 1. All spillway circuits must be off. let cstate = state.features.getItemById(id); if (cstate.isOn !== val) { if (sys.equipment.shared === true) { if (val) { // First we need to check to see if the pool is on. let poolstate = state.temps.bodies.getItemById(1); let spastate = state.temps.bodies.getItemById(2); if ((spastate.isOn || spastate.startDelay || poolstate.isOn || poolstate.startDelay) && val) { logger.warn(`Cannot turn ${cstate.name} on because a body circuit is on`); return cstate; } // If there are any spillway circuits or features that are currently engaged we need to turn them off. await sys.board.circuits.turnOffSpillwayCircuits(true); // If there are any cleaner circuits on for the main body turn them off. await sys.board.circuits.turnOffCleanerCircuits(state.temps.bodies.getItemById(1), true); if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 1, 6]); } else if (!val) { if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 1, 6]); } } logger.verbose(`Turning ${val ? 'on' : 'off'} a spa drain feature ${cstate.name}`); cstate.isOn = val; } return cstate; } catch (err) { logger.error(`Nixie: Error setDrainFeatureStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setDrainFeatureStateAsync ${err.message}`, 'setDrainFeatureStateAsync')); } } public async toggleFeatureStateAsync(id: number): Promise { let feat = state.features.getItemById(id); return this.setFeatureStateAsync(id, !(feat.isOn || false)); } public syncGroupStates() { // The way this should work is that when all of the states are met // the group should be on. Otherwise it should be off. That means that if // you turned on all the group circuits that should be on individually then // the group should be on. for (let i = 0; i < sys.circuitGroups.length; i++) { let grp: CircuitGroup = sys.circuitGroups.getItemByIndex(i); let circuits = grp.circuits.toArray(); if (grp.isActive) { let bIsOn = true; // Iterate the circuits and break out should we find a condition // where the group should be off. for (let j = 0; j < circuits.length && bIsOn === true; j++) { let circuit: CircuitGroupCircuit = grp.circuits.getItemByIndex(j); let cstate = state.circuits.getInterfaceById(circuit.circuit); // RSG: desiredState for Nixie is 1=on, 2=off, 3=ignore if (circuit.desiredState === 1 || circuit.desiredState === 4) { // The circuit should be on if the value is 1. // If we are on 'ignore' we should still only treat the circuit as // desiredstate = 1. if (!utils.makeBool(cstate.isOn)) bIsOn = false; } else if (circuit.desiredState === 2 || circuit.desiredState === 5) { // The circuit should be off. if (utils.makeBool(cstate.isOn)) bIsOn = false; } } let sgrp = state.circuitGroups.getItemById(grp.id); if (bIsOn && typeof sgrp.endTime === 'undefined') { sys.board.circuits.setEndTime(grp, sgrp, bIsOn, true); } sgrp.isOn = bIsOn; if (!sgrp.isOn && sgrp.manualPriorityActive){ delayMgr.cancelManualPriorityDelays(); sgrp.manualPriorityActive = false; // if the delay was previously cancelled, still need to turn this off } } sys.board.valves.syncValveStates(); } // I am guessing that there will only be one here but iterate // just in case we expand. for (let i = 0; i < sys.lightGroups.length; i++) { let grp: LightGroup = sys.lightGroups.getItemByIndex(i); let circuits = grp.circuits.toArray(); if (grp.isActive) { let bIsOn = true; for (let j = 0; j < circuits.length && bIsOn === true; j++) { let circuit: LightGroupCircuit = grp.circuits.getItemByIndex(j); let cstate = state.circuits.getInterfaceById(circuit.circuit); if (!utils.makeBool(cstate.isOn)) bIsOn = false; } let sgrp = state.lightGroups.getItemById(grp.id); sgrp.isOn = bIsOn; if (sgrp.isOn && typeof sgrp.endTime === 'undefined') sys.board.circuits.setEndTime(grp, sgrp, sgrp.isOn, true); if (!sgrp.isOn && sgrp.manualPriorityActive){ delayMgr.cancelManualPriorityDelay(grp.id); sgrp.manualPriorityActive = false; // if the delay was previously cancelled, still need to turn this off } } sys.board.valves.syncValveStates(); } state.emitEquipmentChanges(); } } export class NixiePumpCommands extends PumpCommands { public async setPumpValveDelays(circuitIds: number[], delay?: number) { try { logger.info(`Setting pump valve delays: ${JSON.stringify(circuitIds)}`); // Alright now we have to delay the pumps associated with the circuit. So lets iterate all our // pump states and see where we land. for (let i = 0; i < sys.pumps.length; i++) { let pump = sys.pumps.getItemByIndex(i); let pstate = state.pumps.getItemById(pump.id); let pt = sys.board.valueMaps.pumpTypes.get(pump.type); // Old - [1, { name: 'ss', desc: 'Single Speed', maxCircuits: 0, hasAddress: false, hasBody: true, maxRelays: 1 }], // New 07/22 - [1, { name: 'ss', desc: 'Single Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 1, relays: [{ id: 1, name: 'Pump On/Off' }]}], // [2, { name: 'ds', desc: 'Two Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 2 }], // [3, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true }], // [4, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }], // [5, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }], // [100, { name: 'sf', desc: 'SuperFlo VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1 }] switch (pt.name) { case 'ss':{ // rsg - ss now has circuit assignments. will check but still leave existing code if (pt.maxCircuits === 0 || typeof pump.body !== 'undefined'){ // If a single speed pump is designated it will be the filter pump but we need to map any settings // to bodies. console.log(`Body: ${pump.body} Pump: ${pump.name} Pool: ${circuitIds.includes(6)} `); if ((pump.body === 255 && (circuitIds.includes(6) || circuitIds.includes(1))) || (pump.body === 0 && circuitIds.includes(6)) || (pump.body === 101 && circuitIds.includes(1))) { delayMgr.setPumpValveDelay(pstate); } break; } } default: if (pt.maxCircuits > 0) { for (let j = 0; j < pump.circuits.length; j++) { let circ = pump.circuits.getItemByIndex(j); if (circuitIds.includes(circ.circuit)) { delayMgr.setPumpValveDelay(pstate); break; } } } break; } } } catch (err) { } } } export class NixieValveCommands extends ValveCommands { public async setValveAsync(obj: any): Promise { try { let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1; obj.master = 1; if (isNaN(id) || id <= 0) id = Math.max(sys.valves.getMaxId(false, 49) + 1, 50); if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Nixie: Valve Id has not been defined ${id}`, obj.id, 'Valve')); // Check the Nixie Control Panel to make sure the valve exist there. If it needs to be added then we should add it. let valve = sys.valves.getItemById(id, true); // Set all the valve properies. let vstate = state.valves.getItemById(valve.id, true); valve.isActive = true; valve.circuit = typeof obj.circuit !== 'undefined' ? obj.circuit : valve.circuit; valve.name = typeof obj.name !== 'undefined' ? obj.name : valve.name; valve.connectionId = typeof obj.connectionId ? obj.connectionId : valve.connectionId; valve.deviceBinding = typeof obj.deviceBinding !== 'undefined' ? obj.deviceBinding : valve.deviceBinding; valve.pinId = typeof obj.pinId !== 'undefined' ? obj.pinId : valve.pinId; await ncp.valves.setValveAsync(valve, obj); await sys.board.syncEquipmentItems(); return valve; } catch (err) { logger.error(`Nixie: Error setting valve definition. ${err.message}`); return Promise.reject(err); } } public async deleteValveAsync(obj: any): Promise { try { let id = parseInt(obj.id, 10); // The following code will make sure we do not encroach on any valves defined by the OCP. if (isNaN(id)) return Promise.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); ncp.valves.removeById(id); return valve; } catch (err) { return Promise.reject(new BoardProcessError(err.message, 'deleteValveAsync')); } } public async setValveStateAsync(valve: Valve, vstate: ValveState, isDiverted: boolean) { try { vstate.name = valve.name; await ncp.valves.setValveStateAsync(vstate, isDiverted); } catch (err) { logger.error(`Nixie: Error setting valve ${vstate.id}-${vstate.name} state to ${isDiverted}: ${err}`); return Promise.reject(err); } } } export class NixieHeaterCommands extends HeaterCommands { public async setHeaterAsync(obj: any): Promise { try { let id = typeof obj.id === 'undefined' || !obj.id ? -1 : parseInt(obj.id, 10); if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater')); else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Nixie Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater')); let heater: Heater; if (id <= 0) { // We are adding a heater. In this case all heaters are virtual. let vheaters = sys.heaters.filter(h => h.master === 1); id = Math.max(vheaters.getMaxId() + 1 || 0, vheaters.length + 256); logger.info(`Adding a new heater with id ${id}`); } heater = sys.heaters.getItemById(id, true); if (typeof obj !== undefined) { for (var s in obj) { if (s === 'id') continue; heater[s] = obj[s]; } } let hstate = state.heaters.getItemById(id, true); //hstate.isVirtual = heater.isVirtual = true; hstate.name = heater.name; hstate.type = heater.type; heater.master = 1; await ncp.heaters.setHeaterAsync(heater, obj); await sys.board.heaters.updateHeaterServices(); return heater; } catch (err) { return Promise.reject(new Error(`Error setting heater configuration: ${err}`)); } } public async deleteHeaterAsync(obj: any): Promise { try { let id = parseInt(obj.id, 10); if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater')); let heater = sys.heaters.getItemById(id); heater.isActive = false; await ncp.heaters.deleteHeaterAsync(id); sys.heaters.removeItemById(id); state.heaters.removeItemById(id); sys.board.heaters.updateHeaterServices(); return heater; } catch (err) { return Promise.reject(new BoardProcessError(err.message, 'deleteHeaterAsync')); } } public updateHeaterServices() { let htypes = sys.board.heaters.getInstalledHeaterTypes(); let solarInstalled = htypes.solar > 0; let heatPumpInstalled = htypes.heatpump > 0; let gasHeaterInstalled = htypes.gas > 0; let ultratempInstalled = htypes.ultratemp > 0; let mastertempInstalled = htypes.mastertemp > 0; let jxiInstalled = (htypes.jxi || 0) > 0 || (htypes.lxi || 0) > 0; let hybridInstalled = htypes.hybrid > 0; // The heat mode options are // 1 = Off // 2 = Gas Heater // 3 = Solar Heater // 4 = Solar Preferred // 5 = UltraTemp Only // 6 = UltraTemp Preferred???? This might be 22 // 9 = Heat Pump // 25 = Heat Pump Preferred // ?? = Hybrid // The heat source options are // 0 = No Change // 1 = Off // 2 = Gas Heater // 3 = Solar Heater // 4 = Solar Preferred // 5 = Heat Pump if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[1, { name: 'off', desc: 'Off' }]]); sys.board.valueMaps.heatModes = new byteValueMap([[1, { name: 'off', desc: 'Off' }]]); if (hybridInstalled) { sys.board.valueMaps.heatModes.merge([ [7, { name: 'hybheat', desc: 'Gas Only' }], [8, { name: 'hybheatpump', desc: 'Heat Pump Only' }], [9, { name: 'hybhybrid', desc: 'Hybrid' }], [10, { name: 'hybdual', desc: 'Dual Heat' }] ]); sys.board.valueMaps.heatSources.merge([ [7, { name: 'hybheat', desc: 'Gas Only' }], [8, { name: 'hybheatpump', desc: 'Heat Pump Only' }], [9, { name: 'hybhybrid', desc: 'Hybrid' }], [10, { name: 'hybdual', desc: 'Dual Heat' }] ]); // RKS: 08-24-22 The heat modes and sources for the hybrid heater are unique. Turns out that // these should be available if there is a gas heater ganged to the body as well. // types cannot be ignored since they are specific to the heater. //sys.board.valueMaps.heatModes.merge([ // [9, { name: 'heatpump', desc: 'Heat Pump' }], // [2, { name: 'heater', desc: 'Heater' }], // [25, { name: 'heatpumppref', desc: 'Hybrid' }], // [26, { name: 'dual', desc: 'Dual Heat' }] //]); //sys.board.valueMaps.heatSources.merge([ // [2, { name: 'heater', desc: 'Gas Heat' }], // [9, { name: 'heatpump', desc: 'Heat Pump' }], // [20, { name: 'heatpumppref', desc: 'Hybrid' }], // [21, { name: 'dual', desc: 'Dual Heat' }] //]); } if (gasHeaterInstalled || jxiInstalled) sys.board.valueMaps.heatSources.merge([[2, { name: 'heater', desc: 'Heater' }]]); if (mastertempInstalled) sys.board.valueMaps.heatSources.merge([[11, { name: 'mtheater', desc: 'MasterTemp' }]]); if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar Only', hasCoolSetpoint: htypes.hasCoolSetpoint }], [4, { name: 'solarpref', desc: 'Solar Preferred', hasCoolSetpoint: htypes.hasCoolSetpoint }]]); else if (solarInstalled) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar', hasCoolSetpoint: htypes.hasCoolSetpoint }]]); if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Pref' }]]); else if (heatPumpInstalled) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heat Pump' }]]); if (ultratempInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[5, { name: 'ultratemp', desc: 'UltraTemp Only', hasCoolSetpoint: htypes.hasCoolSetpoint }], [6, { name: 'ultratemppref', desc: 'UltraTemp Pref', hasCoolSetpoint: htypes.hasCoolSetpoint }]]); else if (ultratempInstalled) sys.board.valueMaps.heatSources.merge([[5, { name: 'ultratemp', desc: 'UltraTemp', hasCoolSetpoint: htypes.hasCoolSetpoint }]]); sys.board.valueMaps.heatSources.merge([[0, { name: 'nochange', desc: 'No Change' }]]); if (gasHeaterInstalled || jxiInstalled) sys.board.valueMaps.heatModes.merge([[2, { name: 'heater', desc: 'Heater' }]]); if (mastertempInstalled) sys.board.valueMaps.heatModes.merge([[11, { name: 'mtheater', desc: 'MasterTemp' }]]); if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar Only' }], [4, { name: 'solarpref', desc: 'Solar Preferred' }]]); else if (solarInstalled) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar' }]]); if (ultratempInstalled && (gasHeaterInstalled || heatPumpInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp Only' }], [6, { name: 'ultratemppref', desc: 'UltraTemp Pref' }]]); else if (ultratempInstalled) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp' }]]); if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]); else if (heatPumpInstalled) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heat Pump' }]]); // Now set the body data. for (let i = 0; i < sys.bodies.length; i++) { let body = sys.bodies.getItemByIndex(i); let btemp = state.temps.bodies.getItemById(body.id, body.isActive !== false); let opts = sys.board.heaters.getInstalledHeaterTypes(body.id); btemp.heaterOptions = opts; } this.setActiveTempSensors(); } } export class NixieChemControllerCommands extends ChemControllerCommands { protected async setIntelliChemAsync(data: any): Promise { try { // Nixie is always in control so let her do her thing. let chem = sys.chemControllers.getItemById(data.id, true); await ncp.chemControllers.setControllerAsync(chem, data); return chem; } catch (err) { return Promise.reject(err); } } public async deleteChemControllerAsync(data: any): Promise { try { let id = typeof data.id !== 'undefined' ? parseInt(data.id, 10) : -1; if (typeof id === 'undefined' || isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid Chem Controller Id`, id, 'chemController')); let chem = sys.chemControllers.getItemById(id); let schem = state.chemControllers.getItemById(chem.id); await ncp.chemControllers.removeById(chem.id); chem.isActive = schem.isActive = false; sys.chemControllers.removeItemById(chem.id); state.chemControllers.removeItemById(chem.id); schem.emitEquipmentChange(); return chem; } catch (err) { return Promise.reject(err); } } }