/* 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 path from "path";
import * as fs from "fs";
import * as extend from "extend";
import * as util from "util";
import { setTimeout } from "timers";
import { logger } from "../logger/Logger";
import { state, CommsState, ChemicalChlorState } from "./State";
import { Timestamp, ControllerType, utils } from "./Constants";
export { ControllerType };
import { webApp } from "../web/Server";
import { SystemBoard, EquipmentIdRange } from "./boards/SystemBoard";
import { BoardFactory } from "./boards/BoardFactory";
import { EquipmentStateMessage } from "./comms/messages/status/EquipmentStateMessage";
import { conn } from './comms/Comms';
import { versionCheck } from "../config/VersionCheck";
import { getStateForZip } from "./zipCoords";
import { NixieControlPanel } from "./nixie/Nixie";
import { NixieBoard } from 'controller/boards/NixieBoard';
import { MockSystemBoard } from "../anslq25/boards/MockSystemBoard";
import { MockBoardFactory } from "../anslq25/boards/MockBoardFactory";
import { ScreenLogicComms } from "./comms/ScreenLogic";
import { VirtualEquipmentManager, virtualEquipmentManager } from "./virtualEquipment/VirtualEquipmentManager";
interface IPoolSystem {
cfgPath: string;
data: any;
stopAsync(): void;
persist(): void;
anslq25: Anslq25;
general: General;
equipment: Equipment;
configVersion: ConfigVersion;
bodies: BodyCollection;
schedules: ScheduleCollection;
circuits: CircuitCollection;
features: FeatureCollection;
pumps: PumpCollection;
chlorinators: ChlorinatorCollection;
filters: FilterCollection;
valves: ValveCollection;
heaters: HeaterCollection;
covers: CoverCollection;
circuitGroups: CircuitGroupCollection;
remotes: RemoteCollection;
eggTimers: EggTimerCollection;
security: Security;
alerts: Alerts;
chemControllers: ChemControllerCollection;
board: SystemBoard;
updateControllerDateTimeAsync(
hour: number,
min: number,
date: number,
month: number,
year: number,
dst: number,
dow?: number
): void;
}
export class PoolSystem implements IPoolSystem {
public _hasChanged: boolean = false;
public isReady: boolean = false;
private _startOCPTimer: NodeJS.Timeout;
constructor() {
this.cfgPath = path.posix.join(process.cwd(), '/data/poolConfig.json');
}
public getAvailableControllerTypes(include:string[] = ['easytouch', 'intellitouch', 'intellicenter', 'suntouch', 'nixie']) {
let arr = [];
if (include.indexOf('easytouch')>=0) arr.push({
type: 'easytouch', name: 'EasyTouch',
models: [
{ val: 0, name: 'ET28', part: 'ET2-8', desc: 'EasyTouch2 8', circuits: 8, bodies: 2, shared: true },
{ val: 1, name: 'ET28P', part: 'ET2-8P', desc: 'EasyTouch2 8P', circuits: 8, bodies: 1, shared: false },
{ val: 2, name: 'ET24', part: 'ET2-4', desc: 'EasyTouch2 4', circuits: 4, bodies: 1, shared: false },
{ val: 3, name: 'ET24P', part: 'ET2-4P', desc: 'EasyTouch2 4P', circuits: 4, bodies: 2, shared: true },
{ val: 6, name: 'ETPSL4', part: 'ET-PSL4', desc: 'EasyTouch PSL4', circuits: 4, bodies: 2, features: 2, schedules: 4, pumps: 1, shared: true },
{ val: 7, name: 'ETPL4', part: 'ET-PL4', desc: 'EasyTouch PL4', circuits: 4, features: 2, bodies: 1, schedules: 4, pumps: 1, shared: false },
// EasyTouch 1 models all start at 128.
{ val: 128, name: 'ET8', part: 'ET-8', desc: 'EasyTouch 8', circuits: 8, bodies: 2, shared: true },
{ val: 129, name: 'ET8P', part: 'ET-8P', desc: 'EasyTouch 8', circuits: 8, bodies: 1, shared: false },
{ val: 130, name: 'ET4', part: 'ET-4', desc: 'EasyTouch 4', circuits: 4, bodies: 2, shared: true },
{ val: 131, name: 'ET4P', part: 'ET-4P', desc: 'EasyTouch 4P', circuits: 4, bodies: 1, shared: false }
]
});
if (include.indexOf('intellitouch')>=0) arr.push({
type: 'intellitouch', name: 'IntelliTouch',
models: [
{ val: 0, name: 'IT5', part: 'i5+3', desc: 'IntelliTouch i5+3', bodies: 2, circuits: 6, shared: true },
{ val: 1, name: 'IT7', part: 'i7+3', desc: 'IntelliTouch i7+3', bodies: 2, circuits: 7, shared: true },
{ val: 2, name: 'IT9', part: 'i9+3', desc: 'IntelliTouch i9+3', bodies: 2, circuits: 9, shared: true },
{ val: 3, name: 'IT5S', part: 'i5+3S', desc: 'IntelliTouch i5+3S', bodies: 1, circuits: 6, shared: false },
{ val: 4, name: 'IT9S', part: 'i9+3S', desc: 'IntelliTouch i9+3S', bodies: 1, circuits: 9, shared: false },
{ val: 5, name: 'IT10D', part: 'i10D', desc: 'IntelliTouch i10D', bodies: 1, circuits: 10, shared: false, dual: true }
],
expansionModules: [
{ val: 32, name: 'IT5X', part: 'i5X', desc: 'IntelliTouch i5X', circuits: 5, shared: false },
{ val: 33, name: 'IT10X', part: 'i10X', desc: 'IntelliTouch i10X', circuits: 10, shared: false }
]
});
if (include.indexOf('intellicenter')>=0) arr.push({
type: 'intellicenter', name: 'IntelliCenter',
models: [
{ val: 0, name: 'i5P', part: '523125Z', desc: 'IntelliCenter i5P', bodies: 1, valves: 2, circuits: 5, shared: false, dual: false, chlorinators: 1, chemControllers: 1 },
{ val: 1, name: 'i5PS', part: '521936Z', desc: 'IntelliCenter i5PS', bodies: 2, valves: 4, circuits: 6, shared: true, dual: false, chlorinators: 1, chemControllers: 1 },
{ val: 2, name: 'i8P', part: '521977Z', desc: 'IntelliCenter i8P', bodies: 1, valves: 2, circuits: 8, shared: false, dual: false, chlorinators: 1, chemControllers: 1 },
{ val: 3, name: 'i8PS', part: '521968Z', desc: 'IntelliCenter i8PS', bodies: 2, valves: 4, circuits: 9, shared: true, dual: false, chlorinators: 1, chemControllers: 1 },
{ val: 4, name: 'i10P', part: '521993Z', desc: 'IntelliCenter i10P', bodies: 1, valves: 2, circuits: 10, shared: false, dual: false, chlorinators: 1, chemControllers: 1 }, // This is a guess
{ val: 5, name: 'i10PS', part: '521873Z', desc: 'IntelliCenter i10PS', bodies: 2, valves: 4, circuits: 11, shared: true, dual: false, chlorinators: 1, chemControllers: 1 },
{ val: 7, name: 'i10D', part: '523029Z', desc: 'IntelliCenter i10D', bodies: 2, valves: 2, circuits: 11, shared: false, dual: true, chlorinators: 2, chemControllers: 2 },
]
});
if (include.indexOf('suntouch') >= 0) arr.push({
type: 'suntouch', name: 'SunTouch',
models: [
{ val: 41, name: 'stshared', part: '520820', desc: 'Pool and Spa controller', bodies: 2, valves: 4, circuits: 5, single: false, shared: true, dual: false, features: 4, chlorinators: 1, chemControllers: 1 },
{ val: 40, name: 'stsingle', part: '520819', desc: 'Pool or Spa controller', bodies: 2, valves: 4, circuits: 5, single: true, shared: true, dual: false, features: 4, chlorinators: 1, chemControllers: 1 }
]
})
if (include.indexOf('nixie') >= 0) arr.push({
type: 'nixie', name: 'Nixie', canChange: true,
models: [
{ val: 0, name: 'nxp', part: 'NXP', desc: 'Nixie Single Body', bodies: 1, shared: false, dual: false },
{ val: 1, name: 'nxps', part: 'NXPS', desc: 'Nixie Shared Body', bodies: 2, shared: true, dual: false },
{ val: 2, name: 'nxpd', part: 'NXPD', desc: 'Nixe Dual Body', bodies: 2, shared: false, dual: true },
{ val: 255, name: 'nxnb', part: 'NXNB', desc: 'Nixie No Body', bodies: 0, shared: false, dual: false }
]
});
return arr;
}
public async start() {
let self = this;
this.data.appVersion = state.appVersion.installed = this.appVersion = JSON.parse(fs.readFileSync(path.posix.join(process.cwd(), '/package.json'), 'utf8')).version;
versionCheck.compare(); // if we installed a new version, reset the flag so we don't show an outdated message for up to 2 days
logger.info(`Starting Pool System ${this.controllerType}`);
if (this.controllerType === 'unknown' || typeof this.controllerType === 'undefined') {
// Delay for 7.5 seconds to give any OCPs a chance to start emitting messages.
logger.info(`Listening for any installed OCPs`);
if (this._startOCPTimer) clearTimeout(this._startOCPTimer);
this._startOCPTimer = null;
this._startOCPTimer = setTimeout(() => { self.initNixieController(); }, 7500);
}
else
this.initNixieController();
this.isReady = true;
}
public init() {
let cfg = this.loadConfigFile(this.cfgPath, {});
let cfgDefault = this.loadConfigFile(path.posix.join(process.cwd(), '/defaultPool.json'), {});
cfg = extend(true, {}, cfgDefault, cfg);
// First lets remove all the null ids.
EqItemCollection.removeNullIds(cfg.bodies);
EqItemCollection.removeNullIds(cfg.schedules);
EqItemCollection.removeNullIds(cfg.features);
EqItemCollection.removeNullIds(cfg.circuits);
EqItemCollection.removeNullIds(cfg.pumps);
EqItemCollection.removeNullIds(cfg.chlorinators);
EqItemCollection.removeNullIds(cfg.valves);
EqItemCollection.removeNullIds(cfg.heaters);
EqItemCollection.removeNullIds(cfg.covers);
EqItemCollection.removeNullIds(cfg.circuitGroups);
EqItemCollection.removeNullIds(cfg.lightGroups);
EqItemCollection.removeNullIds(cfg.remotes);
EqItemCollection.removeNullIds(cfg.chemControllers);
EqItemCollection.removeNullIds(cfg.chemDosers);
EqItemCollection.removeNullIds(cfg.filters);
if (typeof cfg.pumps !== 'undefined') {
for (let i = 0; i < cfg.pumps.length; i++) {
EqItemCollection.removeNullIds(cfg.pumps[i].circuits);
}
}
this.data = this.onchange(cfg, function () { sys.dirty = true; });
this.anslq25 = new Anslq25(this.data, 'anslq25');
this.general = new General(this.data, 'pool');
this.equipment = new Equipment(this.data, 'equipment');
this.configVersion = new ConfigVersion(this.data, 'configVersion');
this.bodies = new BodyCollection(this.data, 'bodies');
this.schedules = new ScheduleCollection(this.data, 'schedules');
this.circuits = new CircuitCollection(this.data, 'circuits');
this.features = new FeatureCollection(this.data, 'features');
this.pumps = new PumpCollection(this.data, 'pumps');
this.chlorinators = new ChlorinatorCollection(this.data, 'chlorinators');
this.valves = new ValveCollection(this.data, 'valves');
this.heaters = new HeaterCollection(this.data, 'heaters');
this.covers = new CoverCollection(this.data, 'covers');
this.circuitGroups = new CircuitGroupCollection(this.data, 'circuitGroups');
this.lightGroups = new LightGroupCollection(this.data, 'lightGroups');
this.remotes = new RemoteCollection(this.data, 'remotes');
this.security = new Security(this.data, 'security');
this.alerts = new Alerts(this.data, 'alerts');
this.customNames = new CustomNameCollection(this.data, 'customNames');
this.eggTimers = new EggTimerCollection(this.data, 'eggTimers');
this.chemControllers = new ChemControllerCollection(this.data, 'chemControllers');
this.chemDosers = new ChemDoserCollection(this.data, 'chemDosers');
this.filters = new FilterCollection(this.data, 'filters');
this.board = BoardFactory.fromControllerType(this.controllerType, this);
this.anslq25Board = MockBoardFactory.fromControllerType(this.data.anslq25.controllerType, this);
this.virtualEquipment = virtualEquipmentManager;
// Fire and forget: missing/malformed file is non-fatal and only logs a warning.
this.virtualEquipment.loadAsync().catch(() => { /* logged inside */ });
}
// This performs a safe load of the config file. If the file gets corrupt or actually does not exist
// it will not break the overall system and allow hardened recovery.
public async updateControllerDateTimeAsync(obj: any) { return sys.board.system.setDateTimeAsync(obj); }
private loadConfigFile(path: string, def: any) {
let cfg = def;
if (fs.existsSync(path)) {
try {
cfg = JSON.parse(fs.readFileSync(path, 'utf8') || '{}');
}
catch (ex) {
cfg = def;
}
}
return cfg;
}
public async resetSystemAsync(): Promise {
logger.info(`Resetting System to initial defaults`);
await this.board.closeAsync();
logger.info(`Closed ${this.controllerType} board`);
// Use the controllerType setter so we also clear sys/state and rebuild boards appropriately.
this.controllerType = ControllerType.Unknown;
}
public resetSystem() {
// Backwards-compatible fire-and-forget wrapper.
this.resetSystemAsync().catch((err) => logger.error(`resetSystemAsync failed: ${err?.message || err}`));
/*
let self = this;
if (this.controllerType === ControllerType.Nixie) {
(async () => {
await this.board.closeAsync();
this.board = BoardFactory.fromControllerType(ControllerType.Nixie, this);
let board = sys.board as NixieBoard;
await board.initNixieBoard();
})();
state.status = 0;
logger.info(`Listening for any installed OCPs`);
setTimeout(() => { self.initNixieController(); }, 7500);
}
else {
conn.pauseAll();
this.resetData();
state.resetData();
this.data.controllerType === 'unknown';
state.equipment.controllerType = ControllerType.Unknown;
this.controllerType = ControllerType.Unknown;
state.status = 0;
this.board = BoardFactory.fromControllerType(ControllerType.Unknown, this);
setTimeout(function () { state.status = 0; conn.resumeAll(); }, 0);
}
*/
}
public get anslq25ControllerType(): ControllerType { return this.data.anslq25.controllerType as ControllerType; }
public set anslq25ControllerType(val: ControllerType) {
//let self = this;
if (this.anslq25ControllerType !== val) {
console.log(`Mock Controller type changed from ${this.anslq25.controllerType} to ${val}`);
// Only go in here if there is a change to the controller type.
//this.resetData(); // Clear the configuration data.
//state.resetData(); // Clear the state data.
this.data.anslq25.controllerType = val;
//EquipmentStateMessage.initDefaults();
// We are actually changing the config so lets clear out all the data.
this.anslq25Board = MockBoardFactory.fromControllerType(val, this);
//if (this.data.anslq25.controllerType === ControllerType.Unknown) setTimeout(() => { self.initNixieController(); }, 7500);
}
}
public get controllerType(): ControllerType { return this.data.controllerType as ControllerType; }
public set controllerType(val: ControllerType) {
let self = this;
if (this.controllerType !== val) {
console.log(`RESETTING DATA -- Controller type changed from ${this.controllerType} to ${val}`);
// Only go in here if there is a change to the controller type.
state.equipment.messages.clearAll();
this.resetData(); // Clear the configuration data.
state.resetData(); // Clear the state data.
state.status = 0; // We are performing a re-initialize.
this.data.controllerType = val;
EquipmentStateMessage.initDefaults();
// We are actually changing the config so lets clear out all the data.
this.board = BoardFactory.fromControllerType(val, this);
if (this.data.controllerType === ControllerType.Unknown) {
if (this._startOCPTimer) clearTimeout(this._startOCPTimer);
this._startOCPTimer = null;
this._startOCPTimer = setTimeout(() => { self.initNixieController(); }, 7500);
}
}
}
public resetData() {
if (sys.controllerType !== ControllerType.Nixie) {
// Do not clear this out if it is a virtual controller this causes problems.
this.equipment.reset();
this.circuitGroups.clear(0);
this.lightGroups.clear(0);
this.circuits.clear(0);
//this.bodies.clear(0); RKS: We no longer want to clear out the body data. Each controller is responsible for setting the bodies. Previously
// this used to clear out the bodies which would kill the capacity on the body for *Touch controllers we don't want that.
this.chlorinators.clear(0);
this.configVersion.clear();
this.covers.clear(0);
this.customNames.clear(0);
this.equipment.clear();
this.features.clear(0);
this.general.clear(0);
this.heaters.clear(0);
this.pumps.clear(0);
this.remotes.clear(0);
this.schedules.clear(0);
this.security.clear();
this.alerts.clear();
this.valves.clear(0);
this.chemControllers.clear(0);
this.filters.clear(0);
if (typeof this.data.eggTimers !== 'undefined') this.eggTimers.clear();
}
}
public async stopAsync() {
if (this._timerChanges) clearTimeout(this._timerChanges);
if (this._timerDirty) clearTimeout(this._timerDirty);
logger.info(`Shut down sys (config) object timers`);
return this.board.stopAsync();
}
public initNixieController() {
// Fall back to a nixie controller if an OCP is not installed.
switch (sys.controllerType || ControllerType.Unknown) {
case ControllerType.Unknown:
case ControllerType.Nixie:
state.equipment.controllerType = sys.controllerType = ControllerType.Nixie;
let board = sys.board as NixieBoard;
(async () => { await board.initNixieBoard(); })();
break;
}
}
public board: SystemBoard = new SystemBoard(this);
public anslq25Board: MockSystemBoard; // = new MockSystemBoard(this);
public virtualEquipment: VirtualEquipmentManager;
public ncp: NixieControlPanel = new NixieControlPanel();
public processVersionChanges(ver: ConfigVersion) { this.board.requestConfiguration(ver); }
public checkConfiguration() { this.board.checkConfiguration(); }
public cfgPath: string;
public data: any;
protected _lastUpdated: Date;
protected _isDirty: boolean;
protected _timerDirty: NodeJS.Timeout = null;
protected _timerChanges: NodeJS.Timeout;
protected _needsChanges: boolean;
// All the equipment items below.
public anslq25: Anslq25;
public general: General;
public equipment: Equipment;
public configVersion: ConfigVersion;
public bodies: BodyCollection;
public schedules: ScheduleCollection;
public eggTimers: EggTimerCollection;
public circuits: CircuitCollection;
public features: FeatureCollection;
public pumps: PumpCollection;
public chlorinators: ChlorinatorCollection;
public valves: ValveCollection;
public heaters: HeaterCollection;
public covers: CoverCollection;
public circuitGroups: CircuitGroupCollection;
public lightGroups: LightGroupCollection;
public remotes: RemoteCollection;
public security: Security;
public alerts: Alerts;
public customNames: CustomNameCollection;
public chemControllers: ChemControllerCollection;
public chemDosers: ChemDoserCollection;
public filters: FilterCollection;
public screenlogic: ScreenLogicComms;
public appVersion: string;
public get dirty(): boolean { return this._isDirty; }
public set dirty(val) {
let self = this;
this._isDirty = val;
this._lastUpdated = new Date();
this.data.lastUpdated = this._lastUpdated.toLocaleString();
if (this._timerDirty !== null) {
clearTimeout(this._timerDirty);
this._timerDirty = null;
}
if (this._isDirty) {
this._timerDirty = setTimeout(() => self.persist(), 3000);
}
}
public persist() {
this._isDirty = false;
// Don't overwrite the configuration if we failed during the initialization.
if (typeof sys.circuits === 'undefined' || !this.circuits === null) {
logger.info(`SKIPPING persist equipment because it is empty!`);
return;
}
sys.emitEquipmentChange();
Promise.resolve()
.then(() => { fs.writeFileSync(sys.cfgPath, JSON.stringify(sys.data, undefined, 2)); })
.catch(function (err) { if (err) logger.error('Error writing pool config %s %s', err, sys.cfgPath); });
}
// We are doing this because TS is lame. Accessing the app servers from the routes causes a cyclic include.
public findServersByType(type: string) {
let srv = [];
let servers = webApp.findServersByType(type);
for (let i = 0; i < servers.length; i++) {
srv.push({
uuid: servers[i].uuid,
name: servers[i].name,
type: servers[i].type,
isRunning: servers[i].isRunning,
isConnected: servers[i].isConnected
});
}
return srv;
}
protected onchange = (obj, fn) => {
const handler = {
get(target, property, receiver) {
// console.log(`getting prop: ${property} -- dataName? ${target.length}`)
const val = Reflect.get(target, property, receiver);
if (typeof val === 'function') return val.bind(receiver);
if (typeof val === 'object' && val !== null) {
if (util.types.isProxy(val)) return val;
return new Proxy(val, handler);
}
return val;
},
set(target, property, value, receiver) {
if (property !== 'lastUpdated' && Reflect.get(target, property, receiver) !== value) {
fn();
}
return Reflect.set(target, property, value, receiver);
},
deleteProperty(target, property) {
if (property in target) Reflect.deleteProperty(target, property);
return true;
}
};
return new Proxy(obj, handler);
};
public getSection(section?: string, opts?: any): any {
if (typeof section === 'undefined' || section === 'all') return this.data;
let c: any = this.data;
c = c[section];
if (typeof c !== 'object')
// return single values as objects so browsers don't complain
return { [section]: c };
else if (Array.isArray(c))
return extend(true, [], opts || [], c || []);
else
return extend(true, {}, opts || {}, c || {});
}
public get equipmentState() {
const self = this;
return {
lastUpdated: self._lastUpdated || 0,
controllerType: self.data.controllerType || ControllerType.Unknown,
pool: self.data.pool || {},
bodies: self.data.bodies || [],
schedules: self.data.schedules || [],
eggTimers: self.data.eggTimers || [],
customNames: self.data.customNames || [],
equipment: self.data.equipment || {},
valves: self.data.valves || [],
circuits: self.data.circuits || [],
features: self.data.features || [],
pumps: self.data.pumps || [],
chlorinators: self.data.chlorinators || [],
remotes: self.data.remotes || [],
heaters: self.data.heaters || [],
covers: self.data.covers || [],
security: self.data.security || {},
alerts: self.data.alerts || {},
appVersion: self.data.appVersion || '0.0.0'
};
}
public emitEquipmentChange() {
if (sys._hasChanged) {
this.emitData('config', this.equipmentState);
sys._hasChanged = false;
}
}
public emitData(name: string, data: any) {
webApp.emitToClients(name, data);
}
}
interface IEqItemCreator { ctor(data: any, name: string): T; }
interface IEqItem {
set(data);
clear();
hasChanged: boolean;
get(bCopy: boolean);
master: number;
}
class EqItem implements IEqItemCreator, IEqItem {
public dataName: string;
protected data: any;
public get hasChanged(): boolean { return sys._hasChanged; }
public set hasChanged(val: boolean) {
if (!sys._hasChanged && val) {
sys._hasChanged = true;
}
}
public get master(): number | any { return this.data.master || 0; }
public set master(val: number | any) { this.setDataVal('master', sys.board.valueMaps.equipmentMaster.encode(val)); }
ctor(data, name?: string): EqItem { return new EqItem(data, name); }
constructor(data, name?: string) {
if (typeof name !== 'undefined') {
if (typeof data[name] === 'undefined') data[name] = {};
this.data = data[name];
this.dataName = name;
this.initData();
} else {
this.data = data;
this.initData();
}
}
public initData() { if (typeof this.data.master === 'undefined') this.data.master = sys.board.equipmentMaster; }
public get(bCopy?: boolean): any {
// RSG: 7/2/20 - extend was deep copying arrays (eg pump circuits) by reference
// return bCopy ? extend(true, {}, this.data) : this.data;
return bCopy ? JSON.parse(JSON.stringify(this.data)) : this.data;
}
public clear() {
for (let prop in this.data) {
if (Array.isArray(this.data[prop])) this.data[prop].length = 0;
else this.data[prop] = undefined;
}
}
// This is a tricky endeavor. If we run into a collection then we need to obliterate the existing data and add in our data.
public set(data: any) {
let op = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
for (let i in op) {
let prop = op[i];
if (typeof this[prop] === 'function') continue;
if (typeof data[prop] !== 'undefined') {
if (this[prop] instanceof EqItemCollection) {
((this[prop] as unknown) as IEqItemCollection).set(data[prop]);
}
else if (this[prop] instanceof EqItem)
((this[prop] as unknown) as IEqItem).set(data[prop]);
else {
if (typeof this[prop] === null || typeof data[prop] === null) continue;
this[prop] = data[prop];
// RSG 7/13/2020 - type safety against a user sending the wrong type
// (eg clockSource=3 or clockSource=manual)
// Would be nice to convert types here, but we would need a mapping
// of objects to valueMaps and it isn't worth it (we would still be guessing)
// for something like "type" anyway
// RSG 7/16/2020 Rstrouse pointed out to me this would exclude us setting
// that was already undefined so relaxed the restriction a bit
}
}
}
}
protected setDataVal(name, val, persist?: boolean) {
if (this.data[name] !== val) {
// console.log(`Changing equipment: ${this.dataName} ${this.data.id} ${name}:${this.data[name]} --> ${val}`);
this.data[name] = val;
if (typeof persist === 'undefined' || persist) this.hasChanged = true;
}
else if (typeof persist !== 'undefined' && persist) this.hasChanged = true;
}
}
class ChildEqItem extends EqItem {
private _pmap = new WeakSet();
//private _dataKey = { id: 'parent' };
constructor(data: any, name: string, parent) {
super(data, name);
this._pmap['parent'] = parent;
}
public getParent() {
return this._pmap['parent'];
}
}
interface IEqItemCollection {
set(data);
clear();
}
class EqItemCollection implements IEqItemCollection {
protected data: any;
protected name: string;
constructor(data: any, name?: string) {
if (typeof name === 'undefined') {
this.data = data;
}
else {
if (typeof data[name] === "undefined") data[name] = [];
this.data = data[name];
this.name = name;
}
}
public static removeNullIds(data: any) {
if (typeof data !== 'undefined' && Array.isArray(data) && typeof data.length === 'number') {
for (let i = data.length - 1; i >= 0; i--) {
if (typeof data[i].id !== 'number') {
data.splice(i, 1);
}
else if (typeof data[i].id === 'undefined' || isNaN(data[i].id)) data.splice(i, 1);
}
}
}
public getItemByIndex(ndx: number, add?: boolean, data?: any): T {
if (this.data.length > ndx) return this.createItem(this.data[ndx]);
if (typeof add !== 'undefined' && add)
return this.add(extend({}, { id: ndx + 1 }, data));
return this.createItem(extend({}, { id: ndx + 1 }, data));
}
public getItemById(id: number | string, add?: boolean, data?: any): T {
let itm = this.find(elem => { return typeof (elem as { id?}).id !== 'undefined' && (elem as { id?}).id === id });
if (typeof itm !== 'undefined') return itm;
if (typeof add !== 'undefined' && add) return this.add(extend(true, { id: id }, data));
return this.createItem(data || { id: id });
}
public removeItemById(id: number | string): T {
let rem: T = null;
for (let i = this.data.length - 1; i >= 0; i--)
if (typeof this.data[i].id !== 'undefined' && this.data[i].id === id) {
rem = this.data.splice(i, 1);
return rem;
}
return rem;
}
public set(data) {
if (typeof data !== 'undefined') {
if (Array.isArray(data)) {
this.clear();
for (let i = 0; i < data.length; i++) {
// We are getting clever here in that we are simply adding the object and the add method
// should take care of hooking it all up.
this.add(data[i]);
}
}
}
}
public removeItemByIndex(ndx: number) {
this.data.splice(ndx, 1);
}
// Finds an item and returns undefined if it doesn't exist.
public find(f: (value: T, index?: number, obj?: any) => boolean): T {
let itm = this.data.find(f);
if (typeof itm !== 'undefined') return this.createItem(itm);
}
// This will return a new collection of this type. NOTE: This is a separate object but the data is still attached to the
// overall configuration. This means that changes made to the objects in the collection will reflect in the configuration.
// HOWEVER, any of the array manipulation methods like removeItemBy..., add..., or creation methods will not modify the configuration.
public filter(f: (value: T, index?: any, array?: any[]) => boolean): EqItemCollection {
return new EqItemCollection(this.data.filter(f));
}
public toArray() {
let arr = [];
if (typeof this.data !== 'undefined') {
for (let i = 0; i < this.data.length; i++) {
arr.push(this.createItem(this.data[i]));
}
}
return arr;
}
public createItem(data: any): T { return (new EqItem(data) as unknown) as T; }
public clear(master: number = -1) {
if (master === -1)
this.data.length = 0;
else {
for (let i = this.data.length - 1; i >= 0; i--) {
if (this.data[i].master === master) this.data.splice(i, 1);
}
}
}
public get length(): number { return typeof this.data !== 'undefined' ? this.data.length : 0; }
public set length(val: number) { if (typeof val !== 'undefined' && typeof this.data !== 'undefined') this.data.length = val; }
public add(obj: any): T { this.data.push(obj); return this.createItem(obj); }
public get(bCopy?: boolean): any {
return bCopy ? JSON.parse(JSON.stringify(this.data)) : this.data;
}
public emitEquipmentChange() { webApp.emitToClients(this.name, this.data); }
public sortByName() {
this.sort((a, b) => {
return a.name > b.name ? 1 : a.name !== b.name ? -1 : 0;
});
}
public sortById() {
this.sort((a, b) => {
return a.id > b.id ? 1 : a.id !== b.id ? -1 : 0;
});
}
public sort(fn: (a, b) => number) { this.data.sort(fn); }
public count(fn: (value: T, index?: any, array?: any[]) => boolean): number { return this.data.filter(fn).length; }
public getNextEquipmentId(range?: EquipmentIdRange, exclude?: number[]): number {
// RG 12-4-22 for some reason extend(true, {...}, range) was not evaluating the accessors
// for range.start & range.end
let r = extend(true, { start: 1, end: 255 }, {start: range.start }, {end: range.end});
for (let i = r.start; i <= r.end; i++) {
let eq = this.data.find(elem => elem.id === i);
if (typeof eq === 'undefined') {
if (typeof exclude !== 'undefined' && exclude.indexOf(i) !== -1) continue;
return i;
}
}
}
public getMaxId(activeOnly?: boolean, defId?: number) {
let maxId;
for (let i = 0; i < this.data.length; i++) {
if (typeof this.data[i].id !== 'undefined') {
if (typeof activeOnly !== 'undefined' && this.data[i].isActive === false) continue;
maxId = Math.max(maxId || 0, this.data[i].id);
}
}
return typeof maxId !== 'undefined' ? maxId : defId;
}
public getMinId(activeOnly?: boolean, defId?: number) {
let minId;
for (let i = 0; i < this.data.length; i++) {
if (typeof this.data[i].id !== 'undefined') {
if (typeof activeOnly !== 'undefined' && this.data[i].isActive === false) continue;
minId = Math.min(minId || this.data[i].id, this.data[i].id);
}
}
return typeof minId !== 'undefined' ? minId : defId;
}
}
export class Anslq25 extends EqItem {
ctor(data: any, name?: any): Anslq25 { return new Anslq25(data, name || 'anslq25'); }
public initData(){
if (typeof this.data.isActive === 'undefined') this.data.isActive = false;
}
public get controllerType(): ControllerType { return this.data.controllerType; }
public set controllerType(val: ControllerType) { this.setDataVal('controllerType', val); }
public get model(): number { return this.data.model; }
public set model(val: number) { this.setDataVal('model', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get portId(): number { return this.data.portId; }
public set portId(val: number) { this.setDataVal('portId', val); }
public get broadcastComms(): boolean { return this.data.broadcastComms; }
public set broadcastComms(val: boolean) { this.setDataVal('broadcastComms', val); }
public get modules(): ExpansionModuleCollection { return new ExpansionModuleCollection(this.data, "modules"); }
}
export class General extends EqItem {
ctor(data: any, name?: any): General { return new General(data, name || 'pool'); }
public get alias(): string { return this.data.alias; }
public set alias(val: string) { this.setDataVal('alias', val); }
public get owner(): Owner { return new Owner(this.data, 'owner'); }
public get options(): Options { return new Options(this.data, 'options'); }
public get location(): Location { return new Location(this.data, 'location'); }
public clear(master: number = -1) {
if (master === -1)
super.clear();
}
}
// Custom Names are IntelliTouch Only
export class CustomNameCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "customNames"); }
public createItem(data: any): CustomName { return new CustomName(data); }
}
export class CustomName extends EqItem {
public dataName = 'customNameConfig';
public set id(val: number) { this.setDataVal('id', val); }
public get id(): number { return this.data.id; }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
}
export class Owner extends EqItem {
public dataName = 'ownerConfig';
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get phone(): string { return this.data.phone; }
public set phone(val: string) { this.setDataVal('phone', val); }
public get email(): string { return this.data.email; }
public set email(val: string) { this.setDataVal('email', val); }
public get email2(): string { return this.data.email2; }
public set email2(val: string) { this.setDataVal('email2', val); }
public get phone2(): string { return this.data.phone2; }
public set phone2(val: string) { this.setDataVal('phone2', val); }
}
// Temp sensors are a bit different than other equipment items in that the collection does
// not use numeric ids. This could have been implemented where each potential sensor is identified
// on a tempSensors object and would have been more straight forward but would have limited the potential sensors
// to known items. This approach will allow the ability to include temperature sensors outside that list without
// having to modify external temperature feeds. Think of binding as the id of the sensor which must be unique for
// all defined sensors. The get/set calibration methods resolve the sensors and maintain the data from a single point.
export class TempSensorCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "tempSensors"); }
public createItem(data: any): TempSensor { return new TempSensor(data); }
public setCalibration(id: string, cal: number): TempSensor {
let sensor = this.getItemById(id);
sensor.calibration = cal;
return sensor;
}
public getCalibration(id: string): number {
let sensor = this.getItemById(id);
return sensor.calibration || 0;
}
}
export class FlowSensorCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "flowSensors"); }
public createItem(data: any): TempSensor { return new TempSensor(data); }
}
export class TempSensor extends EqItem {
public dataName = 'sensorConfig';
public set id(val: string) { this.setDataVal('id', val); }
public get id(): string { return this.data.id; }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get calibration(): number { return this.data.calibration; }
public set calibration(val: number) { this.setDataVal('calibration', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
}
export class Options extends EqItem {
public dataName = 'optionsConfig';
public initData() {
if (typeof this.data.units === 'undefined') this.data.units = 0;
if (typeof this.data.clockMode === 'undefined') this.data.clockMode = 12;
if (typeof this.data.adjustDST === 'undefined') this.data.adjustDST = true;
if (typeof this.data.freezeThreshold === 'undefined') this.data.freezeThreshold = 35;
if (typeof this.data.pumpDelay === 'undefined') this.data.pumpDelay = false;
if (typeof this.data.valveDelay === 'undefined') this.data.valveDelay = false;
if (typeof this.data.valveDelayTime === 'undefined') this.data.valveDelayTime = 30;
// RKS: 12-04-21 If you are reading this in a few months delete the line below.
if (this.data.valveDelayTime > 1000) this.data.valveDelayTime = this.data.valveDelayTime / 1000;
if (typeof this.data.heaterStartDelay === 'undefined') this.data.heaterStartDelay = true;
if (typeof this.data.cleanerStartDelay === 'undefined') this.data.cleanerStartDelay = true;
if (typeof this.data.cleanerSolarDelay === 'undefined') this.data.cleanerSolarDelay = true;
if (typeof this.data.heaterStartDelayTime === 'undefined') this.data.heaterStartDelayTime = 10;
if (typeof this.data.cleanerStartDelayTime === 'undefined') this.data.cleanerStartDelayTime = 300; // 5min
if (typeof this.data.cleanerSolarDelayTime === 'undefined') this.data.cleanerSolarDelayTime = 300; // 5min
if (typeof this.data.freezeCycleTime === 'undefined') this.data.freezeCycleTime = 15;
if (typeof this.data.freezeOverride === 'undefined') this.data.freezeOverride = 30;
}
public get clockMode(): number | any { return this.data.clockMode; }
public set clockMode(val: number | any) { this.setDataVal('clockMode', sys.board.valueMaps.clockModes.encode(val)); }
public get units(): number | any { return this.data.units; }
public set units(val: number | any) { this.setDataVal('units', sys.board.valueMaps.systemUnits.encode(val)); }
public get clockSource(): string { return this.data.clockSource; }
public set clockSource(val: string) { this.setDataVal('clockSource', val); }
public get adjustDST(): boolean { return this.data.adjustDST; }
public set adjustDST(val: boolean) { this.setDataVal('adjustDST', val); }
public get manualPriority(): boolean { return this.data.manualPriority; }
public set manualPriority(val: boolean) { this.setDataVal('manualPriority', val); }
public get vacation(): VacationOptions { return new VacationOptions(this.data, 'vacation', this); }
public get manualHeat(): boolean { return this.data.manualHeat; }
public set manualHeat(val: boolean) { this.setDataVal('manualHeat', val); }
public get pumpDelay(): boolean { return this.data.pumpDelay; }
public set pumpDelay(val: boolean) { this.setDataVal('pumpDelay', val); }
public get valveDelay(): boolean { return this.data.valveDelay; }
public set valveDelay(val: boolean) { this.setDataVal('valveDelay', val); }
public get valveDelayTime(): number { return this.data.valveDelayTime; }
public set valveDelayTime(val: number) { this.setDataVal('valveDelayTime', val); }
public get cooldownDelay(): boolean { return this.data.cooldownDelay; }
public set cooldownDelay(val: boolean) { this.setDataVal('cooldownDelay', val); }
public get freezeThreshold(): number { return this.data.freezeThreshold; }
public set freezeThreshold(val: number) { this.setDataVal('freezeThreshold', val); }
public get freezeCycleTime(): number { return this.data.freezeCycleTime; }
public set freezeCycleTime(val: number) { this.setDataVal('freezeCycleTime', val); }
public get freezeOverride(): number { return this.data.freezeOverride; }
public set freezeOverride(val: number) { this.setDataVal('freezeOverride', val); }
public get solarAsHeatPump(): boolean { return this.data.solarAsHeatPump; }
public set solarAsHeatPump(val: boolean) { this.setDataVal('solarAsHeatPump', val); }
public get showBadgeColors(): boolean { return this.data.showBadgeColors; }
public set showBadgeColors(val: boolean) { this.setDataVal('showBadgeColors', val); }
public get heaterStartDelay(): boolean { return this.data.heaterStartDelay; }
public set heaterStartDelay(val: boolean) { this.setDataVal('heaterStartDelay', val); }
public get heaterStartDelayTime(): number { return this.data.heaterStartDelayTime; }
public set heaterStartDelayTime(val: number) { this.setDataVal('heaterStartDelayTime', val); }
public get cleanerStartDelay(): boolean { return this.data.cleanerStartDelay; }
public set cleanerStartDelay(val: boolean) { this.setDataVal('cleanerStartDelay', val); }
public get cleanerStartDelayTime(): number { return this.data.cleanerStartDelayTime; }
public set cleanerStartDelayTime(val: number) { this.setDataVal('cleanerStartDelayTime', val); }
public get cleanerSolarDelay(): boolean { return this.data.cleanerSolarDelay; }
public set cleanerSolarDelay(val: boolean) { this.setDataVal('cleanerSolarDelay', val); }
public get cleanerSolarDelayTime(): number { return this.data.cleanerSolarDelayTime; }
public set cleanerSolarDelayTime(val: number) { this.setDataVal('cleanerSolarDelayTime', val); }
}
export class VacationOptions extends ChildEqItem {
public initData() {
if (typeof this.data.enabled === 'undefined') this.data.enabled = false;
if (typeof this.data.useTimeframe === 'undefined') this.data.useTimeframe = false;
}
private _startDate: Date;
private _endDate: Date;
public get enabled(): boolean { return this.data.enabled; }
public set enabled(val: boolean) { this.setDataVal('enabled', val); }
public get useTimeframe(): boolean { return this.data.useTimeframe; }
public set useTimeframe(val: boolean) { this.setDataVal('useTimeframe', val); }
public get startDate() {
if (typeof this._startDate === 'undefined') this._startDate = typeof this.data.startDate === 'string' ? new Date(this.data.startDate) : undefined;
return this._startDate;
}
public set startDate(val: Date | string | number) {
this._startDate = new Date(val);
this._saveDate('startDate', this._startDate);
}
public get endDate() {
if (typeof this._endDate === 'undefined') this._endDate = typeof this.data.endDate === 'string' ? new Date(this.data.endDate) : undefined;
return this._endDate;
}
public set endDate(val: Date | string | number) {
this._endDate = new Date(val);
this._saveDate('endDate', this._endDate);
}
private _saveDate(prop: string, dt: Date) {
if (typeof dt !== 'undefined' && !isNaN(dt.getTime())) {
dt.setHours(0, 0, 0, 0);
this.setDataVal(prop, Timestamp.toISOLocal(dt));
}
else this.setDataVal(prop, undefined);
}
}
export class Location extends EqItem {
public dataName = 'locationConfig';
public get address(): string { return this.data.address; }
public set address(val: string) { this.setDataVal('address', val); }
public get city(): string { return this.data.city; }
public set city(val: string) { this.setDataVal('city', val); }
public get state(): string {
if (!this.data.state && this.data.zip) {
const st = getStateForZip(this.data.zip);
if (st) this.setDataVal('state', st);
}
return this.data.state;
}
public set state(val: string) { this.setDataVal('state', val); }
public get zip(): string { return this.data.zip; }
public set zip(val: string) {
this.setDataVal('zip', val);
if (val && val.length >= 5) {
const st = getStateForZip(val);
if (st) this.setDataVal('state', st);
}
}
public get country(): string { return this.data.country; }
public set country(val: string) { this.setDataVal('country', val); }
public get latitude(): number { return this.data.latitude; }
public set latitude(val: number) { this.setDataVal('latitude', val); }
public get longitude(): number { return this.data.longitude; }
public set longitude(val: number) { this.setDataVal('longitude', val); }
public get timeZone(): number | any { return this.data.timeZone; }
public set timeZone(val: number | any) { this.setDataVal('timeZone', sys.board.valueMaps.timeZones.encode(val)); }
}
export class ExpansionModuleCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "modules"); }
public createItem(data: any): ExpansionModule { return new ExpansionModule(data); }
}
export class ExpansionModule extends EqItem {
public dataName = 'expansionModuleConfig';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get desc(): string { return this.data.desc; }
public set desc(val: string) { this.setDataVal('desc', val); }
public get type(): number { return this.data.type; }
public set type(val: number) { this.setDataVal('type', sys.board.valueMaps.expansionBoards.encode(val)); }
public get part(): string { return this.data.part; }
public set part(val: string) { this.setDataVal('part', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
}
export class ExpansionPanelCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "expansions"); }
public createItem(data: any): ExpansionPanel { return new ExpansionPanel(data); }
}
export class ExpansionPanel extends EqItem {
public dataName = 'expansionPanelConfig';
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get type(): number { return this.data.type; }
public set type(val: number) { this.setDataVal('type', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get modules(): ExpansionModuleCollection { return new ExpansionModuleCollection(this.data, "modules"); }
}
export class Equipment extends EqItem {
public dataName = 'equipmentConfig';
public initData() {
if (typeof this.data.single === 'undefined') {
if (this.data.dual === true || this.data.shared === true) this.data.single = false;
else if (sys.controllerType !== ControllerType.IntelliTouch) this.data.single = true;
}
if (typeof this.data.softwareVersion === 'undefined') this.data.softwareVersion = '';
if (typeof this.data.bootloaderVersion === 'undefined') this.data.bootloaderVersion = '';
if (typeof this.data.maxChemDosers === 'undefined') this.data.maxChemDosers = 10;
}
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get type(): number { return this.data.type; }
public set type(val: number) { this.setDataVal('type', val); }
public get single(): boolean { return this.data.single; }
public set single(val: boolean) { this.setDataVal('single', val); }
public get shared(): boolean { return this.data.shared; }
public set shared(val: boolean) { this.setDataVal('shared', val); }
public get dual(): boolean { return this.data.dual; }
public set dual(val: boolean) { this.setDataVal('dual', val); }
public get intakeReturnValves(): boolean { return this.data.intakeReturnValves; }
public set intakeReturnValves(val: boolean) { this.setDataVal('intakeReturnValves', val); }
public get maxBodies(): number { return this.data.maxBodies || 4; }
public set maxBodies(val: number) { this.setDataVal('maxBodies', val); }
public get maxValves(): number { return this.data.maxValves || 26; }
public set maxValves(val: number) { this.setDataVal('maxValves', val); }
public get maxPumps(): number { return this.data.maxPumps || 16; }
public set maxPumps(val: number) { this.setDataVal('maxPumps', val); }
public set maxSchedules(val: number) { this.setDataVal('maxSchedules', val); }
public get maxSchedules(): number { return this.data.maxSchedules || 12; }
public get maxCircuits(): number { return this.data.maxCircuits || 3; }
public set maxCircuits(val: number) { this.setDataVal('maxCircuits', val); }
public get maxFeatures(): number { return this.data.maxFeatures || 10; }
public set maxFeatures(val: number) { this.setDataVal('maxFeatures', val); }
public get maxRemotes(): number { return this.data.maxRemotes || 9; }
public set maxRemotes(val: number) { this.setDataVal('maxRemotes', val); }
public get maxCircuitGroups(): number { return this.data.maxCircuitGroups || 32; }
public set maxCircuitGroups(val: number) { this.setDataVal('maxCircuitGroups', val); }
public get maxLightGroups(): number { return this.data.maxLightGroups || 40; }
public set maxLightGroups(val: number) { this.setDataVal('maxLightGroups', val); }
public get maxChlorinators(): number { return this.data.maxChlorinators || 1; }
public set maxChlorinators(val: number) { this.setDataVal('maxChlorinators', val); }
public get maxHeaters(): number { return this.data.maxHeaters || 16; }
public set maxHeaters(val: number) { this.setDataVal('maxHeaters', val); }
public get model(): string { return this.data.model; }
public set model(val: string) { this.setDataVal('model', val); }
public get maxIntelliBrites(): number { return this.data.maxIntelliBrites; }
public set maxIntelliBrites(val: number) { this.setDataVal('maxIntelliBrites', val); }
public get maxChemControllers(): number { return this.data.maxChemControllers; }
public set maxChemControllers(val: number) { this.setDataVal('maxChemControllers', val); }
public get maxChemDosers(): number { return this.data.maxChemDosers; }
public set maxChemDosers(val: number) { this.setDataVal('maxChemDosers', val); }
public get expansions(): ExpansionPanelCollection { return new ExpansionPanelCollection(this.data, "expansions"); }
public get modules(): ExpansionModuleCollection { return new ExpansionModuleCollection(this.data, "modules"); }
public get maxCustomNames(): number { return this.data.maxCustomNames || 10; }
public set maxCustomNames(val: number) { this.setDataVal('maxCustomNames', val); }
public get tempSensors(): TempSensorCollection { return new TempSensorCollection(this.data); }
public get flowSensors(): FlowSensorCollection { return new FlowSensorCollection(this.data); }
public set controllerFirmware(val: string) { this.setDataVal('softwareVersion', val); }
public get controllerFirmware(): string { return this.data.softwareVersion; }
/**
* IntelliCenter v3 behavior gate.
*
* Returns false unless:
* - controller type is IntelliCenter
* - controller firmware is present and parses as a number
* - firmware major.minor >= 3.0
*/
public get isIntellicenterV3(): boolean {
// Note: We intentionally do NOT check controllerType here because this property
// is queried during early initialization before controllerType is confirmed.
// All callers either run in IntelliCenter-specific code paths or explicitly check controllerType.
const fw = parseFloat(this.controllerFirmware || this.data.softwareVersion || '');
if (!Number.isFinite(fw)) return false;
return fw >= 3.0;
}
public set bootloaderVersion(val: string) { this.setDataVal('bootloaderVersion', val); }
public get bootloaderVersion(): string { return this.data.bootloaderVersion; }
public reset() {
}
setEquipmentIds() {
this.data.equipmentIds = {
circuits: { start: sys.board.equipmentIds.circuits.start, end: sys.board.equipmentIds.circuits.end },
features: { start: sys.board.equipmentIds.features.start, end: sys.board.equipmentIds.features.end },
circuitGroups: { start: sys.board.equipmentIds.circuitGroups.start, end: sys.board.equipmentIds.circuitGroups.end },
virtualCircuits: { start: sys.board.equipmentIds.virtualCircuits.start, end: sys.board.equipmentIds.virtualCircuits.end },
invalidIds: sys.board.equipmentIds.invalidIds.get()
};
this.hasChanged = true;
}
public get equipmentIds(): any {
return this.data.equipmentIds;
}
}
export class IntelliTouchEquipment extends Equipment { }
export class ConfigVersion extends EqItem {
constructor(data: any, name?: string) {
super(data, name);
/*
RG - Changed lastUpdated to be a permanent attribute to the data.
*Touch doesn't keep versions of individual types so date is needed to periodically check the config.
if (typeof data.lastUpdated === 'undefined') this._lastUpdated = new Date();
else this._lastUpdated = new Date(data.lastUpdated);
if (isNaN(this._lastUpdated.getTime())) this._lastUpdated = new Date(); */
}
//protected _lastUpdated: Date;
public get lastUpdated(): Date {
if (typeof this.data.lastUpdated === 'undefined') { this.data.lastUpdated = new Date().setHours(new Date().getHours() - 1); }
return new Date(this.data.lastUpdated);
}
public set lastUpdated(val: Date) { this.setDataVal('lastUpdated', Timestamp.toISOLocal(val), false); }
public get options(): number { return this.data.options; }
public set options(val: number) { this.setDataVal('options', val); }
public get circuits(): number { return this.data.circuits; }
public set circuits(val: number) { this.setDataVal('circuits', val); }
public get features(): number { return this.data.features; }
public set features(val: number) { this.setDataVal('features', val); }
public get pumps(): number { return this.data.pumps; }
public set pumps(val: number) { this.setDataVal('pumps', val); }
public get remotes(): number { return this.data.remotes; }
public set remotes(val: number) { this.setDataVal('remotes', val); }
public get circuitGroups(): number { return this.data.circuitGroups; }
public set circuitGroups(val: number) { this.setDataVal('circuitGroups', val); }
public get chlorinators(): number { return this.data.chlorinators; }
public set chlorinators(val: number) { this.setDataVal('chlorinators', val); }
public get intellichem(): number { return this.data.intellichem; }
public set intellichem(val: number) { this.setDataVal('intellichem', val); }
public get systemState(): number { return this.data.systemState; }
public set systemState(val: number) { this.setDataVal('systemState', val); }
public get valves(): number { return this.data.valves; }
public set valves(val: number) { this.setDataVal('valves', val); }
public get heaters(): number { return this.data.heaters; }
public set heaters(val: number) { this.setDataVal('heaters', val); }
public get security(): number { return this.data.security; }
public set security(val: number) { this.setDataVal('security', val); }
public get general(): number { return this.data.general; }
public set general(val: number) { this.setDataVal('general', val); }
public get equipment(): number { return this.data.equipment; }
public set equipment(val: number) { this.setDataVal('equipment', val, false); }
public get covers(): number { return this.data.covers; }
public set covers(val: number) { this.setDataVal('covers', val); }
public get schedules(): number { return this.data.schedules; }
public set schedules(val: number) { this.setDataVal('schedules', val); }
public hasChanges(ver: ConfigVersion) {
// This will only check for items that are in the incoming ver data. This
// is intentional so that only new versioned items will be detected.
for (let prop in ver.data) {
if (prop === 'lastUpdated') continue;
if (ver.data[prop] !== this.data[prop]) {
return true;
}
}
return false;
}
public clear() {
for (let prop in this.data) {
if (prop === 'lastUpdated') continue;
this.data[prop] = 0;
}
}
}
export class BodyCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "bodies"); }
public createItem(data: any): Body { return new Body(data); }
// RKS: This finds a body by any of it's identifying factors.
public findByObject(obj: any): Body {
// This has a priority weight to the supplied object values.
// 1. If the id is provided it will search by id.
// 2. If the id is not provided and the circuit id is provided it will search by the circuit id.
// 3. Finally if nothing else is provided it will search by name.
let body = this.find(elem => {
if (typeof obj.id !== 'undefined') return obj.id === elem.id;
else if (typeof obj.circuit !== 'undefined') return obj.circuit === elem.circuit;
else if (typeof obj.name !== 'undefined') return obj.name === elem.name;
else return false;
});
return body;
}
}
export class Body extends EqItem {
public dataName = 'bodyConfig';
public initData() {
if (typeof this.data.capacityUnits === 'undefined') this.data.capacityUnits = 1;
if (typeof this.data.showInDashboard === 'undefined') this.data.showInDashboard = true;
if (typeof this.data.heatMode === 'undefined') this.data.heatMode = 0;
}
public get id(): number { return this.data.id; }
public set id(val: number) { this.data.id = val; }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get alias(): string { return this.data.alias; }
public set alias(val: string) { this.setDataVal('alias', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.bodies.encode(val)); }
public get circuit(): number { return this.data.circuit; }
public set circuit(val: number) { this.setDataVal('circuit', val); }
public get capacity(): number { return this.data.capacity; }
public set capacity(val: number) { this.setDataVal('capacity', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get manualHeat(): boolean { return this.data.manualHeat; }
public set manualHeat(val: boolean) { this.setDataVal('manualHeat', val); }
public get setPoint(): number { return this.data.setPoint; }
public set setPoint(val: number) { this.setDataVal('setPoint', val); }
public get heatMode(): number { return this.data.heatMode; }
public set heatMode(val: number) { this.setDataVal('heatMode', val); }
public get heatSetpoint(): number { return this.data.setPoint; }
public set heatSetpoint(val: number) { this.setDataVal('setPoint', val); }
public get coolSetpoint(): number { return this.data.coolSetpoint; }
public set coolSetpoint(val: number) { this.setDataVal('coolSetpoint', val); }
public get showInDashboard(): boolean { return this.data.showInDashboard; }
public set showInDashboard(val: boolean) { this.setDataVal('showInDashboard', val); }
public get capacityUnits(): number | any { return this.data.capacityUnits; }
public set capacityUnits(val: number | any) { this.setDataVal('capacityUnits', sys.board.valueMaps.volumeUnits.encode(val)); }
public getHeatModes() { return sys.board.bodies.getHeatModes(this.id); }
public getHeatModesV2() { return sys.board.bodies.getHeatModesV2(this.id); }
public getExtended() {
let body = this.get(true);
body.capacityUnits = sys.board.valueMaps.volumeUnits.transform(this.capacityUnits || 1);
return body;
}
}
export class ScheduleCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "schedules"); }
public createItem(data: any): Schedule { return new Schedule(data); }
public getNextEquipmentId(range: EquipmentIdRange): number {
let data = [...this.data, ...sys.eggTimers.get()]
for (let i = range.start; i <= range.end; i++) {
let eq = data.find(elem => elem.id === i);
if (typeof eq === 'undefined') return i;
}
}
}
export class Schedule extends EqItem {
constructor(data: any) { super(data); }
public initData() {
if (typeof this.data.startDate === 'undefined') this._startDate = new Date();
else this._startDate = new Date(this.data.startDate);
if (isNaN(this._startDate.getTime())) this._startDate = new Date();
if (typeof this.data.startTimeType === 'undefined') this.data.startTimeType = 0;
if (typeof this.data.endTimeType === 'undefined') this.data.endTimeType = 0;
if (typeof this.data.display === 'undefined') this.data.display = 0;
if (typeof this.data.startTimeOffset === 'undefined') this.data.startTimeOffset = 0;
if (typeof this.data.endTimeOffset === 'undefined') this.data.endTimeOffset = 0;
}
// todo: investigate schedules having startDate and _startDate
private _startDate: Date;
public dataName = 'scheduleConfig';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get startTime(): number { return this.data.startTime; }
public set startTime(val: number) { this.setDataVal('startTime', val); }
public get endTime(): number { return this.data.endTime; }
public set endTime(val: number) { this.setDataVal('endTime', val); }
public get startTimeOffset(): number { return this.data.startTimeOffset || 0; }
public set startTimeOffset(val: number) { this.setDataVal('startTimeOffset', val); }
public get endTimeOffset(): number { return this.data.endTimeOffset || 0; }
public set endTimeOffset(val: number) { this.setDataVal('endTimeOffset', val); }
public get scheduleDays(): number { return this.data.scheduleDays; }
public set scheduleDays(val: number) { this.setDataVal('scheduleDays', val); }
public get circuit(): number { return this.data.circuit; }
public set circuit(val: number) { this.setDataVal('circuit', val); }
public get heatSource(): number | any { return this.data.heatSource; }
public set heatSource(val: number | any) { this.setDataVal('heatSource', sys.board.valueMaps.heatSources.encode(val)); }
public get changeHeatSetpoint(): boolean { return this.data.changeHeatSetpoint; }
public set changeHeatSetpoint(val: boolean) { this.setDataVal('changeHeatSetpoint', val); }
public get heatSetpoint(): number { return this.data.heatSetpoint; }
public set heatSetpoint(val: number) { this.setDataVal('heatSetpoint', val); }
public get coolSetpoint(): number { return this.data.coolSetpoint; }
public set coolSetpoint(val: number) { this.setDataVal('coolSetpoint', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get disabled(): boolean { return this.data.disabled; }
public set disabled(val: boolean) { this.setDataVal('disabled', val); }
public get startMonth(): number { return this._startDate.getMonth() + 1; }
public set startMonth(val: number) { if (typeof this._startDate === 'undefined') this._startDate = new Date(); this._startDate.setMonth(val - 1); this._saveStartDate(); }
public get startDay(): number { if (typeof this._startDate === 'undefined') this._startDate = new Date(); return this._startDate.getDate(); }
public set startDay(val: number) { if (typeof this._startDate === 'undefined') this._startDate = new Date(); this._startDate.setDate(val); this._saveStartDate(); }
public get startYear(): number { if (typeof this._startDate === 'undefined') this._startDate = new Date(); return this._startDate.getFullYear(); }
public set startYear(val: number) { if (typeof this._startDate === 'undefined') this._startDate = new Date(); this._startDate.setFullYear(val < 100 ? val + 2000 : val); this._saveStartDate(); }
public get startDate(): Date {
this._startDate = typeof this._startDate === 'undefined' ? new Date(this.data.startDate) : this._startDate;
return typeof this._startDate === 'undefined' || isNaN(this._startDate.getTime()) ? new Date() : this._startDate;
}
public set startDate(val: Date) { this._startDate = val; }
public get scheduleType(): number | any { return this.data.scheduleType; }
public set scheduleType(val: number | any) { this.setDataVal('scheduleType', sys.board.valueMaps.scheduleTypes.encode(val)); }
public get startTimeType(): number | any { return this.data.startTimeType; }
public set startTimeType(val: number | any) { this.setDataVal('startTimeType', sys.board.valueMaps.scheduleTimeTypes.encode(val)); }
public get endTimeType(): number | any { return this.data.endTimeType; }
public set endTimeType(val: number | any) { this.setDataVal('endTimeType', sys.board.valueMaps.scheduleTimeTypes.encode(val)); }
public get display() { return this.data.display; }
public set display(val: number | any) { this.setDataVal('display', sys.board.valueMaps.scheduleDisplayTypes.encode(val)); }
public get schedGroup(): number { return this.data.schedGroup || 0; }
public set schedGroup(val: number) { this.setDataVal('schedGroup', val); }
private _saveStartDate() { this.startDate.setHours(0, 0, 0, 0); this.setDataVal('startDate', Timestamp.toISOLocal(this.startDate)); }
public get flags(): number { return this.data.flags; }
public set flags(val: number) { this.setDataVal('flags', val); }
}
// TODO: Get rid of this
export class EggTimerCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "eggTimers"); }
public createItem(data: any): EggTimer { return new EggTimer(data); }
}
// TODO: Get rid of this
export class EggTimer extends EqItem {
constructor(data: any) {
super(data);
if (typeof data.startDate === "undefined") this._startDate = new Date();
else this._startDate = new Date(data.startDate);
if (isNaN(this._startDate.getTime())) this._startDate = new Date();
}
public dataName = 'eggTimerConfig';
private _startDate: Date = new Date();
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get runTime(): number { return this.data.runTime; }
public set runTime(val: number) { this.setDataVal('runTime', val); }
public get circuit(): number { return this.data.circuit; }
public set circuit(val: number) { this.setDataVal('circuit', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
}
export class CircuitCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "circuits"); }
public filter(f: (value: Circuit, index?: any, array?: any[]) => boolean): CircuitCollection {
return new CircuitCollection({ circuits: this.data.filter(f) });
}
public createItem(data: any): Circuit { return new Circuit(data); }
public add(obj: any): Circuit {
this.data.push(obj);
let circuit = this.createItem(obj);
if (typeof circuit.name === "undefined")
circuit.name = Circuit.getIdName(circuit.id);
return circuit;
}
public getInterfaceById(id: number, add?: boolean, data?: any): ICircuit {
if (sys.board.equipmentIds.circuitGroups.isInRange(id))
return sys.circuitGroups.getInterfaceById(id);
else if (sys.board.equipmentIds.features.isInRange(id))
return sys.features.getItemById(id, add, data);
return sys.circuits.getItemById(id, add, data);
}
}
export class Circuit extends EqItem implements ICircuit {
public dataName = 'circuitConfig';
public initData() {
if (typeof this.data.freeze === 'undefined') this.data.freeze = false;
if (typeof this.data.type === 'undefined') this.data.type = 0;
if (typeof this.data.isActive === 'undefined') this.data.isActive = false;
if (typeof this.data.eggTimer === 'undefined') this.data.eggTimer = 720;
if (typeof this.data.showInFeatures === 'undefined') this.data.showInFeatures = true;
}
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get nameId(): number { return this.data.nameId; }
public set nameId(val: number) { this.setDataVal('nameId', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.circuitFunctions.encode(val)); }
// RG - remove this after I figure out what a macro means
public get macro(): boolean { return this.data.macro; }
public set macro(val: boolean) { this.setDataVal('macro', val); }
// end remove
public get freeze(): boolean { return this.data.freeze; }
public set freeze(val: boolean) { this.setDataVal('freeze', val); }
public get showInFeatures(): boolean { return this.data.showInFeatures; }
public set showInFeatures(val: boolean) { this.setDataVal('showInFeatures', val); }
public get eggTimer(): number { return this.data.eggTimer; }
public set eggTimer(val: number) { this.setDataVal('eggTimer', val); }
public get lightingTheme(): number | any { return this.data.lightingTheme; }
public set lightingTheme(val: number | any) { this.setDataVal('lightingTheme', sys.board.valueMaps.lightThemes.encode(val)); }
public get level(): number { return this.data.level; }
public set level(val: number) { this.setDataVal('level', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get dontStop(): boolean { return this.data.dontStop; }
public set dontStop(val: boolean) { this.setDataVal('dontStop', val); }
public get connectionId(): string { return this.data.connectionId; }
public set connectionId(val: string) { this.setDataVal('connectionId', val); }
public get deviceBinding(): string { return this.data.deviceBinding; }
public set deviceBinding(val: string) { this.setDataVal('deviceBinding', val); }
public get hasHeatSource() { return typeof sys.board.valueMaps.circuitFunctions.get(this.type || 0).hasHeatSource !== 'undefined' ? sys.board.valueMaps.circuitFunctions.get(this.type || 0).hasHeatSource : false };
public getLightThemes() {
// Lets do this universally driven by the metadata.
let cf = sys.board.valueMaps.circuitFunctions.transform(this.type);
if (cf.isLight && typeof cf.theme !== 'undefined') {
let arrThemes = sys.board.valueMaps.lightThemes.toArray();
let themes = [];
for (let i = 0; i < arrThemes.length; i++) {
let thm = arrThemes[i];
if (typeof thm.types !== 'undefined' && thm.types.length > 0 && thm.types.includes(cf.theme)) themes.push(thm);
}
return themes;
}
else return [];
}
public getLightCommands() {
// Lets do this universally driven by the metadata.
let cf = sys.board.valueMaps.circuitFunctions.transform(this.type);
if (cf.isLight && typeof cf.theme !== 'undefined') {
let arrCommands = sys.board.valueMaps.lightCommands.toArray();
let cmds = [];
for (let i = 0; i < arrCommands.length; i++) {
let cmd = arrCommands[i];
if (typeof cmd.types !== 'undefined' && cmd.types.length > 0 && cmd.types.includes(cf.theme)) cmds.push(cmd);
}
return cmds;
}
else return [];
}
public static getIdName(id: number) {
let defName;
// RKS: 07-19-22 I think this is some sort of artifact. The system should not be creating this data as a default because
// the board itself should be coming up with these names.
switch (sys.controllerType) {
case ControllerType.SunTouch:
break;
default:
if (sys.board.equipmentIds.circuitGroups.isInRange(id))
defName = `Group ${id - sys.board.equipmentIds.circuitGroups.start + 1}`;
else if (sys.board.equipmentIds.features.isInRange(id))
defName = `Feature ${id - sys.board.equipmentIds.features.start + 1}`;
else if (sys.board.equipmentIds.circuits.isInRange(id)) {
if (id <= 1) defName = 'Spa';
else if (id === 6) defName = 'Pool';
else if (id < 6) defName = `Aux ${id - 1}`;
else defName = `Aux ${id - 2}`;
}
break;
}
return defName;
}
}
export class FeatureCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "features"); }
public filter(f: (value: Circuit, index?: any, array?: any[]) => boolean): FeatureCollection {
return new FeatureCollection({ features: this.data.filter(f) });
}
public createItem(data: any): Feature { return new Feature(data); }
}
export class Feature extends EqItem implements ICircuit {
public initData() {
if (typeof this.data.freeze === 'undefined') this.data.freeze = false;
if (typeof this.data.type === 'undefined') this.data.type = 0;
if (typeof this.data.isActive === 'undefined') this.data.isActive = true;
if (typeof this.data.eggTimer === 'undefined') this.data.eggTimer = 720;
if (typeof this.data.showInFeatures === 'undefined') this.data.showInFeatures = true;
if (typeof this.data.master === 'undefined') this.data.master = sys.board.equipmentMaster;
}
public dataName = 'featureConfig';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get nameId(): number { return this.data.nameId; }
public set nameId(val: number) { this.setDataVal('nameId', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.featureFunctions.encode(val)); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get freeze(): boolean { return this.data.freeze; }
public set freeze(val: boolean) { this.setDataVal('freeze', val); }
public get showInFeatures(): boolean { return this.data.showInFeatures; }
public set showInFeatures(val: boolean) { this.setDataVal('showInFeatures', val); }
public get eggTimer(): number { return this.data.eggTimer; }
public set eggTimer(val: number) { this.setDataVal('eggTimer', val); }
public get dontStop(): boolean { return utils.makeBool(this.data.dontStop); }
public set dontStop(val: boolean) { this.setDataVal('dontStop', val); }
public get macro(): boolean { return this.data.macro; }
public set macro(val: boolean) { this.setDataVal('macro', val); }
}
export interface ICircuitCollection {
getItemById(id: number, add?: boolean, data?: any);
}
export interface ICircuit {
id: number;
name: string;
nameId?: number;
type: number;
eggTimer: number;
dontStop: boolean;
freeze?: boolean;
isActive: boolean;
lightingTheme?: number;
level?: number;
//showInCircuits?: boolean;
showInFeatures?: boolean;
macro?: boolean;
getLightThemes?: (type?: number) => {};
getLightCommands?: (type?: number) => {};
get(copy?: boolean);
master: number;
}
export class PumpCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "pumps"); }
public createItem(data: any): Pump { return new Pump(data); }
public getDualSpeed(add?: boolean): Pump {
let ds = sys.board.valueMaps.pumpTypes.getValue('ds');
let pump = this.find(x => x.type === ds);
return typeof pump !== 'undefined' ? pump : this.getItemById(10, add, { id: 10, type: ds, name: 'Two Speed', master: 0 });
}
public removePumpByAddress(address: number) {
let pmp = this.find(elem => elem.address === address);
if (typeof pmp !== 'undefined') return this.removeItemById(pmp.id);
return pmp;
}
public getPumpByAddress(address: number, add?: boolean, data?: any) : Pump {
let pmp = this.find(elem => elem.address === address);
if (typeof pmp !== 'undefined') return pmp;
if (typeof add !== 'undefined' && add) return this.add(data || { id: this.data.length + 1, address: address });
return this.createItem(data || { id: this.getNextEquipmentId(), address: address });
}
public getNextEquipmentId(range?: EquipmentIdRange, exclude?: number[]): number { return super.getNextEquipmentId(typeof range === 'undefined' ? sys.board.equipmentIds.pumps : range, exclude); }
}
export class Pump extends EqItem {
public dataName = 'pumpConfig';
public initData() {
if (typeof this.data.isVirtual !== 'undefined') delete this.data.isVirtual;
if (typeof this.data.portId === 'undefined') this.data.portId = 0;
if (typeof this.data.body === 'number' && this.data.model === 2 && this.data.master === 1){
// convert SS from body types to circuit arrays
if (this.data.body === 255 || this.data.body === 0 && !this.data.circuits.find(el => el.circuit === 6)) {
this.data.circuits.push({"circuit": 6, "relay": 1, "units": 0, "id": this.data.circuits.length + 1, "master": 1})
}
if (this.data.body === 255 || this.data.body === 101 && !this.data.circuits.find(el => el.circuit === 1)) {
this.data.circuits.push({"circuit": 1, "relay": 1, "units": 0, "id": this.data.circuits.length + 1, "master": 1})
}
this.data.body = undefined;
}
}
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get portId(): number { return this.data.portId; }
public set portId(val: number) { this.setDataVal('portId', val); }
public get address(): number { return this.data.address; }
public set address(val: number) { this.setDataVal('address', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.pumpTypes.encode(val)); }
public get minSpeed(): number { return this.data.minSpeed; }
public set minSpeed(val: number) { this.setDataVal('minSpeed', val); }
public get maxSpeed(): number { return this.data.maxSpeed; }
public set maxSpeed(val: number) { this.setDataVal('maxSpeed', val); }
public get primingSpeed(): number { return this.data.primingSpeed; }
public set primingSpeed(val: number) { this.setDataVal('primingSpeed', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get flowStepSize(): number { return this.data.flowStepSize; }
public set flowStepSize(val: number) { this.setDataVal('flowStepSize', val); }
public get minFlow(): number { return this.data.minFlow; }
public set minFlow(val: number) { this.setDataVal('minFlow', val); }
public get maxFlow(): number { return this.data.maxFlow; }
public set maxFlow(val: number) { this.setDataVal('maxFlow', val); }
public get primingTime(): number { return this.data.primingTime; }
public set primingTime(val: number) { this.setDataVal('primingTime', val); }
public get speedStepSize(): number { return this.data.speedStepSize; }
public set speedStepSize(val: number) { this.setDataVal('speedStepSize', val); }
public get turnovers() { return this.data.turnovers; }
public set turnovers(val: number) { this.setDataVal('turnovers', val); }
public get manualFilterGPM() { return this.data.manualFilterGPM; }
public set manualFilterGPM(val: number) { this.setDataVal('manualFilterGPM', val); }
public get maxSystemTime() { return this.data.maxSystemTime; }
public set maxSystemTime(val: number) { this.setDataVal('maxSystemTime', val); }
public get maxPressureIncrease() { return this.data.maxPressureIncrease; }
public set maxPressureIncrease(val: number) { this.setDataVal('maxPressureIncrease', val); }
public get backwashFlow() { return this.data.backwashFlow; }
public set backwashFlow(val: number) { this.setDataVal('backwashFlow', val); }
public get backwashTime() { return this.data.backwashTime; }
public set backwashTime(val: number) { this.setDataVal('backwashTime', val); }
public get rinseTime() { return this.data.rinseTime; }
public set rinseTime(val: number) { this.setDataVal('rinseTime', val); }
public get vacuumFlow() { return this.data.vacuumFlow; }
public set vacuumFlow(val: number) { this.setDataVal('vacuumFlow', val); }
public get vacuumTime() { return this.data.vacuumTime; }
public set vacuumTime(val: number) { this.setDataVal('vacuumTime', val); }
public get backgroundCircuit() { return this.data.backgroundCircuit; }
public set backgroundCircuit(val: number) { this.setDataVal('backgroundCircuit', val); }
public get filterSize() { return this.data.filterSize; }
public set filterSize(val: number) { this.setDataVal('filterSize', val); }
// public get isVirtual() { return this.data.isVirtual; }
// public set isVirtual(val: boolean) { this.setDataVal('isVirtual', val); }
public get connectionId(): string { return this.data.connectionId; }
public set connectionId(val: string) { this.setDataVal('connectionId', val); }
public get deviceBinding(): string { return this.data.deviceBinding; }
public set deviceBinding(val: string) { this.setDataVal('deviceBinding', val); }
public get defaultUnits() {
if (sys.board.valueMaps.pumpTypes.getName(this.type) === 'vf')
return sys.board.valueMaps.pumpUnits.getValue('gpm');
else
return sys.board.valueMaps.pumpUnits.getValue('rpm');
}
// This is relevant only for single speed pumps. All other pumps are driven from the circuits. You cannot
// identify a single speed pump in *Touch but the definition can be stored and the wattage is acquired by the model.
public get body(): number | any { return this.data.body; }
public set body(val: number | any) { this.setDataVal('body', sys.board.valueMaps.pumpBodies.encode(val)); }
public get model(): number { return this.data.model; }
public set model(val: number) { this.setDataVal('model', val); }
public get circuits(): PumpCircuitCollection { return new PumpCircuitCollection(this.data, "circuits"); }
public get relays(): PumpRelayCollection { return new PumpRelayCollection(this.data, "relays"); }
public deletePumpCircuit(pumpCircuitId: number) {
return sys.board.pumps.deletePumpCircuit(this, pumpCircuitId);
}
//public setType(pumpType: number) {
// sys.board.pumps.setType(this, pumpType);
//}
public nextAvailablePumpCircuit(): number {
let pumpCircuits = this.circuits;
for (let i = 1; i <= 8; i++) {
let _circ = pumpCircuits.getItemById(i);
if (typeof _circ.circuit === 'undefined')
return i;
}
return 0;
}
public checkOrMakeValidRPM(rpm?: number) {
if (!rpm || rpm <= this.minSpeed || rpm >= this.maxSpeed)
return 1000;
else return rpm;
}
public checkOrMakeValidGPM(gpm?: number) {
if (!gpm || gpm <= this.minFlow || gpm >= this.maxFlow)
return 15;
else return gpm;
}
public isRPMorGPM(rate: number): 'rpm' | 'gpm' | 'none' {
if (rate >= this.minFlow || rate <= this.maxFlow) return 'gpm';
if (rate >= this.minSpeed || rate <= this.maxSpeed) return 'rpm';
return 'none';
}
}
export class PumpCircuitCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "circuits"); }
public createItem(data: any): PumpCircuit { return new PumpCircuit(data); }
}
export class PumpCircuit extends EqItem {
public dataName = 'pumpCircuitConfig';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get circuit(): number { return this.data.circuit; }
public set circuit(val: number) { this.setDataVal('circuit', val); }
public get flow(): number { return this.data.flow; }
public set flow(val: number) { this.setDataVal('flow', val); }
public get speed(): number { return this.data.speed; }
public set speed(val: number) { this.setDataVal('speed', val); }
public get relay(): number { return this.data.relay; }
public set relay(val: number) { this.setDataVal('relay', val); }
public get units(): number | any { return this.data.units; }
public set units(val: number | any) { this.setDataVal('units', sys.board.valueMaps.pumpUnits.encode(val)); }
public get maxPressure(): number { return this.data.maxPressure; }
public set maxPressure(val: number) { this.setDataVal('maxPressure', val); }
}
export class PumpRelayCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "relays"); }
public createItem(data: any): PumpRelay { return new PumpRelay(data); }
}
export class PumpRelay extends EqItem {
public dataName = 'pumpCircuitConfig';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get connectionId(): string { return this.data.connectionId; }
public set connectionId(val: string) { this.setDataVal('connectionId', val); }
public get deviceBinding(): string { return this.data.deviceBinding; }
public set deviceBinding(val: string) { this.setDataVal('deviceBinding', val); }
}
export class ChlorinatorCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "chlorinators"); }
public createItem(data: any): Chlorinator { return new Chlorinator(data); }
public filter(f: (value: Chlorinator, index?: any, array?: any[]) => boolean): ChlorinatorCollection {
return new ChlorinatorCollection({ chlorinators: this.data.filter(f) });
}
public getByBody(body: number): EqItemCollection {
return this.filter(elem => elem.body === body ||
body === 32 && elem.body <= 2 ||
elem.body === 32 && body <= 2);
}
public getItemByPortId(portId: number, add?: boolean, data?: any): Chlorinator {
let itm = this.find(elem => { return typeof (elem as { portId?}).portId !== 'undefined' && (elem as { portId?}).portId === portId });
if (typeof itm !== 'undefined') return itm;
if (typeof add !== 'undefined' && add) return this.add(extend(true, { portId: portId }, data));
return this.createItem(extend(true, data, { portId: portId }));
}
public findItemByPortId(portId: number) { return this.find(elem => { return typeof (elem as { portId?}).portId !== 'undefined' && (elem as { portId?}).portId === portId }); }
}
export class Chlorinator extends EqItem {
public dataName = 'chlorinatorConfig';
public initData() {
if (typeof this.data.disabled === 'undefined') this.data.disabled = false;
if (typeof this.data.ignoreSaltReading === 'undefined') this.data.ignoreSaltReading = false;
if (typeof this.data.isVirtual !== 'undefined') delete this.data.isVirtual;
if (typeof this.data.portId === 'undefined') this.data.portId = 0;
if (typeof this.data.saltTarget === 'undefined') this.data.saltTarget = 3400;
}
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get portId(): number { return this.data.portId; }
public set portId(val: number) { this.setDataVal('portId', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.chlorinatorType.encode(val)); }
public get body(): number | any { return this.data.body; }
public set body(val: number | any) { this.setDataVal('body', sys.board.valueMaps.bodies.encode(val)); }
public get poolSetpoint(): number { return this.data.poolSetpoint; }
public set poolSetpoint(val: number) { this.setDataVal('poolSetpoint', val); }
public get spaSetpoint(): number { return this.data.spaSetpoint; }
public set spaSetpoint(val: number) { this.setDataVal('spaSetpoint', val); }
public get superChlorHours(): number { return this.data.superChlorHours; }
public set superChlorHours(val: number) { this.setDataVal('superChlorHours', val); }
public get saltTarget(): number { return this.data.saltTarget; }
public set saltTarget(val: number) {
if (this.data.saltTarget !== val) {
this.setDataVal('saltTarget', val);
let cstate = state.chlorinators.getItemById(this.id, true);
cstate.calcSaltRequired(this.saltTarget);
}
}
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get address(): number { return this.data.address; }
public set address(val: number) { this.setDataVal('address', val); }
public get superChlor(): boolean { return this.data.superChlor; }
public set superChlor(val: boolean) { this.setDataVal('superChlor', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
//public get isVirtual() { return this.data.isVirtual; }
//public set isVirtual(val: boolean) { this.setDataVal('isVirtual', val); }
public get disabled(): boolean { return this.data.disabled; }
public set disabled(val: boolean) { this.setDataVal('disabled', val); }
public get isDosing(): boolean { return this.data.isDosing; }
public set isDosing(val: boolean) { this.setDataVal('isDosing', val); }
public get ignoreSaltReading() { return this.data.ignoreSaltReading; }
public set ignoreSaltReading(val: boolean) { this.setDataVal('ignoreSaltReading', val); }
public get model() { return this.data.model; }
public set model(val: number | any) { this.setDataVal('model', sys.board.valueMaps.chlorinatorModel.encode(val)); }
public get ratedLbs(): number {
let model = sys.board.valueMaps.chlorinatorModel.get(this.model);
return typeof model.chlorinePerSec !== 'undefined' ? model.chlorinePerSec : 0;
}
}
export class ValveCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "valves"); }
public createItem(data: any): Valve { return new Valve(data); }
public getIntake(): Valve[] {
let valves = this.data.filter(x => x.isIntake === true);
let ret = [];
for (let i = 0; i < valves.length; i++) {
ret.push(this.getItemById(valves[i].id));
}
return ret;
}
public getReturn(): Valve[] {
let valves = this.data.filter(x => x.isReturn === true);
let ret = [];
for (let i = 0; i < valves.length; i++) {
ret.push(this.getItemById(valves[i].id));
}
return ret;
}
}
export class Valve extends EqItem {
public dataName = 'valveConfig';
public initData() {
if (typeof this.data.type === 'undefined') this.data.type = 0;
}
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.valveTypes.encode(val)); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get circuit(): number { return this.data.circuit; }
public set circuit(val: number) { this.setDataVal('circuit', val); }
public get isIntake(): boolean { return utils.makeBool(this.data.isIntake); }
public set isIntake(val: boolean) { this.setDataVal('isIntake', val); }
public get isReturn(): boolean { return utils.makeBool(this.data.isReturn); }
public set isReturn(val: boolean) { this.setDataVal('isReturn', val); }
// public get isVirtual(): boolean { return utils.makeBool(this.data.isVirtual); }
// public set isVirtual(val: boolean) { this.setDataVal('isVirtual', val); }
public get pinId(): number { return this.data.pinId || 0; }
public set pinId(val: number) { this.setDataVal('pinId', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get connectionId(): string { return this.data.connectionId; }
public set connectionId(val: string) { this.setDataVal('connectionId', val); }
public get deviceBinding(): string { return this.data.deviceBinding; }
public set deviceBinding(val: string) { this.setDataVal('deviceBinding', val); }
}
export class HeaterCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "heaters"); }
public createItem(data: any): Heater { return new Heater(data); }
public getItemByAddress(address: number, add?: boolean, data?: any): Heater {
let itm = this.find(elem => elem.address === address && typeof elem.address !== 'undefined');
if (typeof itm !== 'undefined') return itm;
if (typeof add !== 'undefined' && add) return this.add(data || { id: this.data.length + 1, address: address });
return this.createItem(data || { id: this.data.length + 1, address: address });
}
public filter(f: (value: Heater, index?: any, array?: any[]) => boolean): HeaterCollection {
return new HeaterCollection({ heaters: this.data.filter(f) });
}
public getSolarHeaters(bodyId?: number): EqItemCollection {
let htype = sys.board.valueMaps.heaterTypes.getValue('solar');
return new HeaterCollection(this.data.filter(x => {
if (x.type === htype) {
if (typeof bodyId !== 'undefined') {
if (!x.isActive) return false;
return (bodyId === x.body || (sys.equipment.shared && x.body === 32)) ? true : false;
}
}
return false;
}));
}
}
export class Heater extends EqItem {
public dataName = 'heaterConfig';
public initData() {
if (typeof this.data.isActive === 'undefined') this.data.isActive = true;
if (typeof this.data.portId === 'undefined') this.data.portId = 0;
if (typeof this.data.address === 'undefined' || this.data.address === 0) {
let htype = sys.board.valueMaps.heaterTypes.transform(this.data.type);
if (htype.hasAddress && htype.defaultAddress) this.data.address = htype.defaultAddress;
}
}
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get portId(): number { return this.data.portId; }
public set portId(val: number) { this.setDataVal('portId', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.heaterTypes.encode(val)); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get body(): number | any { return this.data.body; }
public set body(val: number | any) { this.setDataVal('body', sys.board.valueMaps.bodies.encode(val)); }
public get maxBoostTemp(): number { return this.data.maxBoostTemp; }
public set maxBoostTemp(val: number) { this.setDataVal('maxBoostTemp', val); }
public get startTempDelta(): number { return this.data.startTempDelta; }
public set startTempDelta(val: number) { this.setDataVal('startTempDelta', val); }
public get stopTempDelta(): number { return this.data.stopTempDelta; }
public set stopTempDelta(val: number) { this.setDataVal('stopTempDelta', val); }
public get cooldownDelay(): number { return this.data.cooldownDelay; }
public set cooldownDelay(val: number) { this.setDataVal('cooldownDelay', val); }
public get address(): number { return this.data.address; }
public set address(val: number) { this.setDataVal('address', val); }
public get efficiencyMode(): number { return this.data.efficiencyMode; }
public set efficiencyMode(val: number) { this.setDataVal('efficiencyMode', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get coolingEnabled(): boolean { return this.data.coolingEnabled; }
public set coolingEnabled(val: boolean) { this.setDataVal('coolingEnabled', val); }
public get heatingEnabled(): boolean { return this.data.heatingEnabled; }
public set heatingEnabled(val: boolean) { this.setDataVal('heatingEnabled', val); }
public get differentialTemp(): number { return this.data.differentialTemp; }
public set differentialTemp(val: number) { this.setDataVal('differentialTemp', val); }
public get freeze(): boolean { return this.data.freeze; }
public set freeze(val: boolean) { this.setDataVal('freeze', val); }
public get economyTime(): number { return this.data.economyTime; }
public set economyTime(val: number) { this.setDataVal('economyTime', val); }
public get connectionId(): string { return this.data.connectionId; }
public set connectionId(val: string) { this.setDataVal('connectionId', val); }
public get minCycleTime(): number { return this.data.minCycleTime; }
public set minCycleTime(val: number) { this.setDataVal('minCycleTime', val); }
public get deviceBinding(): string { return this.data.deviceBinding; }
public set deviceBinding(val: string) { this.setDataVal('deviceBinding', val); }
}
export class CoverCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "covers"); }
public createItem(data: any): Cover {
if (typeof data.circuits === 'undefined') data.circuits = [];
return new Cover(data);
}
}
export class Cover extends EqItem {
public dataName = 'coverConfig';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get body(): number | any { return this.data.body; }
public set body(val: number | any) { this.setDataVal('body', sys.board.valueMaps.bodies.encode(val)); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get normallyOn(): boolean { return this.data.normallyOn; }
public set normallyOn(val: boolean) { this.setDataVal('normallyOn', val); }
public get circuits(): number[] { return this.data.circuits; }
public set circuits(val: number[]) { this.setDataVal('circuits', val); }
// ISSUE-075 / ISSUE-080: IntelliChlor Active is cat=14 flags bit 0 (OCP cover-menu
// "IntelliChlor Active" toggle — switch the chlorinator off while this cover is closed).
public get chlorActive(): boolean { return this.data.chlorActive; }
public set chlorActive(val: boolean) { this.setDataVal('chlorActive', val); }
// ISSUE-075 / ISSUE-080: IntelliChlor Output is a numeric % (0-50 for Pool body, 0-10 for Spa body).
// It does NOT live in the cat=14 payload; it is supplied by the A30 cat=7 chlorinator packet at
// slot-0 offset 7 (Pool) / offset 8 (Spa), and by A168 cat=7 bytes 11/12 for live-edit piggyback.
// See .plan/v3.008/covers-packet-reference.md §4.2.
public get chlorOutput(): number { return this.data.chlorOutput; }
public set chlorOutput(val: number) { this.setDataVal('chlorOutput', val); }
}
export interface ICircuitGroup {
id: number;
type: number;
name: string;
nameId?: number;
eggTimer: number;
dontStop: boolean;
isActive: boolean;
lightingTheme?: number;
showInFeatures?: boolean;
circuits: EqItemCollection;
master: number;
get(copy?: boolean);
}
export interface ICircuitGroupCircuit {
id: number,
circuit: number
}
export class LightGroupCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "lightGroups"); }
public createItem(data: any): LightGroup { return new LightGroup(data); }
}
export class LightGroupCircuitCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || 'circuits'); }
public createItem(data: any): LightGroupCircuit { return new LightGroupCircuit(data); }
public getItemByCircuitId(circuitId: number, add?: boolean, data?: any) {
for (let i = 0; i < this.data.length; i++)
if (typeof this.data[i].circuit !== 'undefined' && this.data[i].circuit === circuitId) {
return this.createItem(this.data[i]);
}
if (typeof add !== 'undefined' && add)
return this.add(data || { id: this.data.length + 1, circuit: circuitId, position: this.data.length, color: 0, swimDelay: 1 });
return this.createItem(data || { id: this.data.length + 1, circuit: circuitId, position: this.data.length, color: 0, swimDelay: 1 });
}
public removeItemByCircuitId(id: number): LightGroupCircuit {
let rem: LightGroupCircuit = null;
for (let i = this.data.length - 1; i >= 0; i--)
if (typeof this.data[i].circuit !== 'undefined' && this.data[i].circuit === id) {
rem = this.data.splice(i, 1);
}
sys._hasChanged = true;
return rem;
}
// public sortByPosition() { sys.intellibrite.circuits.sort((a, b) => { return a.position > b.position ? 1 : -1; }); }
}
export class LightGroupCircuit extends EqItem implements ICircuitGroupCircuit {
public dataName = 'lightGroupCircuitConfig';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get circuit(): number { return this.data.circuit; }
public set circuit(val: number) { this.setDataVal('circuit', val); }
// RG - these shouldn't be here; only need them for CircuitGroupCircuit but if I remove it getItemById returns an error... to be fixed.
// RKS: 09-26-20 Deprecated the desiredStateOn property. There are 3 values for this and they include on-off-ignore. For non-IC boards these resolve to 0 or 1 for on/off.
//public get desiredStateOn(): boolean { return this.data.desiredStateOn; }
//public set desiredStateOn(val: boolean) { this.setDataVal('desiredStateOn', val); }
public get desiredState(): number { return this.data.desiredState; }
public set desiredState(val: number) { this.setDataVal('desiredState', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val, false); }
public get lightingTheme(): number | any { return this.data.lightingTheme; }
public set lightingTheme(val: number | any) { this.setDataVal('lightingTheme', sys.board.valueMaps.lightThemes.encode(val)); }
public get position(): number { return this.data.position; }
public set position(val: number) { this.setDataVal('position', val); }
public get color(): number | any { return this.data.color; }
public set color(val: number | any) { this.setDataVal('color', sys.board.valueMaps.lightColors.encode(val)); }
public get swimDelay(): number { return this.data.swimDelay; }
public set swimDelay(val: number) { this.setDataVal('swimDelay', val); }
public getExtended() {
let circ = this.get(true);
circ.circuit = state.circuits.getInterfaceById(this.circuit).get(true);
circ.lightingTheme = undefined;
circ.swimDelay = this.swimDelay;
circ.position = this.position;
circ.color = sys.board.valueMaps.lightColors.transform(this.color);
return circ;
}
}
export class LightGroup extends EqItem implements ICircuitGroup, ICircuit {
constructor(data, name?: string, obj?: any) {
super(data, name);
if (typeof obj !== 'undefined') extend(true, this.data, obj);
}
public dataName = 'lightGroupConfig';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get nameId(): number { return this.data.nameId; }
public set nameId(val: number) { this.setDataVal('nameId', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.circuitGroupTypes.encode(val)); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get eggTimer(): number { return this.data.eggTimer; }
public set eggTimer(val: number) { this.setDataVal('eggTimer', val); }
public get dontStop(): boolean { return utils.makeBool(this.data.dontStop); }
public set dontStop(val: boolean) { this.setDataVal('dontStop', val); }
public get showInFeatures(): boolean { return utils.makeBool(this.data.showInFeatures); }
public set showInFeatures(val: boolean) { this.setDataVal('showInFeatures', val); }
public get lightingTheme(): number | any { return this.data.lightingTheme; }
public set lightingTheme(val: number | any) { this.setDataVal('lightingTheme', sys.board.valueMaps.lightThemes.encode(val)); }
public get circuits(): LightGroupCircuitCollection { return new LightGroupCircuitCollection(this.data, "circuits"); }
public getLightThemes() {
// Go through the circuits and gather the themes.
// This method first looks at the circuits to determine their type (function)
// then it filters the list by the types associated with the circuits. It does this because
// there can be combined ColorLogic and IntelliBrite lights. The themes array has
// the circuit function.
let arrThemes = [];
for (let i = 0; i < this.circuits.length; i++) {
let circ = this.circuits.getItemByIndex(i);
let c = sys.circuits.getInterfaceById(circ.circuit);
let cf = sys.board.valueMaps.circuitFunctions.transform(c.type);
if (cf.isLight && typeof cf.theme !== 'undefined') {
if (!arrThemes.includes(cf.theme)) arrThemes.push(cf.theme);
}
}
// Alright now we need to get a listing of the themes.
let t = sys.board.valueMaps.lightThemes.toArray();
let ret = [];
for (let i = 0; i < t.length; i++) {
let thm = t[i];
if (typeof thm.types !== 'undefined' && thm.types.length > 0) {
// Look in the themes array of the theme.
if (arrThemes.some(x => thm.types.includes(x))) ret.push(thm);
}
}
return ret;
}
public getLightCommands() {
// Go through the circuits and gather the themes.
// This method first looks at the circuits to determine their type (function)
// then it filters the list by the types associated with the circuits. It does this because
// there can be combined ColorLogic and IntelliBrite lights. The themes array has
// the circuit function.
let arrThemes = [];
for (let i = 0; i < this.circuits.length; i++) {
let circ = this.circuits.getItemByIndex(i);
let c = sys.circuits.getInterfaceById(circ.circuit);
let cf = sys.board.valueMaps.circuitFunctions.transform(c.type);
if (cf.isLight && typeof cf.theme !== 'undefined') {
if (!arrThemes.includes(cf.theme)) arrThemes.push(cf.theme);
}
}
// Alright now we need to get a listing of the themes.
let t = sys.board.valueMaps.lightGroupCommands.toArray();
let ret = [];
for (let i = 0; i < t.length; i++) {
let cmd = t[i];
if (typeof cmd.types !== 'undefined' && cmd.types.length > 0) {
// Look in the themes array of the theme.
if (arrThemes.some(x => cmd.types.includes(x))) ret.push(cmd);
}
}
return ret;
}
public getExtended() {
let group = this.get(true);
group.type = sys.board.valueMaps.circuitGroupTypes.transform(group.type);
group.lightingTheme = sys.board.valueMaps.lightThemes.transform(group.lightingTheme || 0);
let gstate = state.lightGroups.getItemById(this.id).getExtended();
group.action = gstate.action;
group.isOn = gstate.isOn;
group.circuits = [];
for (let i = 0; i < this.circuits.length; i++) {
group.circuits.push(this.circuits.getItemByIndex(i).getExtended());
}
return group;
}
}
export class CircuitGroupCircuitCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || 'circuits'); }
public createItem(data: any): CircuitGroupCircuit { return new CircuitGroupCircuit(data); }
}
export class CircuitGroupCircuit extends EqItem implements ICircuitGroupCircuit {
public dataName = 'circuitGroupCircuitConfig';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get circuit(): number { return this.data.circuit; }
public set circuit(val: number) { this.setDataVal('circuit', val); }
// RKS: 09-26-20 Deprecated the desiredStateOn property. The values are mapped to on-off-ignore in IC and on-off in all other boards.
//public get desiredStateOn(): boolean { return this.data.desiredStateOn; }
//public set desiredStateOn(val: boolean) { this.setDataVal('desiredStateOn', val); }
public get desiredState(): number { return this.data.desiredState; }
public set desiredState(val: number) { this.setDataVal('desiredState', val); }
//public get lightingTheme(): number { return this.data.lightingTheme; }
//public set lightingTheme(val: number) { this.setDataVal('lightingTheme', val); }
public getExtended() {
let circ = this.get(true);
circ.circuit = state.circuits.getInterfaceById(circ.circuit);
return circ;
}
}
export class CircuitGroupCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "circuitGroups"); }
public createItem(data: any): CircuitGroup { return new CircuitGroup(data); }
public getInterfaceById(id: number): ICircuitGroup {
let iGroup: ICircuitGroup = this.getItemById(id, false, { id: id, isActive: false });
if (!iGroup.isActive) iGroup = sys.lightGroups.getItemById(id, false, { id: id, isActive: false });
return iGroup;
}
public getItemById(id: number, add?: boolean, data?: any): LightGroup | CircuitGroup {
for (let i = 0; i < this.data.length; i++) {
if (typeof this.data[i].id !== 'undefined' && this.data[i].id === id) {
return this.createItem(this.data[i]);
}
}
if (typeof add !== 'undefined' && add)
return this.add(data || { id: id });
// RKS: This finds its way to the light groups if we aren't adding a circuit group so calling getItemById will first
// look for the group in circuit groups then look for it in the light groups.
return sys.lightGroups.getItemById(id, add, data);
}
}
export class CircuitGroup extends EqItem implements ICircuitGroup, ICircuit {
public dataName = 'circuitGroupConfig';
public initData() {
if (typeof this.data.showInFeatures === 'undefined') this.data.showInFeatures = true;
}
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get nameId(): number { return this.data.nameId; }
public set nameId(val: number) { this.setDataVal('nameId', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.circuitGroupTypes.encode(val)); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get eggTimer(): number { return this.data.eggTimer; }
public set eggTimer(val: number) { this.setDataVal('eggTimer', val); }
public get dontStop(): boolean { return utils.makeBool(this.data.dontStop); }
public set dontStop(val: boolean) { this.setDataVal('dontStop', val); }
public get showInFeatures(): boolean { return utils.makeBool(this.data.showInFeatures); }
public set showInFeatures(val: boolean) { this.setDataVal('showInFeatures', val); }
public get circuits(): CircuitGroupCircuitCollection { return new CircuitGroupCircuitCollection(this.data, "circuits"); }
public getExtended() {
let group = this.get(true);
group.type = sys.board.valueMaps.circuitGroupTypes.transform(group.type);
// group.lightingTheme = sys.board.valueMaps.lightThemes.transform(group.lightingTheme || 0);
let gstate = state.lightGroups.getItemById(this.id).getExtended();
group.action = gstate.action;
group.isOn = gstate.isOn;
group.showInFeatures = this.showInFeatures;
group.circuits = [];
for (let i = 0; i < this.circuits.length; i++) {
group.circuits.push(this.circuits.getItemByIndex(i).getExtended());
}
return group;
}
}
export class RemoteCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "remotes"); }
public createItem(data: any): Remote { return new Remote(data); }
}
export class Remote extends EqItem {
public dataName = 'remoteConfig';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.remoteTypes.encode(val)); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get hardwired(): boolean { return this.data.hardwired; }
public set hardwired(val: boolean) { this.setDataVal('hardwired', val); }
public get body(): number { return this.data.body; }
public set body(val: number) { this.setDataVal('body', val); }
public get pumpId(): number { return this.data.pumpId; }
public set pumpId(val: number) { this.setDataVal('pumpId', val); }
public get address(): number { return this.data.address; }
public set address(val: number) { this.setDataVal('address', val); }
public get button1(): number { return this.data.button1; }
public set button1(val: number) { this.setDataVal('button1', val); }
public get button2(): number { return this.data.button2; }
public set button2(val: number) { this.setDataVal('button2', val); }
public get button3(): number { return this.data.button3; }
public set button3(val: number) { this.setDataVal('button3', val); }
public get button4(): number { return this.data.button4; }
public set button4(val: number) { this.setDataVal('button4', val); }
public get button5(): number { return this.data.button5; }
public set button5(val: number) { this.setDataVal('button5', val); }
public get button6(): number { return this.data.button6; }
public set button6(val: number) { this.setDataVal('button6', val); }
public get button7(): number { return this.data.button7; }
public set button7(val: number) { this.setDataVal('button7', val); }
public get button8(): number { return this.data.button8; }
public set button8(val: number) { this.setDataVal('button8', val); }
public get button9(): number { return this.data.button9; }
public set button9(val: number) { this.setDataVal('button9', val); }
public get button10(): number { return this.data.button10; }
public set button10(val: number) { this.setDataVal('button10', val); }
public set stepSize(val: number) { this.setDataVal('stepSize', val); }
public get stepSize(): number { return this.data.stepSize; }
}
export class SecurityRoleCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "roles"); }
public createItem(data: any): SecurityRole { return new SecurityRole(data); }
}
export class SecurityRole extends EqItem {
public dataName = 'roleConfig';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get timeout(): number { return this.data.timeout; }
public set timeout(val: number) { this.setDataVal('timeout', val); }
public get flag1(): number { return this.data.flag1; }
public set flag1(val: number) { this.setDataVal('flag1', val); }
public get flag2(): number { return this.data.flag2; }
public set flag2(val: number) { this.setDataVal('flag2', val); }
public get pin(): string { return this.data.pin; }
public set pin(val: string) { this.setDataVal('pin', val); }
public get permissionsMask(): number { return this.data.permissionsMask; }
public set permissionsMask(val: number) { this.setDataVal('permissionsMask', val); }
public get permissionsBytes(): number[] { return Array.isArray(this.data.permissionsBytes) ? this.data.permissionsBytes : []; }
public set permissionsBytes(val: number[]) { this.setDataVal('permissionsBytes', Array.isArray(val) ? [...val] : []); }
}
export class Security extends EqItem {
public dataName = 'securityConfig';
public initData() {
if (typeof this.data.enabled === 'undefined') this.data.enabled = false;
if (typeof this.data.guestEnabled === 'undefined') this.data.guestEnabled = false;
if (typeof this.data.enabledByte === 'undefined') this.data.enabledByte = 0;
}
public get enabled(): boolean { return this.data.enabled; }
public set enabled(val: boolean) { this.setDataVal('enabled', val); }
public get guestEnabled(): boolean { return this.data.guestEnabled; }
public set guestEnabled(val: boolean) { this.setDataVal('guestEnabled', val); }
public get enabledByte(): number { return this.data.enabledByte; }
public set enabledByte(val: number) { this.setDataVal('enabledByte', val); }
public get roles(): SecurityRoleCollection { return new SecurityRoleCollection(this.data, "roles"); }
}
export class Alerts extends EqItem {
public dataName = 'alertsConfig';
public initData() {
if (typeof this.data.circuitNotifications === 'undefined') this.data.circuitNotifications = 0;
if (typeof this.data.pumpNotifications === 'undefined') this.data.pumpNotifications = 0;
if (typeof this.data.heaterNotifications !== 'undefined') {
if (typeof this.data.ultratempNotifications === 'undefined') this.data.ultratempNotifications = this.data.heaterNotifications;
delete this.data.heaterNotifications;
}
if (typeof this.data.ultratempNotifications === 'undefined') this.data.ultratempNotifications = 0;
if (typeof this.data.chlorinatorNotifications === 'undefined') this.data.chlorinatorNotifications = 0;
if (typeof this.data.intellichemNotifications === 'undefined') this.data.intellichemNotifications = 0;
if (typeof this.data.hybridNotifications === 'undefined') this.data.hybridNotifications = 0;
if (typeof this.data.connectedGasNotifications === 'undefined') this.data.connectedGasNotifications = 0;
if (typeof this.data.raw !== 'object' || this.data.raw === null) this.data.raw = {};
}
public get circuitNotifications(): number { return this.data.circuitNotifications; }
public set circuitNotifications(val: number) { this.setDataVal('circuitNotifications', val); this.applyVisibilityToMessages(); }
public get pumpNotifications(): number { return this.data.pumpNotifications; }
public set pumpNotifications(val: number) { this.setDataVal('pumpNotifications', val); this.applyVisibilityToMessages(); }
public get ultratempNotifications(): number { return this.data.ultratempNotifications; }
public set ultratempNotifications(val: number) { this.setDataVal('ultratempNotifications', val); this.applyVisibilityToMessages(); }
public get chlorinatorNotifications(): number { return this.data.chlorinatorNotifications; }
public set chlorinatorNotifications(val: number) { this.setDataVal('chlorinatorNotifications', val); this.applyVisibilityToMessages(); }
public get intellichemNotifications(): number { return this.data.intellichemNotifications; }
public set intellichemNotifications(val: number) { this.setDataVal('intellichemNotifications', val); this.applyVisibilityToMessages(); }
public get hybridNotifications(): number { return this.data.hybridNotifications; }
public set hybridNotifications(val: number) { this.setDataVal('hybridNotifications', val); this.applyVisibilityToMessages(); }
public get connectedGasNotifications(): number { return this.data.connectedGasNotifications; }
public set connectedGasNotifications(val: number) { this.setDataVal('connectedGasNotifications', val); this.applyVisibilityToMessages(); }
// Sweep state.equipment.messages and remove A204-sourced alerts whose category/bit is
// now disabled in sys.alerts. Out-of-scope codes (Nixie/RS-485 IntelliChem, etc.) are
// left alone. Called from each notification setter so toggling the dashPanel pref
// immediately removes hidden alerts; newly-enabled alerts re-appear on the next A204.
public applyVisibilityToMessages() {
try {
// Filtering applies only to IC v3 (sys.alerts is populated by IC v3 piggyback decode).
if (!sys.equipment.isIntellicenterV3) return;
const messages = state.equipment.messages;
const all = messages.get(true) as any[];
if (!Array.isArray(all) || all.length === 0) return;
for (let i = 0; i < all.length; i++) {
const code = all[i] && all[i].code;
if (typeof code !== 'string') continue;
const vis = EquipmentStateMessage.resolveA204AlertVisibility(code);
if (!vis) continue;
if (!EquipmentStateMessage.isA204AlertEnabled(vis.category, vis.bit)) {
messages.removeItemByCode(code);
}
}
} catch (err) {
// Defensive: never let visibility sweeping break a config write.
}
}
public setRaw(selector: number, raw: number[]) {
if (typeof this.data.raw !== 'object' || this.data.raw === null) this.data.raw = {};
this.data.raw[`selector${selector}`] = Array.isArray(raw) ? [...raw] : [];
this.hasChanged = true;
}
}
export class ChemControllerCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "chemControllers"); }
public createItem(data: any): ChemController { return new ChemController(data); }
public getItemByAddress(address: number, add?: boolean, data?: any): ChemController {
let itm = this.find(elem => elem.address === address && typeof elem.address !== 'undefined');
if (typeof itm !== 'undefined') return itm;
if (typeof add !== 'undefined' && add) return this.add(data || { id: this.data.length + 1, address: address });
return this.createItem(data || { id: this.data.length + 1, address: address });
}
//public nextAvailableChemController(): number {
// for (let i = 1; i <= sys.equipment.maxChemControllers; i++) {
// let chem = sys.chemControllers.getItemById(i);
// if (!chem.isActive) return i;
// }
// return undefined;
//}
public getNextControllerId(type: number): number {
let arr = this.toArray();
let id = type === 2 ? 0 : 49;
for (let i = 0; i < arr.length; i++) {
if (arr[i].type === type) id = Math.max(id, arr[i].id);
}
return id + 1;
}
}
export class ChemDoserCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "chemDosers"); }
public createItem(data: any): ChemDoser { return new ChemDoser(data); }
}
export class FlowSensor extends ChildEqItem {
public dataName = 'flowSensorConfig';
public initData() {
if (typeof this.data.type === 'undefined') this.data.type = 0;
if (typeof this.data.enabled === 'undefined') this.data.enabled = true;
super.initData();
}
public get enabled(): boolean { return utils.makeBool(this.data.enabled); }
public set enabled(val: boolean) { this.setDataVal('enabled', val); }
public get connectionId(): string { return this.data.connectionId; }
public set connectionId(val: string) { this.setDataVal('connectionId', val); }
public get deviceBinding(): string { return this.data.deviceBinding; }
public set deviceBinding(val: string) { this.setDataVal('deviceBinding', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.chemPumpTypes.encode(val)); }
public getExtended() {
let sensor = this.get(true);
sensor.type = sys.board.valueMaps.flowSensorTypes.transform(this.type);
return sensor;
}
}
export interface IChemController {
}
export class ChemController extends EqItem implements IChemController {
public initData() {
if (typeof this.data.lsiRange === 'undefined') this.data.lsiRange = { low: -.5, high: .5, enabled: true };
if (typeof this.data.borates === 'undefined') this.data.borates = 0;
if (typeof this.data.siCalcType === 'undefined') this.data.siCalcType = 0;
if (typeof this.data.intellichemStandalone === 'undefined') this.data.intellichemStandalone = false;
if (typeof this.data.singleMixPeriod === 'undefined') this.data.singleMixPeriod = false;
super.initData();
}
public dataName = 'chemControllerConfig';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.chemControllerTypes.encode(val)); }
public get body(): number | any { return this.data.body; }
public set body(val: number | any) { this.setDataVal('body', sys.board.valueMaps.bodies.encode(val)); }
public get address(): number { return this.data.address; }
public set address(val: number) { this.setDataVal('address', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get intellichemStandalone(): boolean { return utils.makeBool(this.data.intellichemStandalone); }
public set intellichemStandalone(val: boolean) { this.setDataVal('intellichemStandalone', val); }
public get calciumHardness(): number { return this.data.calciumHardness; }
public set calciumHardness(val: number) { this.setDataVal('calciumHardness', val); }
public get cyanuricAcid(): number { return this.data.cyanuricAcid; }
public set cyanuricAcid(val: number) { this.setDataVal('cyanuricAcid', val); }
public get alkalinity(): number { return this.data.alkalinity; }
public set alkalinity(val: number) { this.setDataVal('alkalinity', val); }
public get borates(): number { return this.data.borates; }
public set borates(val: number) { this.setDataVal('borates', val); }
public get flowSensor(): ChemFlowSensor { return new ChemFlowSensor(this.data, 'flowSensor', this); }
public get HMIAdvancedDisplay(): boolean { return this.data.HMIAdvancedDisplay; }
public set HMIAdvancedDisplay(val: boolean) { this.setDataVal('HMIAdvancedDisplay', val); }
public get siCalcType(): number | any { return this.data.siCalcType; }
public set siCalcType(val: number | any) { this.setDataVal('siCalcType', sys.board.valueMaps.siCalcTypes.encode(val)); }
public get ph(): ChemicalPh { return new ChemicalPh(this.data, 'ph', this); }
public get orp(): ChemicalORP { return new ChemicalORP(this.data, 'orp', this); }
public get lsiRange(): AlarmSetting { return new AlarmSetting(this.data, 'lsiRange', this); }
public get firmware(): string { return this.data.firmware; }
public set firmware(val: string) { this.setDataVal('firmware', val); }
public get singleMixPeriod(): boolean { return this.data.singleMixPeriod; }
public set singleMixPeriod(val: boolean) { this.setDataVal('singleMixPeriod', val); }
public getExtended() {
let chem = this.get(true);
chem.type = sys.board.valueMaps.chemControllerTypes.transform(this.type);
chem.siCalcType = sys.board.valueMaps.siCalcTypes.transform(this.siCalcType || 0);
chem.body = sys.board.valueMaps.bodies.transform(this.body);
chem.ph = this.ph.getExtended();
chem.orp = this.orp.getExtended();
chem.flowSensor = this.flowSensor.getExtended();
return chem;
}
// Chem controller alarms
// Controller
// 1. LSI Out of range.
//
// Chemical
// 1. Chemical out of range. -- Need to be able to set the range and the warning tolerance.
// Probe: Probe not reporting
// Tank: Tank Empty
// Dosing: Max Dose limit
// ORP
// 1. Chlorinator Comms Lost.
}
export class ChemDoser extends EqItem implements IChemical {
public dataName = 'chemDoserConfig';
public initData() {
if (typeof this.data.pump === 'undefined') this.data.pump = {};
if (typeof this.data.tank === 'undefined') this.data.tank = {};
if (typeof this.data.flowSensor === 'undefined') this.data.flowSensor = {};
if (typeof this.data.enabled === 'undefined') this.data.enabled = true;
if (typeof this.data.dosingMethod === 'undefined') this.data.dosingMethod = 0;
if (typeof this.data.startDelay === 'undefined') this.data.startDelay = 1.5;
if (typeof this.data.maxDailyVolume === 'undefined') this.data.maxDailyVolume = 500;
if (typeof this.data.disableOnFreeze === 'undefined') this.data.disableOnFreeze = true;
if (typeof this.data.disableChlorinator === 'undefined') this.data.disableChlorinator = true;
if (typeof this.mixingTime === 'undefined') this.data.mixingTime = 3600;
if (typeof this.data.setpoint === 'undefined') this.data.setpoint = 100;
if (typeof this.data.type === 'undefined') this.data.type = 0;
if (typeof this.data.singleMixPeriod === 'undefined') this.data.singleMixPeriod = false;
super.initData();
}
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get setpoint(): number { return this.data.setpoint; }
public set setpoint(val: number) { this.setDataVal('setpoint', val); }
public get body(): number | any { return this.data.body; }
public set body(val: number | any) { this.setDataVal('body', sys.board.valueMaps.bodies.encode(val)); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get chemType(): string { return this.data.chemType; }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) {
this.setDataVal('type', sys.board.valueMaps.chemDoserTypes.encode(val));
let t = sys.board.valueMaps.chemDoserTypes.findItem(val) || { val: 0, name: 'acid', desc: 'Acid' };
this.setDataVal('chemType', t.desc);
}
public get enabled(): boolean { return utils.makeBool(this.data.enabled); }
public set enabled(val: boolean) { this.setDataVal('enabled', val); }
public get disableChlorinator(): boolean { return utils.makeBool(this.data.disableChlorinator); }
public set disableChlorinator(val: boolean) { this.setDataVal('disableChlorinator', val); }
public get disableOnFreeze(): boolean { return utils.makeBool(this.data.disableOnFreeze); }
public set disableOnFreeze(val: boolean) { this.setDataVal('disableOnFreeze', val); }
public get dosingVolume(): number { return this.data.dosingVolume; }
public set dosingVolume(val: number) { this.setDataVal('dosingVolume', val); }
public get mixingTime(): number { return this.data.mixingTime; }
public set mixingTime(val: number) { this.setDataVal('mixingTime', val); }
public get maxDailyVolume(): number { return this.data.maxDailyVolume; }
public set maxDailyVolume(val: number) { this.setDataVal('maxDailyVolume', val); }
public get dosingMethod(): number | any { return this.data.dosingMethod; }
public set dosingMethod(val: number | any) { this.setDataVal('dosingMethod', sys.board.valueMaps.chemDosingMethods.encode(val)); }
public get startDelay(): number { return this.data.startDelay; }
public set startDelay(val: number) { this.setDataVal('startDelay', val); }
public get flowSensor(): ChemFlowSensor { return new ChemFlowSensor(this.data, 'flowSensor', this); }
public get flowOnlyMixing(): boolean { return utils.makeBool(this.data.flowOnlyMixing); }
public set flowOnlyMixing(val: boolean) { this.setDataVal('flowOnlyMixing', val); }
public get singleMixPeriod(): boolean { return this.data.singleMixPeriod; }
public set singleMixPeriod(val: boolean) { this.setDataVal('singleMixPeriod', val); }
public get pump(): ChemicalPump { return new ChemicalPump(this.data, 'pump', this); }
public get tank(): ChemicalTank { return new ChemicalTank(this.data, 'tank', this); }
public getExtended() {
let chem = this.get(true);
chem.tank = this.tank.getExtended();
chem.pump = this.pump.getExtended();
return chem;
}
}
export class ChemFlowSensor extends FlowSensor {
public dataName = 'flowSensorConfig';
public initData() {
super.initData();
}
public get minimumFlow(): number { return this.data.minimumFlow || 0; }
public set minimumFlow(val: number) { this.setDataVal('minimumFlow', val); }
public get minimumPressure(): number { return this.data.minimumPressure || 0; }
public set minimumPressure(val: number) { this.setDataVal('minimumPressure', val); }
public getExtended() {
let sensor = this.get(true);
sensor.type = sys.board.valueMaps.flowSensorTypes.transform(this.type);
return sensor;
}
}
export interface IChemical {
dosingMethod: number;
startDelay: number;
maxDosingTime?: number;
maxDosingVolume?: number;
}
export class Chemical extends ChildEqItem implements IChemical {
public dataName = 'chemicalConfig';
public initData() {
if (typeof this.data.pump === 'undefined') this.data.pump = {};
if (typeof this.data.tank === 'undefined') this.data.tank = {};
if (typeof this.data.enabled === 'undefined') this.data.enabled = true;
if (typeof this.data.dosingMethod === 'undefined') this.data.dosingMethod = 0;
if (typeof this.data.startDelay === 'undefined') this.data.startDelay = 1.5;
if (typeof this.data.flowReadingsOnly === 'undefined') this.data.flowReadingsOnly = true;
if (typeof this.data.flowOnlyMixing === 'undefined') this.data.flowOnlyMixing = true;
if (typeof this.data.maxDailyVolume === 'undefined') this.data.maxDailyVolume = 500;
if (typeof this.data.disableOnFreeze === 'undefined') this.data.disableOnFreeze = true;
super.initData();
}
public get chemType(): string { return this.data.chemType; }
public get enabled(): boolean { return utils.makeBool(this.data.enabled); }
public set enabled(val: boolean) { this.setDataVal('enabled', val); }
public get disableOnFreeze(): boolean { return utils.makeBool(this.data.disableOnFreeze); }
public set disableOnFreeze(val: boolean) { this.setDataVal('disableOnFreeze', val); }
public get maxDosingTime(): number { return this.data.maxDosingTime; }
public set maxDosingTime(val: number) { this.setDataVal('maxDosingTime', val); }
public get maxDosingVolume(): number { return this.data.maxDosingVolume; }
public set maxDosingVolume(val: number) { this.setDataVal('maxDosingVolume', val); }
public get maxDailyVolume(): number { return this.data.maxDailyVolume; }
public set maxDailyVolume(val: number) { this.setDataVal('maxDailyVolume', val); }
public get mixingTime(): number { return this.data.mixingTime; }
public set mixingTime(val: number) { this.setDataVal('mixingTime', val); }
public get flowOnlyMixing(): boolean { return utils.makeBool(this.data.flowOnlyMixing); }
public set flowOnlyMixing(val: boolean) { this.setDataVal('flowOnlyMixing', val); }
public get dosingMethod(): number | any { return this.data.dosingMethod; }
public set dosingMethod(val: number | any) { this.setDataVal('dosingMethod', sys.board.valueMaps.chemDosingMethods.encode(val)); }
public get startDelay(): number { return this.data.startDelay; }
public set startDelay(val: number) { this.setDataVal('startDelay', val); }
public get pump(): ChemicalPump { return new ChemicalPump(this.data, 'pump', this); }
public get tank(): ChemicalTank { return new ChemicalTank(this.data, 'tank', this); }
// public get chlor(): Chlorinator { return new ChemicalChlor(this.data, 'chlor', this); }
public get setpoint(): number { return this.data.setpoint; }
public set setpoint(val: number) { this.setDataVal('setpoint', val); }
public get tolerance(): AlarmSetting { return new AlarmSetting(this.data, 'tolerance', this); }
public getExtended() {
let chem = this.get(true);
chem.tank = this.tank.getExtended();
chem.pump = this.pump.getExtended();
chem.dosingMethod = sys.board.valueMaps.chemDosingMethods.transform(this.dosingMethod);
return chem;
}
public get flowReadingsOnly(): boolean { return utils.makeBool(this.data.flowReadingsOnly); }
public set flowReadingsOnly(val: boolean) { this.setDataVal('flowReadingsOnly', val); }
}
export class ChemicalPh extends Chemical {
public initData() {
this.data.chemType = 'ph';
if (typeof this.data.setpoint === 'undefined') this.data.setpoint = 7.2;
if (typeof this.data.phSupply === 'undefined') this.data.phSupply = 1;
if (typeof this.data.probe === 'undefined') this.data.probe = {};
if (typeof this.data.acidType === 'undefined') this.data.acidType = 0;
if (typeof this.data.tolerance === 'undefined') this.data.tolerance = { low: 7.2, high: 7.6, enabled: true };
if (typeof this.data.dosePriority === 'undefined') this.data.dosePriority = true;
if (typeof this.data.doserType === 'undefined') this.data.doserType = 0;
super.initData();
}
public get phSupply(): number | any { return this.data.phSupply; }
public set phSupply(val: number | any) { this.setDataVal('phSupply', sys.board.valueMaps.phSupplyTypes.encode(val)); }
public get acidType(): number | any { return this.data.acidType; }
public set acidType(val: number | any) { this.setDataVal('acidType', sys.board.valueMaps.acidTypes.encode(val)); }
public get dosePriority(): boolean { return this.data.dosePriority; }
public set dosePriority(val: boolean) { this.setDataVal('dosePriority', val); }
public get doserType(): number | any { return this.data.doserType; }
public set doserType(val: number | any) { this.setDataVal('doserType', sys.board.valueMaps.phDoserTypes.encode(val)); }
public get probe(): ChemicalPhProbe { return new ChemicalPhProbe(this.data, 'probe', this); }
public getExtended() {
let chem = super.getExtended();
chem.probe = this.probe.getExtended();
chem.tank = this.tank.getExtended();
chem.phSupply = sys.board.valueMaps.phSupplyTypes.transform(this.phSupply);
chem.doserType = sys.board.valueMaps.phDoserTypes.transform(this.doserType);
return chem;
}
}
export class ChemicalORP extends Chemical {
public initData() {
this.data.chemType = 'orp';
if (typeof this.data.setpoint === 'undefined') this.data.setpoint = 600;
if (typeof this.data.useChlorinator === 'undefined') this.data.useChlorinator = false;
if (typeof this.data.chlorDosingMethod === 'undefined') this.data.chlorDosingMethod = 0;
if (typeof this.data.probe === 'undefined') this.data.probe = {};
if (typeof this.data.tolerance === 'undefined') this.data.tolerance = { low: 650, high: 800, enabled: true };
if (typeof this.data.phLockout === 'undefined') this.data.phLockout = 7.8;
if (typeof this.data.doserType === 'undefined') this.data.doserType = 0;
super.initData();
}
public get useChlorinator(): boolean { return utils.makeBool(this.data.useChlorinator); }
public set useChlorinator(val: boolean) { this.setDataVal('useChlorinator', val); }
public get chlorId(): number {
if (typeof this.data.chlorId === 'undefined'){
// default to 1st chlorinator if not set; this is a backwards compatibility item when upgrading to 8.1
return sys.chlorinators.getItemByIndex(0).id;
}
return this.data.chlorId;
}
public set chlorId(val: number) { this.setDataVal('chlorId', val); }
public get phLockout(): number { return this.data.phLockout; }
public set phLockout(val: number) { this.setDataVal('phLockout', val); }
public get probe(): ChemicalORPProbe { return new ChemicalORPProbe(this.data, 'probe', this); }
public get chlorDosingMethod(): number | any { return this.data.chlorDosingMethod; }
public set chlorDosingMethod(val: number | any) { this.setDataVal('chlorDosingMethod', sys.board.valueMaps.chemChlorDosingMethods.encode(val)); }
public get doserType(): number | any { return this.data.doserType; }
public set doserType(val: number | any) { this.setDataVal('doserType', sys.board.valueMaps.orpDoserTypes.encode(val)); }
public getExtended() {
let chem = super.getExtended();
chem.probe = this.probe.getExtended();
chem.tank = this.tank.getExtended();
chem.doserType = sys.board.valueMaps.orpDoserTypes.transform(this.doserType);
return chem;
}
}
export class FilterCollection extends EqItemCollection {
constructor(data: any, name?: string) { super(data, name || "filters"); }
public createItem(data: any): Filter { return new Filter(data); }
}
export class Filter extends EqItem {
public dataName = 'filterConfig';
public initData() {
if (typeof this.data.filterType === 'undefined') this.data.filterType = 3;
if (typeof this.data.capacity === 'undefined') this.data.capacity = 0;
if (typeof this.data.capacityUnits === 'undefined') this.data.capacityUnits = 0;
if (typeof this.data.cleanPressure === 'undefined') this.data.cleanPressure = 0;
if (typeof this.data.dirtyPressure === 'undefined') this.data.dirtyPressure = 0;
if (typeof this.data.pressureUnits === 'undefined') this.data.pressureUnits = 0;
// Start this out at pool and let the user switch it around if necessary.
if (typeof this.data.pressureCircuitId === 'undefined') this.data.pressureCircuitId = 6;
}
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', val); }
public get filterType(): number | any { return this.data.filterType; }
public set filterType(val: number | any) { this.setDataVal('filterType', sys.board.valueMaps.filterTypes.encode(val)); }
public get capacity(): number | any { return this.data.capacity; }
public set capacity(val: number | any) { this.setDataVal('capacity', val); }
public get capacityUnits(): number | any { return this.data.capacityUnits; }
public set capacityUnits(val: number | any) { this.setDataVal('capacityUnits', sys.board.valueMaps.areaUnits.encode(val)); }
public get body(): number | any { return this.data.body; }
public set body(val: number | any) { this.setDataVal('body', sys.board.valueMaps.bodies.encode(val)); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get showPressure(): boolean { return this.data.showPressure; }
public set showPressure(val: boolean) { this.setDataVal('showPressure', val); }
public get lastCleanDate(): Timestamp { return this.data.lastCleanDate; }
public set lastCleanDate(val: Timestamp) { this.setDataVal('lastCleanDate', val); }
public get needsCleaning(): number { return this.data.needsCleaning; }
public set needsCleaning(val: number) { this.setDataVal('needsCleaning', val); }
public get pressureUnits(): number { return this.data.pressureUnits; }
public set pressureUnits(val: number) { this.setDataVal('pressureUnits', val); }
public get pressureCircuitId(): number { return this.data.pressureCircuitId; }
public set pressureCircuitId(val: number) { this.setDataVal('pressureCircuitId', val); }
public get cleanPressure(): number { return this.data.cleanPressure; }
public set cleanPressure(val: number) { this.setDataVal('cleanPressure', val); }
public get dirtyPressure(): number { return this.data.dirtyPressure; }
public set dirtyPressure(val: number) { this.setDataVal('dirtyPressure', val); }
public get connectionId(): string { return this.data.connectionId; }
public set connectionId(val: string) { this.setDataVal('connectionId', val); }
public get deviceBinding(): string { return this.data.deviceBinding; }
public set deviceBinding(val: string) { this.setDataVal('deviceBinding', val); }
}
export class ChemicalProbe extends ChildEqItem {
public initData() {
if (typeof this.data.enabled === 'undefined') this.data.enabled = true;
super.initData();
}
public get enabled(): boolean { return utils.makeBool(this.data.enabled); }
public set enabled(val: boolean) { this.setDataVal('enabled', val); }
public get connectionId(): string { return this.data.connectionId; }
public set connectionId(val: string) { this.setDataVal('connectionId', val); }
public get deviceBinding(): string { return this.data.deviceBinding; }
public set deviceBinding(val: string) { this.setDataVal('deviceBinding', val); }
public get remFeedEnabled(): boolean { return this.data.remFeedEnabled; }
public set remFeedEnabled(val: boolean) { this.setDataVal('remFeedEnabled', val); }
public get remFeedId(): number { return this.data.remFeedId; }
public set remFeedId(val: number) { this.setDataVal('remFeedId', val); }
public getExtended() { return this.get(true); }
}
export class ChemicalPhProbe extends ChemicalProbe {
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.chemPhProbeTypes.encode(val)); }
public get feedBodyTemp(): boolean { return utils.makeBool(this.data.feedBodyTemp); }
public set feedBodyTemp(val: boolean) { this.setDataVal('feedBodyTemp', val); }
}
export class ChemicalORPProbe extends ChemicalProbe {
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.chemORPProbeTypes.encode(val)); }
}
export class ChemicalPump extends ChildEqItem {
public dataName = 'chemicalPumpConfig';
public initData() {
if (typeof this.data.type === 'undefined') this.data.type = 0;
if (typeof this.data.ratedFlow === 'undefined') this.data.ratedFlow = 0;
if (typeof this.data.enabled === 'undefined') this.data.enabled = true;
super.initData();
}
public get enabled(): boolean { return utils.makeBool(this.data.enabled); }
public set enabled(val: boolean) { this.setDataVal('enabled', val); }
public get connectionId(): string { return this.data.connectionId; }
public set connectionId(val: string) { this.setDataVal('connectionId', val); }
public get deviceBinding(): string { return this.data.deviceBinding; }
public set deviceBinding(val: string) { this.setDataVal('deviceBinding', val); }
public get type(): number | any { return this.data.type; }
public set type(val: number | any) { this.setDataVal('type', sys.board.valueMaps.chemPumpTypes.encode(val)); }
public get ratedFlow(): number { return this.data.ratedFlow || 0; }
public set ratedFlow(val: number) { this.setDataVal('ratedFlow', val); }
public getExtended() {
let pump = this.get(true);
pump.type = sys.board.valueMaps.chemPumpTypes.transform(this.type);
return pump;
}
}
export class ChemicalTank extends ChildEqItem {
public dataName = 'chemicalTankConfig';
public initData() {
if (typeof this.data.capacity === 'undefined') this.data.capacity = 0;
if (typeof this.data.units === 'undefined') this.data.units = 0;
if (typeof this.data.alarmEmptyEnabled === 'undefined') this.data.alarmEmptyEnabled = true;
if (typeof this.data.alarmEmptyLevel === 'undefined') this.data.alarmEmptyLevel = 20;
super.initData();
}
public get capacity(): number { return this.data.capacity; }
public set capacity(val: number) { this.setDataVal('capacity', val); }
public get alarmEmptyEnabled(): boolean { return this.data.alarmEmptyEnabled; }
public set alarmEmptyEnabled(val: boolean) { this.setDataVal('alarmEmptyEnabled', val); }
public get alarmEmptyLevel(): number { return this.data.alarmEmptyLevel; }
public set alarmEmptyLevel(val: number) { this.setDataVal('alarmEmptyLevel', val); }
public get units(): number | any { return this.data.units; }
public set units(val: number | any) { this.setDataVal('units', sys.board.valueMaps.volumeUnits.encode(val)); }
public getExtended() {
let tank = this.get(true);
tank.units = sys.board.valueMaps.volumeUnits.transform(this.units);
return tank;
}
}
/* export class ChemicalChlor extends ChildEqItem {
// This whole class is a reference to the first chlorinator.
// This may not follow a best practice
// and certainly won't work for multiple chlors
public dataName = 'chemicalChlorConfig';
public initData() {
super.initData();
}
public get enabled(): boolean {
let chlor = sys.chlorinators.getItemById(1);
return chlor.isActive;
}
public get type(): number | any {
let chlor = sys.chlorinators.getItemById(1);
return chlor.type;
}
public get model(): number {
let chlor = sys.chlorinators.getItemById(1);
return typeof chlor.model !== 'undefined' ? chlor.model : 0;
}
public get ratedLbs(): number {
let model = sys.board.valueMaps.chlorinatorModel.get(this.model);
return typeof model.chlorinePerSec !== 'undefined' ? model.chlorinePerSec : 0;
}
public set ratedLbs(val: number) { this.setDataVal('ratedLbs', val); }
public get superChlor() {
let chlor = sys.chlorinators.getItemById(1);
return typeof chlor.superChlor !== 'undefined' ? chlor.superChlor : false;
}
public getExtended() {
let chlor = this.get(true);
chlor.model = sys.board.valueMaps.chlorinatorModel.transform(this.model);
return chlor;
}
} */
export class AlarmSetting extends ChildEqItem {
public dataName = 'AlarmSettingConfig';
public initData() {
if (typeof this.data.low === 'undefined') this.data.low = 0;
if (typeof this.data.high === 'undefined') this.data.high = 0;
}
public get enabled(): boolean { return utils.makeBool(this.data.enabled); }
public set enabled(val: boolean) { this.setDataVal('enabled', val); }
public get low(): number { return this.data.low; }
public set low(val: number) { this.setDataVal('low', val); }
public get high(): number { return this.data.high; }
public set high(val: number) { this.setDataVal('high', val); }
}
export class Screenlogic extends EqItem {
ctor(data: any, name?: any): General { return new General(data, name || 'pool'); }
public get enabled(): boolean { return this.data.enabled; }
public set enabled(val: boolean) { this.setDataVal('enabled', val); }
public get type(): 'local'|'remote' { return this.data.type; }
public set type(val: 'local'|'remote') { this.setDataVal('type', val); }
public get systemName(): string { return this.data.systemName; }
public set systemName(val: string) { this.setDataVal('systemName', val); }
public get password(): string { return this.data.password; }
public set password(val: string) { this.setDataVal('password', val); }
public clear(master: number = -1) {
if (master === -1)
super.clear();
}
}
export let sys = new PoolSystem();