/* 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 { byteValueMap, EquipmentIdRange, } from './SystemBoard'; import { logger } from '../../logger/Logger'; import { EasyTouchBoard, TouchConfigQueue, GetTouchConfigCategories, TouchCircuitCommands } from './EasyTouchBoard'; import { state, ICircuitGroupState } from '../State'; import { PoolSystem, sys, ExpansionPanel, Equipment } from '../Equipment'; import { conn } from '../comms/Comms'; import { InvalidEquipmentDataError } from '../Errors'; import { log } from 'winston'; export class IntelliTouchBoard extends EasyTouchBoard { constructor(system: PoolSystem) { super(system); // Circuits even in single body IntelliTouch always start at 1. this.equipmentIds.circuits.start = 1; this.equipmentIds.features.start = 41; this.equipmentIds.features.end = 50; this.equipmentIds.virtualCircuits = new EquipmentIdRange(154, 162); this._configQueue = new ITTouchConfigQueue(); this.valueMaps.expansionBoards = new byteValueMap([ [0, { name: 'IT5', part: 'i5+3', desc: 'IntelliTouch i5+3', circuits: 6, shared: true }], [1, { name: 'IT7', part: 'i7+3', desc: 'IntelliTouch i7+3', circuits: 8, shared: true }], [2, { name: 'IT9', part: 'i9+3', desc: 'IntelliTouch i9+3', circuits: 10, shared: true }], [3, { name: 'IT5S', part: 'i5+3S', desc: 'IntelliTouch i5+3S', circuits: 5, single: true, shared: true, bodies: 2, intakeReturnValves: false }], [4, { name: 'IT9S', part: 'i9+3S', desc: 'IntelliTouch i9+3S', circuits: 9, single: true, shared: true, bodies: 2, intakeReturnValves: false }], [5, { name: 'IT10D', part: 'i10D', desc: 'IntelliTouch i10D', circuits: 10, single: true, shared: false, dual: true }], [32, { name: 'IT5X', part: 'i5X', desc: 'IntelliTouch i5X', circuits: 5 }], [33, { name: 'IT10X', part: 'i10X', desc: 'IntelliTouch i10X', circuits: 10 }], [64, { name: 'Valve Exp', part: '520285', desc: 'Valve Expansion Module', valves: 3 }] ]); // I don't think these have ever been right. // So far I have seen below based on discussion #436 // 155 = heater // 156 = poolheater // 157 = spaheater this.valueMaps.virtualCircuits = new byteValueMap([ [154, { name: 'freeze', desc: 'Freeze', assignableToPumpCircuit: true }], [155, { name: 'spaHeater', desc: 'Spa Heater', assignableToPumpCircuit: true }], [156, { name: 'poolHeater', desc: 'Pool Heater', assignableToPumpCircuit: true }], [157, { name: 'heater', desc: 'Either Heater', assignableToPumpCircuit: true }], [158, { name: 'solar', desc: 'Solar', assignableToPumpCircuit: true }], [159, { name: 'heatBoost', desc: 'Heat Boost', assignableToPumpCircuit: false }], [160, { name: 'heatEnable', desc: 'Heat Enable', assignableToPumpCircuit: false }], [161, { name: 'pumpSpeedUp', desc: 'Pump Speed +', assignableToPumpCircuit: false }], [162, { name: 'pumpSpeedDown', desc: 'Pump Speed -', assignableToPumpCircuit: false }], [255, { name: 'notused', desc: 'NOT USED', assignableToPumpCircuit: true }], [258, { name: 'anyHeater', desc: 'Any Heater' }] ]); } public initVirtualCircuits() { for (let i = state.virtualCircuits.length - 1; i >= 0; i--) { let vc = state.virtualCircuits.getItemByIndex(i); if (vc.id > this.equipmentIds.virtualCircuits.end || vc.id < this.equipmentIds.virtualCircuits.start) state.virtualCircuits.removeItemByIndex(i); } // Now that we removed all the virtual circuits that should not be there we need // to update them based upon the data. } public initValves(eq) { if (typeof sys.valves.find((v) => v.id === 1) === 'undefined') { let valve = sys.valves.getItemById(1, true); valve.isIntake = false; valve.isReturn = false; valve.type = 0; valve.master = 0; valve.isActive = true; valve.name = 'Valve A'; logger.info(`Initializing IntelliTouch Valve A`); } if (typeof sys.valves.find((v) => v.id === 2) === 'undefined') { let valve = sys.valves.getItemById(2, true); valve.isIntake = false; valve.isReturn = false; valve.type = 0; valve.master = 0; valve.isActive = true; valve.name = 'Valve B'; logger.info(`Initializing IntelliTouch Valve B`); } if (eq.intakeReturnValves) { logger.info(`Initializing IntelliTouch Intake/Return Valves`); let valve = sys.valves.getItemById(3, true); valve.isIntake = true; valve.isReturn = false; valve.circuit = 6; valve.type = 0; valve.master = 0; valve.isActive = true; valve.name = 'Intake'; valve = sys.valves.getItemById(4, true); valve.isIntake = false; valve.isReturn = true; valve.circuit = 6; valve.type = 0; valve.master = 0; valve.isActive = true; valve.name = 'Return'; } } public initExpansionModules(byte1: number, byte2: number) { console.log(`Pentair IntelliTouch System Detected!`); // For i9+3S with valve expansion the bytes are 4, 32 the expectation is // that the 32 contains the indicator that there is a valve expansion module. // Initialize the installed personality board. let mt = this.valueMaps.expansionBoards.transform(byte1); let mod = sys.equipment.modules.getItemById(0, true); mod.name = mt.name; mod.desc = mt.desc; mod.type = byte1; mod.part = mt.part; let eq = sys.equipment; let md = mod.get(); //eq.maxBodies = md.bodies = typeof mt.bodies !== 'undefined' ? mt.bodies : mt.shared || mt.dual ? 2 : 1; // There are always 2 bodies on an IntelliTouch even the single body models. eq.maxBodies = 2; eq.maxCircuits = md.circuits = typeof mt.circuits !== 'undefined' ? mt.circuits : 6; eq.maxFeatures = md.features = typeof mt.features !== 'undefined' ? mt.features : 8; eq.maxValves = md.valves = typeof mt.valves !== 'undefined' ? mt.valves : mt.shared ? 4 : 2; eq.maxPumps = md.maxPumps = typeof mt.pumps !== 'undefined' ? mt.pumps : 8; eq.shared = mt.shared; eq.dual = typeof mt.dual !== 'undefined' ? mt.dual : false; eq.single = typeof mt.single !== 'undefined' ? mt.single : false; eq.intakeReturnValves = eq.single ? false : true; eq.maxChlorinators = md.chlorinators = 1; eq.maxChemControllers = md.chemControllers = 1; eq.maxCustomNames = 20; eq.maxSchedules = 99; eq.maxCircuitGroups = 10; // Not sure why this is 10 other than to allow for those that we are in control of. if (eq.single) { // Replace the body types with Hi-Temp and Lo-Temp sys.board.valueMaps.bodyTypes.merge([[0, { name: 'pool', desc: 'Lo-Temp' }], [1, { name: 'spa', desc: 'Hi-Temp' }]]); } // Calculate out the invalid ids. // Add in all the invalid ids from the base personality board. sys.board.equipmentIds.invalidIds.set([16, 17, 18]); // These appear to alway be invalid in IntelliTouch. for (let i = 7; i <= 10; i++) { // This will add all the invalid ids between 7 and 10 that are omitted for IntelliTouch models. // For instance an i7+3 can have up to 8 circuits since 1 and 6 are shared but an i7+3S will only have 7. if (i > eq.maxCircuits) sys.board.equipmentIds.invalidIds.merge([i]); } // This code should be repeated if we ever see a panel with more than one expansion panel. let pnl1: ExpansionPanel; if ((byte2 & 0x40) === 64) { // 64 indicates one expansion panel; SL defaults to i10x but it could also be i5x until we know better pnl1 = sys.equipment.expansions.getItemById(1, true); pnl1.type = 32; let emt = this.valueMaps.expansionBoards.transform(pnl1.type); pnl1.name = emt.desc; pnl1.isActive = true; eq.maxCircuits += emt.circuits; } else sys.equipment.expansions.removeItemById(1); let pnl2: ExpansionPanel; if ((byte2 & 0x80) === 128) { // SL defaults to i5x but it could also be i10x until we know better pnl2 = sys.equipment.expansions.getItemById(2, true); pnl2.type = 32; let emt = this.valueMaps.expansionBoards.transform(pnl2.type); pnl2.name = emt.desc; pnl2.isActive = true; eq.maxCircuits += emt.circuits; } else sys.equipment.expansions.removeItemById(2); let pnl3: ExpansionPanel; if ((byte2 & 0xC0) === 192) { // SL defaults to i5x but it could also be i10x until we know better pnl3 = sys.equipment.expansions.getItemById(3, true); pnl3.type = 32; let emt = this.valueMaps.expansionBoards.transform(pnl3.type); pnl3.name = emt.desc; pnl3.isActive = true; eq.maxCircuits += emt.circuits; } else sys.equipment.expansions.removeItemById(3); // Detect the valve expansion module. if ((byte2 & 0x20) === 20) { // The valve expansion module is installed so this should add 3 valves. eq.maxValves += 3; let vexp = eq.modules.getItemById(1, true); vexp.isActive = true; let mt = this.valueMaps.expansionBoards.transform(64); vexp.name = mt.name; vexp.desc = mt.desc; vexp.type = byte1; vexp.part = mt.part; } else eq.modules.removeItemById(1); if (byte1 !== 14) sys.board.equipmentIds.invalidIds.merge([10, 19]); state.equipment.model = sys.equipment.model = mt.desc; state.equipment.controllerType = 'intellitouch'; this.initBodyDefaults(); this.initHeaterDefaults(); (async () => { try { sys.board.bodies.initFilters(); } catch (err) { logger.error(`Error initializing IntelliTouch Filters`); } })(); for (let i = 0; i < sys.circuits.length; i++) { let c = sys.circuits.getItemByIndex(i); if (c.id <= 40) c.master = 0; } for (let i = 0; i < sys.valves.length; i++) { let v = sys.valves.getItemByIndex(i); if (v.id < 50) v.master = 0; } // Clean up any schedules that shouldn't be there. for (let i = sys.schedules.length - 1; i > 0; i--) { let sched = sys.schedules.getItemByIndex(i); if (sched.id < 1) { sys.schedules.removeItemByIndex(i); state.schedules.removeItemById(sched.id); } if (sched.id < eq.maxSchedules) sched.master = 0; else if (sched.id > eq.maxSchedules && (sched.master === 0 || typeof sched.master === 'undefined')) { sys.schedules.removeItemByIndex(i); state.schedules.removeItemById(sched.id); } } eq.setEquipmentIds(); this.initVirtualCircuits(); this.initValves(eq); state.equipment.maxBodies = sys.equipment.maxBodies; state.equipment.maxCircuitGroups = sys.equipment.maxCircuitGroups; state.equipment.maxCircuits = sys.equipment.maxCircuits; state.equipment.maxFeatures = sys.equipment.maxFeatures; state.equipment.maxHeaters = sys.equipment.maxHeaters; state.equipment.maxLightGroups = sys.equipment.maxLightGroups; state.equipment.maxPumps = sys.equipment.maxPumps; state.equipment.maxSchedules = sys.equipment.maxSchedules; state.equipment.maxValves = sys.equipment.maxValves; state.equipment.single = sys.equipment.single; state.equipment.shared = sys.equipment.shared; state.equipment.dual = sys.equipment.dual; state.emitControllerChange(); } public circuits: ITTouchCircuitCommands = new ITTouchCircuitCommands(this); public async setControllerType(obj): Promise { try { if (obj.controllerType !== sys.controllerType) { return Promise.reject(new InvalidEquipmentDataError(`You may not change the controller type data for ${sys.controllerType} controllers`, 'controllerType', obj.controllerType)); } let mod = sys.equipment.modules.getItemById(0); let mt = this.valueMaps.expansionBoards.get(mod.type); let _circuits = mt.circuits; let pnl1 = sys.equipment.expansions.getItemById(1); if (typeof obj.expansion1 !== 'undefined' && obj.expansion1 !== pnl1.type) { let emt = this.valueMaps.expansionBoards.transform(obj.expansion1); logger.info(`Changing expansion 1 to ${emt.desc}.`); pnl1.type = emt.val; pnl1.name = emt.desc; pnl1.isActive = true; } let pnl2 = sys.equipment.expansions.getItemById(2); if (typeof obj.expansion2 !== 'undefined' && obj.expansion2 !== pnl2.type) { let emt = this.valueMaps.expansionBoards.transform(obj.expansion2); logger.info(`Changing expansion 2 to ${emt.desc}.`); pnl2.type = emt.val; pnl2.name = emt.desc; pnl2.isActive = true; } let pnl3 = sys.equipment.expansions.getItemById(3); if (typeof obj.expansion3 !== 'undefined' && obj.expansion3 !== pnl3.type) { let emt = this.valueMaps.expansionBoards.transform(obj.expansion3); logger.info(`Changing expansion 3 to ${emt.desc}.`); pnl3.type = emt.val; pnl3.name = emt.desc; pnl3.isActive = true; } let prevMaxCircuits = sys.equipment.maxCircuits; if (pnl1.isActive) _circuits += this.valueMaps.expansionBoards.get(pnl1.type).circuits; if (pnl2.isActive) _circuits += this.valueMaps.expansionBoards.get(pnl2.type).circuits; if (pnl3.isActive) _circuits += this.valueMaps.expansionBoards.get(pnl3.type).circuits; if (_circuits < prevMaxCircuits) { // if we downsize expansions, remove circuits for (let i = _circuits + 1; i <= prevMaxCircuits; i++) { sys.circuits.removeItemById(i); state.circuits.removeItemById(i); } } else if (_circuits > prevMaxCircuits) { this._configQueue.queueChanges(); } sys.equipment.maxCircuits = _circuits; return sys.equipment; } catch (err) { logger.error(`Error setting expansion panels: ${err.message}`); } } public initBodyDefaults() { // Initialize the bodies. We will need these very soon. for (let i = 1; i <= sys.equipment.maxBodies; i++) { // Add in the bodies for the configuration. These need to be set. let cbody = sys.bodies.getItemById(i, true); let tbody = state.temps.bodies.getItemById(i, true); cbody.isActive = true; // If the body doesn't represent a spa then we set the type. tbody.type = cbody.type = i - 1; // This will set the first body to pool/Lo-Temp and the second body to spa/Hi-Temp. if (typeof cbody.name === 'undefined') { let bt = sys.board.valueMaps.bodyTypes.transform(cbody.type); tbody.name = cbody.name = bt.desc; } } sys.bodies.removeItemById(3); sys.bodies.removeItemById(4); state.temps.bodies.removeItemById(3); state.temps.bodies.removeItemById(4); for (let i = 0; i < sys.bodies.length; i++) { let b = sys.bodies.getItemByIndex(i); b.master = 0; } sys.board.heaters.initTempSensors(); sys.general.options.clockMode = sys.general.options.clockMode || 12; sys.general.options.clockSource = sys.general.options.clockSource || 'manual'; } } class ITTouchConfigQueue extends TouchConfigQueue { public queueChanges() { this.reset(); logger.info(`Requesting ${sys.controllerType} configuration`); this.queueItems(GetTouchConfigCategories.dateTime, [0]); this.queueItems(GetTouchConfigCategories.heatTemperature, [0]); this.queueItems(GetTouchConfigCategories.solarHeatPump, [0]); this.queueRange(GetTouchConfigCategories.customNames, 0, sys.equipment.maxCustomNames - 1); this.queueRange(GetTouchConfigCategories.circuits, 1, sys.equipment.maxCircuits); // circuits this.queueRange(GetTouchConfigCategories.circuits, 41, 41 + sys.equipment.maxFeatures); // features this.queueRange(GetTouchConfigCategories.schedules, 1, sys.equipment.maxSchedules); this.queueItems(GetTouchConfigCategories.delays, [0]); this.queueItems(GetTouchConfigCategories.settings, [0]); this.queueItems(GetTouchConfigCategories.intellifloSpaSideRemotes, [0]); this.queueItems(GetTouchConfigCategories.is4is10, [0]); this.queueItems(GetTouchConfigCategories.quickTouchRemote, [0]); this.queueItems(GetTouchConfigCategories.valves, [0]); this.queueItems(GetTouchConfigCategories.lightGroupPositions); this.queueItems(GetTouchConfigCategories.highSpeedCircuits, [0]); this.queueRange(GetTouchConfigCategories.pumpConfig, 1, sys.equipment.maxPumps); this.queueRange(GetTouchConfigCategories.circuitGroups, 0, sys.equipment.maxFeatures - 1); this.queueItems(GetTouchConfigCategories.intellichlor, [0]); if (this.remainingItems > 0) { var self = this; setTimeout(() => { self.processNext(); }, 50); } else state.status = 1; state.emitControllerChange(); } } class ITTouchCircuitCommands extends TouchCircuitCommands { public async setCircuitGroupStateAsync(id: number, val: boolean): Promise { // intellitouch supports groups/macros with id's 41-50 with a macro flag let grp = sys.circuitGroups.getItemById(id, false, { isActive: false }); return new Promise(async (resolve, reject) => { try { await sys.board.circuits.setCircuitStateAsync(id, val); resolve(state.circuitGroups.getInterfaceById(id)); } catch (err) { reject(err); } }); } }