/* 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 of1
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
import * as extend from 'extend';
import * as fs from 'fs';
import * as path from 'path';
import { setTimeout } from 'timers';
import * as util from 'util';
import { logger } from '../logger/Logger';
import { webApp } from '../web/Server';
import { ControllerType, Timestamp, utils, Heliotrope } from './Constants';
import { sys, Chemical, ChemController, ChemicalTank, ChemicalPump, Schedule } from './Equipment';
import { versionCheck } from '../config/VersionCheck';
import { DataLogger, DataLoggerEntry } from '../logger/DataLogger';
import { delayMgr } from './Lockouts';
import { time } from 'console';
import { getCoordinatesForZip } from './zipCoords';
export class State implements IState {
statePath: string;
data: any;
_dirtyList: DirtyStateCollection = new DirtyStateCollection();
protected _lastUpdated: Date;
private _isDirty: boolean;
private _timerDirty: NodeJS.Timeout;
protected _dt: Timestamp;
protected _startTime: Timestamp;
protected _controllerType: ControllerType;
protected onchange = (obj, fn) => {
const handler = {
get(target, property, receiver) {
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) || util.types.isDate(val) || util.types.isBoxedPrimitive(val))
return val;
return new Proxy(val, handler);
}
return val;
},
set(target, property, value, receiver) {
if (property !== 'lastComm' && Reflect.get(target, property, receiver) !== value) {
fn();
}
return Reflect.set(target, property, value, receiver);
},
deleteProperty(target, property) {
if (property in target) {
delete target[property];
}
return true;
}
};
return new Proxy(obj, handler);
};
constructor() { this.statePath = path.posix.join(process.cwd(), '/data/poolState.json'); }
public heliotrope: Heliotrope;
public get dirty(): boolean { return this._isDirty; }
public set dirty(val) {
var self = this;
if (val !== this._isDirty) {
this._isDirty = val;
if (this._timerDirty) {
clearTimeout(this._timerDirty);
this._timerDirty = null;
}
if (this._isDirty) this._timerDirty = setTimeout(function () { self.persist(); }, 3000);
}
}
public persist() {
this._isDirty = false;
var self = this;
Promise.resolve()
.then(() => {
fs.writeFileSync(self.statePath, JSON.stringify(self.data, undefined, 2));
})
.catch(function (err) { if (err) logger.error('Error writing pool state %s %s', err, self.statePath); });
}
public async readLogFile(logFile: string): Promise {
try {
let logPath = path.join(process.cwd(), '/logs');
if (!fs.existsSync(logPath)) fs.mkdirSync(logPath);
logPath += (`/${logFile}`);
let lines = [];
if (fs.existsSync(logPath)) {
let buff = fs.readFileSync(logPath);
lines = buff.toString().split('\n');
}
return lines;
} catch (err) { logger.error(`Error reading log file ${logFile}: ${err.message}`); }
}
public async logData(logFile: string, data: any) {
try {
let logPath = path.join(process.cwd(), '/logs');
if (!fs.existsSync(logPath)) fs.mkdirSync(logPath);
logPath += (`/${logFile}`);
let lines = [];
if (fs.existsSync(logPath)) {
let buff = fs.readFileSync(logPath);
lines = buff.toString().split('\n');
}
if (typeof data === 'object')
lines.unshift(JSON.stringify(data));
else
lines.unshift(data.toString());
fs.writeFileSync(logPath, lines.join('\n'));
} catch (err) { logger.error(`Error reading or writing logData ${logFile}: ${err.message}`); }
}
public getState(section?: string): any {
// todo: getState('time') returns an array of chars. Needs no be fixed.
//let state:any = {};
let obj: any = this;
if (typeof section === 'undefined' || section === 'all') {
var _state: any = this.controllerState;
_state.temps = this.temps.getExtended();
_state.equipment = this.equipment.getExtended();
_state.pumps = this.pumps.getExtended();
_state.valves = this.valves.getExtended();
_state.heaters = this.heaters.getExtended();
_state.chlorinators = this.chlorinators.getExtended();
_state.circuits = this.circuits.getExtended();
_state.features = this.features.getExtended();
_state.circuitGroups = this.circuitGroups.getExtended();
_state.lightGroups = this.lightGroups.getExtended();
_state.virtualCircuits = this.virtualCircuits.getExtended();
_state.covers = this.covers.getExtended();
_state.filters = this.filters.getExtended();
_state.schedules = this.schedules.getExtended();
_state.chemControllers = this.chemControllers.getExtended();
_state.chemDosers = this.chemDosers.getExtended();
_state.delays = delayMgr.serialize();
return _state;
}
else {
if (typeof this[section] !== 'undefined' && typeof this[section].getExtended === 'function')
return this[section].getExtended();
else
if (typeof this.data[section] !== 'object')
// return object so browsers don't complain
return { [section]: this.data[section] };
else if (Array.isArray(this.data[section]))
return extend(true, [], this.data[section] || []);
else
return extend(true, {}, this.data[section] || {});
}
}
public async stopAsync() {
try {
if (this._timerDirty) clearTimeout(this._timerDirty);
this.persist();
/* if (sys.controllerType === ControllerType.Virtual) {
for (let i = 0; i < state.temps.bodies.length; i++) {
state.temps.bodies.getItemByIndex(i).isOn = false;
}
for (let i = 0; i < state.circuits.length; i++) {
state.circuits.getItemByIndex(i).isOn = false;
}
for (let i = 0; i < state.features.length; i++) {
state.features.getItemByIndex(i).isOn = false;
}
} */
logger.info('State process shut down');
} catch (err) { logger.error(`Error shutting down state process: ${err.message}`); }
}
private _emitTimerDirty: NodeJS.Timeout;
private _hasChanged = false;
private get hasChanged() { return this._hasChanged; }
private set hasChanged(val: boolean) {
// RSG: 7/4/2020 - added this because it is now a "lazy" emit.
// If emitControllerChange isn't called right away this will call the emit fn after 3s.
// Previously, this would not happen every minute when the time changed.
this._hasChanged = val;
var self = this;
if (val !== this._hasChanged) {
clearTimeout(this._emitTimerDirty);
this._emitTimerDirty = null;
}
if (this._hasChanged) { this._emitTimerDirty = setTimeout(function () { self.emitControllerChange(); }, 3000) }
}
public get controllerState() {
var self = this;
return {
systemUnits: sys.board.valueMaps.systemUnits.transform(sys.general.options.units),
startTime: self.data.startTime || '',
time: self.data.time || '',
// body: self.data.body || {},
valve: self.data.valve || 0,
//delay: typeof self.data.delay === 'undefined' ? sys.board.valueMaps.delay.transformByName('nodelay') : self.data.delay,
delay: self.data.delay || {},
// adjustDST: self.data.adjustDST || false,
batteryVoltage: self.data.batteryVoltage || 0,
status: self.data.status || {},
mode: self.data.mode || {},
appVersion: sys.appVersion || '',
appVersionState: self.appVersion.get(true) || {},
clockMode: sys.board.valueMaps.clockModes.transform(sys.general.options.clockMode) || {},
clockSource: sys.board.valueMaps.clockSources.transformByName(sys.general.options.clockSource) || {},
controllerType: sys.controllerType,
model: sys.equipment.model,
sunrise: self.data.sunrise || '',
sunset: self.data.sunset || '',
nextSunrise: self.data.nextSunrise || '',
nextSunset: self.data.nextSunset || '',
alias: sys.general.alias,
freeze: utils.makeBool(self.data.freeze),
vacation: utils.makeBool(self.data.vacation),
valveMode: self.data.valveMode || {},
};
}
public emitAllEquipmentChanges() {
// useful for setting initial states of external clients like MQTT, SmartThings, Hubitat, etc
state.temps.hasChanged = true;
state.equipment.hasChanged = true;
for (let i = 0; i < state.circuits.length; i++) {
state.circuits.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.features.length; i++) {
state.features.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.temps.bodies.length; i++) {
state.temps.bodies.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.circuits.length; i++) {
state.circuits.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.pumps.length; i++) {
state.pumps.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.valves.length; i++) {
state.valves.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.heaters.length; i++) {
state.heaters.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.chlorinators.length; i++) {
state.chlorinators.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.circuitGroups.length; i++) {
state.circuitGroups.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.lightGroups.length; i++) {
state.lightGroups.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.virtualCircuits.length; i++) {
state.virtualCircuits.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.covers.length; i++) {
state.covers.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.filters.length; i++) {
state.filters.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.schedules.length; i++) {
state.schedules.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.chemControllers.length; i++) {
state.chemControllers.getItemByIndex(i).hasChanged = true;
}
for (let i = 0; i < state.chemDosers.length; i++) {
state.chemDosers.getItemByIndex(i).hasChanged = true;
}
state.emitEquipmentChanges();
}
public emitEquipmentChanges() {
if (typeof (webApp) !== 'undefined' && webApp) { this._dirtyList.emitChanges(); }
}
public emitControllerChange() {
var self = this;
if (typeof (webApp) !== 'undefined' && webApp) {
if (self.hasChanged) {
self.hasChanged = false;
webApp.emitToClients('controller', self.controllerState);
}
}
}
public get time(): Timestamp {
if (typeof this._dt === 'undefined' || !this._dt.isValid) this._dt = new Timestamp(new Date());
return this._dt;
}
public get mode(): number { return typeof (this.data.mode) !== 'undefined' ? this.data.mode.val : -1; }
public set mode(val: number) {
let m = sys.board.valueMaps.panelModes.transform(val);
if (m.val !== this.mode) {
this.data.mode = m;
this.hasChanged = true;
}
}
public get valveMode(): number { return typeof this.data.valveMode !== 'undefined' ? this.data.valveMode.val : 0; }
public set valveMode(val: number) {
let m = sys.board.valueMaps.valveModes.transform(val);
if (m.val !== this.valveMode) {
this.data.valveMode = m;
this.hasChanged = true;
}
}
public get freeze(): boolean { return this.data.freeze === true; }
public set freeze(val: boolean) {
if (this.data.freeze !== val) {
this.data.freeze = val;
this.hasChanged = true;
}
}
public get vacation(): boolean { return this.data.vacation === true; }
public set vacation(val: boolean) {
if (this.data.vacation !== val) {
this.data.vacation = val;
this.hasChanged = true;
}
}
public get status() { return typeof (this.data.status) !== 'undefined' ? this.data.status.val : -1; }
public set status(val) {
if (typeof (val) === 'number') {
if (this.status !== val) {
this.data.status = sys.board.valueMaps.controllerStatus.transform(val);
this.hasChanged = true;
}
}
else if (typeof val !== 'undefined' && typeof val.val !== 'undefined') {
if (this.status !== val.val || this.status.percent !== val.percent) {
this.data.status = val;
this.hasChanged = true;
}
}
}
public get valve(): number { return this.data.valve; }
public set valve(val: number) {
if (this.data.valve !== val) {
this.data.valve = val;
this.hasChanged = true;
}
}
public get delay(): number { return typeof this.data.delay !== 'undefined' ? this.data.delay.val : -1; }
public set delay(val: number) {
if (this.delay !== val) {
this.data.delay = sys.board.valueMaps.delay.transform(val);
this.hasChanged = true;
}
}
public get batteryVoltage(): number { return this.data.batteryVoltage; }
public set batteryVoltage(val: number) {
if (this.data.batteryVoltage !== val) {
this.data.batteryVoltage = val;
}
}
public get isInitialized(): boolean { return typeof (this.data.status) !== 'undefined' && this.data.status.val !== 0; }
public init() {
console.log(`Init state for Pool Controller`);
var sdata = this.loadFile(this.statePath, {});
sdata = extend(true, { mode: { val: -1 }, temps: { units: { val: 0, name: 'F', desc: 'Fahrenheit' } } }, sdata);
this.sanitizeTransientLightGroupState(sdata);
if (typeof sdata.temps !== 'undefined' && typeof sdata.temps.bodies !== 'undefined') {
EqStateCollection.removeNullIds(sdata.temps.bodies);
}
EqStateCollection.removeNullIds(sdata.schedules);
EqStateCollection.removeNullIds(sdata.features);
EqStateCollection.removeNullIds(sdata.circuits);
EqStateCollection.removeNullIds(sdata.pumps);
EqStateCollection.removeNullIds(sdata.chlorinators);
EqStateCollection.removeNullIds(sdata.valves);
EqStateCollection.removeNullIds(sdata.heaters);
EqStateCollection.removeNullIds(sdata.covers);
EqStateCollection.removeNullIds(sdata.circuitGroups);
EqStateCollection.removeNullIds(sdata.lightGroups);
EqStateCollection.removeNullIds(sdata.remotes);
EqStateCollection.removeNullIds(sdata.chemControllers);
EqStateCollection.removeNullIds(sdata.chemDosers);
EqStateCollection.removeNullIds(sdata.filters);
// Initialize the schedules.
if (typeof sdata.schedules !== 'undefined') {
for (let i = 0; i < sdata.schedules.length; i++) {
let ssched = sdata.schedules[i];
ssched.manualPriorityActive = ssched.isOn = ssched.triggered = false;
if (typeof ssched.scheduleTime !== 'undefined') ssched.scheduleTime.calculated = false;
}
}
var self = this;
let pnlTime = typeof sdata.time !== 'undefined' && sdata.time !== '' ? new Date(sdata.time) : new Date();
if (isNaN(pnlTime.getTime())) pnlTime = new Date();
this._dt = new Timestamp(pnlTime);
this._dt.milliseconds = 0;
this.data = sdata;
this.equipment = new EquipmentState(this.data, 'equipment');
this.equipment.messages.clear();
//this.onchange(state, function () { self.dirty = true; });
this._dt.emitter.on('change', function () {
self.data.time = self._dt.format();
self.hasChanged = true;
self.heliotrope.date = self._dt.toDate();
// Provide safe access & environment fallback for coordinates
const loc = sys?.general?.location || {} as any;
let lon = loc.longitude;
let lat = loc.latitude;
if (typeof lon !== 'number' || typeof lat !== 'number') {
const envLat = process.env.POOL_LATITUDE ? parseFloat(process.env.POOL_LATITUDE) : undefined;
const envLon = process.env.POOL_LONGITUDE ? parseFloat(process.env.POOL_LONGITUDE) : undefined;
if (typeof lon !== 'number' && typeof envLon === 'number' && !isNaN(envLon)) lon = envLon;
if (typeof lat !== 'number' && typeof envLat === 'number' && !isNaN(envLat)) lat = envLat;
}
if (typeof lon !== 'number' || typeof lat !== 'number') {
const zipCoords = getCoordinatesForZip(loc.zip);
if (zipCoords) {
if (typeof lat !== 'number') lat = zipCoords.latitude;
if (typeof lon !== 'number') lon = zipCoords.longitude;
}
}
self.heliotrope.longitude = lon;
self.heliotrope.latitude = lat;
let times = self.heliotrope.calculatedTimes;
self.data.sunrise = times.isValid ? Timestamp.toISOLocal(times.sunrise) : '';
self.data.sunset = times.isValid ? Timestamp.toISOLocal(times.sunset) : '';
self.data.nextSunrise = times.isValid ? Timestamp.toISOLocal(times.nextSunrise) : '';
self.data.nextSunset = times.isValid ? Timestamp.toISOLocal(times.nextSunset) : '';
self.data.prevSunrise = times.isValid ? Timestamp.toISOLocal(times.prevSunrise) : '';
self.data.prevSunset = times.isValid ? Timestamp.toISOLocal(times.prevSunset) : '';
versionCheck.checkGitRemote();
});
this.status = 0; // Initializing
this.equipment.controllerType = this._controllerType;
this.temps = new TemperatureState(this.data, 'temps');
this.pumps = new PumpStateCollection(this.data, 'pumps');
this.valves = new ValveStateCollection(this.data, 'valves');
this.heaters = new HeaterStateCollection(this.data, 'heaters');
this.circuits = new CircuitStateCollection(this.data, 'circuits');
this.features = new FeatureStateCollection(this.data, 'features');
this.chlorinators = new ChlorinatorStateCollection(this.data, 'chlorinators');
this.schedules = new ScheduleStateCollection(this.data, 'schedules');
this.circuitGroups = new CircuitGroupStateCollection(this.data, 'circuitGroups');
this.lightGroups = new LightGroupStateCollection(this.data, 'lightGroups');
this.virtualCircuits = new VirtualCircuitStateCollection(this.data, 'virtualCircuits');
this.chemControllers = new ChemControllerStateCollection(this.data, 'chemControllers');
this.chemDosers = new ChemDoserStateCollection(this.data, 'chemDosers');
this.covers = new CoverStateCollection(this.data, 'covers');
this.filters = new FilterStateCollection(this.data, 'filters');
this.comms = new CommsState();
this.heliotrope = new Heliotrope();
this.appVersion = new AppVersionState(this.data, 'appVersion');
this.data.startTime = Timestamp.toISOLocal(new Date());
versionCheck.checkGitLocal();
}
private sanitizeTransientLightGroupState(sdata: any) {
if (!sdata || !Array.isArray(sdata.lightGroups)) return;
for (let i = 0; i < sdata.lightGroups.length; i++) {
const lg = sdata.lightGroups[i];
if (!lg || typeof lg !== 'object') continue;
// Sequencing state is transient and must never survive process restarts.
if (typeof lg.action !== 'undefined') delete lg.action;
if (typeof lg.endTime !== 'undefined') delete lg.endTime;
}
}
public resetData() {
this.circuitGroups.clear();
this.lightGroups.clear();
this.circuits.clear();
this.temps.clear();
this.chlorinators.clear();
this.covers.clear();
this.equipment.clear();
this.features.clear();
this.data.general = {};
this.heaters.clear();
this.pumps.clear();
this.schedules.clear();
this.valves.clear();
this.virtualCircuits.clear();
this.filters.clear();
this.chemControllers.clear();
}
public equipment: EquipmentState;
public temps: TemperatureState;
public pumps: PumpStateCollection;
public valves: ValveStateCollection;
public heaters: HeaterStateCollection;
public circuits: CircuitStateCollection;
public features: FeatureStateCollection;
public chlorinators: ChlorinatorStateCollection;
public schedules: ScheduleStateCollection;
public circuitGroups: CircuitGroupStateCollection;
public lightGroups: LightGroupStateCollection;
public virtualCircuits: VirtualCircuitStateCollection;
public covers: CoverStateCollection;
public filters: FilterStateCollection;
public chemControllers: ChemControllerStateCollection;
public chemDosers: ChemDoserStateCollection;
public comms: CommsState;
public appVersion: AppVersionState;
// This performs a safe load of the state file. If the file gets corrupt or actually does not exist
// it will not break the overall system and allow hardened recovery.
private loadFile(path: string, def: any) {
let state = def;
if (fs.existsSync(path)) {
try {
state = JSON.parse(fs.readFileSync(path, 'utf8') || '{}');
}
catch (ex) {
state = def;
}
}
return state;
}
public cleanupState() {
// Chem Controllers
this.chemControllers.cleanupState();
// Valves
this.valves.cleanupState();
// Heaters
this.heaters.cleanupState();
// Features
this.features.cleanupState();
// Circuits
this.circuits.cleanupState();
// CircuitGroups
this.circuitGroups.cleanupState();
// Light Groups
this.lightGroups.cleanupState();
// Chlorinators
this.chlorinators.cleanupState();
// Pumps
this.pumps.cleanupState();
// Bodies
this.temps.cleanupState();
}
}
interface IState {
equipment: EquipmentState;
temps: TemperatureState;
pumps: PumpStateCollection;
valves: ValveStateCollection;
heaters: HeaterStateCollection;
circuits: CircuitStateCollection;
features: FeatureStateCollection;
chlorinators: ChlorinatorStateCollection;
schedules: ScheduleStateCollection;
circuitGroups: CircuitGroupStateCollection;
virtualCircuits: VirtualCircuitStateCollection;
chemControllers: ChemControllerStateCollection;
filters: FilterStateCollection;
comms: CommsState;
}
export interface ICircuitState {
id: number;
type: number;
name: string;
nameId?: number;
isOn: boolean;
startTime?: Timestamp;
endTime: Timestamp;
priority?: string,
lightingTheme?: number;
action?: number;
emitEquipmentChange();
get(bCopy?: boolean);
showInFeatures?: boolean;
isActive?: boolean;
level?: number;
color?: { red: number; green: number; blue: number };
startDelay?: boolean;
stopDelay?: boolean;
manualPriorityActive?: boolean;
dataName?: string;
}
interface IEqStateCreator { ctor(data: any, name: string, parent?): T; }
class EqState implements IEqStateCreator {
public dataName: string;
public data: any;
protected _hasChanged: boolean = false;
public get hasChanged(): boolean { return this._hasChanged; }
public set hasChanged(val: boolean) {
// If we are not already on the dirty list add us.
if (!this._hasChanged && val) {
state._dirtyList.maybeAddEqState(this);
}
this._hasChanged = val;
}
ctor(data, name?: string): EqState { return new EqState(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() { }
public emitEquipmentChange() {
if (typeof (webApp) !== 'undefined' && webApp) {
if (this.hasChanged) this.emitData(this.dataName, this.getEmitData());
this.hasChanged = false;
state._dirtyList.removeEqState(this);
}
}
public getEmitData() { return this.data; }
public emitData(name: string, data: any) { webApp.emitToClients(name, data); }
protected setDataVal(name, val, persist?: boolean): any {
if (this.data[name] !== val) {
this.data[name] = val;
if (typeof persist === 'undefined' || persist) {
this.hasChanged = true;
state.dirty = true;
}
}
else if (typeof persist !== 'undefined' && persist) this.hasChanged = true;
// Added for chaining.
return this.data[name];
}
public get(bCopy?: boolean): any {
if (typeof bCopy === 'undefined' || !bCopy) return this.data;
let copy = extend(true, {}, this.data);
if (typeof this.dataName !== 'undefined') copy.equipmentType = this.dataName;
// RSG 7/10/2020 - nested object were still being returned as proxy; changed to parse/stringify
return JSON.parse(JSON.stringify(copy));
}
public clear() {
for (let prop in this.data) {
if (Array.isArray(this.data[prop])) this.data[prop].length = 0;
else this.data[prop] = undefined;
}
}
public isEqual(eq: EqState) {
if (eq.dataName === this.dataName) {
if ('id' in eq === true)
return this.data.id === eq.data.id;
else
return true;
}
return false;
}
public getExtended(): any { return this.get(true); }
}
class ChildEqState extends EqState implements IEqStateCreator {
private _pmap = new WeakSet();
//private _dataKey = { id: 'parent' };
constructor(data: any, name: string, parent) {
super(data, name);
this._pmap['parent'] = parent;
}
public get hasChanged(): boolean { return this._hasChanged; }
public set hasChanged(val: boolean) {
// Bubble up to the parent state.
if (val) {
let parent = this.getParent();
if (typeof parent !== 'undefined' && typeof parent['hasChanged'] !== 'undefined') {
parent.hasChanged = true;
}
}
}
public getParent() { return typeof this._pmap !== 'undefined' ? this._pmap['parent'] : undefined; }
}
class EqStateCollection {
protected data: any;
constructor(data: [], name: string) {
if (typeof (data[name]) === 'undefined') data[name] = [];
this.data = data[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') {
console.log(`Removing ${data[i].id}-${data[i].name}`);
data.splice(i, 1);
}
else if (typeof data[i].id === 'undefined' || isNaN(data[i].id)) {
console.log(`Removing isNaN ${data[i].id}-${data[i].name}`);
data.splice(i, 1);
}
}
}
}
public getItemById(id: number, add?: boolean, data?: any): T {
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 });
return this.createItem(data || { id: id });
}
public getItemByIndex(ndx: number, add?: boolean): T {
return (this.data.length > ndx) ? this.createItem(this.data[ndx]) : (typeof (add) !== 'undefined' && add) ? this.add(this.createItem({ id: ndx + 1 })) : this.createItem({ id: ndx + 1 });
}
public removeItemById(id: number): 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;
}
public removeItemByIndex(ndx: number) {
return this.data.splice(ndx, 1);
}
public createItem(data: any): T { return new EqState(data) as unknown as T; }
public clear() { this.data.length = 0; }
public get length(): number { return typeof (this.data) !== 'undefined' ? this.data.length : 0; }
public add(obj: any): T { this.data.push(obj); return this.createItem(obj); }
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 get(bCopy?: boolean) { return typeof bCopy === 'undefined' || !bCopy ? this.data : JSON.parse(JSON.stringify(this.data)) };// extend(true, {}, this.data); }
public getExtended(): any {
let arr = [];
for (let i = 0; i < this.length; i++) {
let itm = (this.createItem(this.data[i]) as unknown) as EqState;
if (typeof itm.getExtended === 'function') arr.push(itm.getExtended());
else arr.push(this.data[i]);
}
return arr;
}
// 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);
}
public exists(f: (value: any, index?: number, obj?: any) => boolean): boolean {
let itm = this.find(f);
return typeof itm === 'object';
}
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;
}
}
class DirtyStateCollection extends Array {
public maybeAddEqState(eqItem: EqState): boolean {
if (!this.eqStateExists(eqItem)) {
this.push(eqItem);
return true;
}
return false;
}
public eqStateExists(eqItem: EqState): boolean {
for (let i = this.length - 1; i >= 0; i--) {
if (eqItem.isEqual(this[i])) return true;
}
return false;
}
public findEqState(eqItem: EqState): EqState {
let itm = this.find((eq, ndx, eqList): boolean => {
return eq.isEqual(eqItem);
});
return itm;
}
public emitChanges() {
while (this.length > 0) {
let eqItem = this.shift();
eqItem.emitEquipmentChange();
eqItem.hasChanged = false;
}
}
public removeEqState(eqItem: EqState) {
// We need to go through all the items on the dirty list for now. In the future we
// may not need to look at them all since the global emitter will clear the list for us.
for (let i = this.length - 1; i >= 0; i--) {
let itm = this[i];
if (itm.isEqual(eqItem)) this.splice(i, 1);
}
}
}
export class EquipmentState extends EqState {
public initData() {
if (typeof this.data.messages === 'undefined') this.data.messages = [];
// v3.004+ device registration state
// Persisted shape is { status: number } but callers should only set/get the numeric status via accessors.
// Normalize legacy persisted shapes (e.g. {status, lastConfirmed} or a raw number) to keep poolState.json clean.
const reg = this.data.registration;
if (typeof reg === 'undefined') {
this.data.registration = { status: 0 };
}
else if (typeof reg === 'number') {
this.data.registration = { status: reg };
}
else if (reg === null || typeof reg !== 'object' || typeof reg.status !== 'number' || 'lastConfirmed' in reg) {
const status = (reg && typeof reg.status === 'number') ? reg.status : 0;
this.data.registration = { status };
}
}
public get controllerType(): string { return this.data.controllerType; }
public set controllerType(val: string) { this.setDataVal('controllerType', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get model(): string { return this.data.model; }
public set model(val: string) { this.setDataVal('model', 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 maxValves(): number { return this.data.maxValves; }
public set maxValves(val: number) { this.setDataVal('maxValves', val); }
public get maxCircuits(): number { return this.data.maxCircuits; }
public set maxCircuits(val: number) { this.setDataVal('maxCircuits', val); }
public get maxFeatures(): number { return this.data.maxFeatures; }
public set maxFeatures(val: number) { this.setDataVal('maxFeatures', val); }
public get maxBodies(): number { return this.data.maxBodies; }
public set maxBodies(val: number) { this.setDataVal('maxBodies', val); }
public get maxSchedules(): number { return this.data.maxSchedules; }
public set maxSchedules(val: number) { this.setDataVal('maxSchedules', val); }
public get maxPumps(): number { return this.data.maxPumps; }
public set maxPumps(val: number) { this.setDataVal('maxPumps', val); }
public get maxHeaters(): number { return this.data.maxHeaters; }
public set maxHeaters(val: number) { this.setDataVal('maxHeaters', val); }
public get maxCircuitGroups(): number { return this.data.maxCircuitGroups; }
public set maxCircuitGroups(val: number) { this.setDataVal('maxCircuitGroups', val); }
public get maxLightGroups(): number { return this.data.maxLightGroups; }
public set maxLightGroups(val: number) { this.setDataVal('maxLightGroups', val); }
public get messages(): EquipmentMessages { return new EquipmentMessages(this.data, 'messages'); }
// Current runtime RS-485 bus address njsPC is using (the "plugin address").
// For IntelliCenter v3.004+ this can diverge from the configured value at runtime
// when the OCP converges njsPC onto the first ICP slot (32 vs 33). dashPanel uses
// this to dynamically render the "njsPC" label on whichever slot we're occupying.
public get pluginAddress(): number { return typeof this.data.pluginAddress === 'number' ? this.data.pluginAddress : undefined; }
public set pluginAddress(val: number) { this.setDataVal('pluginAddress', val); }
// v3.004+ device registration state
public get registration(): number {
const reg = this.data.registration;
if (typeof reg === 'number') return reg;
if (reg && typeof reg.status === 'number') return reg.status;
return 0;
}
public set registration(val: number) {
const reg = this.data.registration;
const needsNormalize = (typeof reg !== 'object' || reg === null || typeof reg.status !== 'number' || 'lastConfirmed' in reg);
if (this.registration !== val || needsNormalize) {
this.data.registration = { status: val };
// During module init, `state` may not be constructed yet (State.ts exports `var state = new State()`).
// Guard writes that depend on `state`/dirty list.
if (typeof state !== 'undefined' && state) {
this.hasChanged = true;
state.dirty = true;
}
else {
this._hasChanged = true;
}
}
}
// This could be extended to include all the expansion panels but not sure why.
public getExtended() {
let obj = this.get(true);
obj.softwareVersion = sys.equipment.controllerFirmware || "";
obj.bootLoaderVersion = sys.equipment.bootloaderVersion || "";
return obj;
}
}
// Equipment messages work like this. While other equipment items are unique by id, these are unique by code. This
// means that at any given point there should only be one message per code. Messages should always be referenced by
// code. As a result the codes have meaning and should be encoded as such. That way messages related to a specific topic can be
// removed while preserving any messages previously set. Adding new messages or removing messages always results in
// resorting of the message array so care should be taken when referings the collection by index.
// Message Encoding Structure:
// In the message encoding structure the severity is omitted. This is on purpose so messages can be promoted and demoted
// in severity while still ensuring proper encoding. Do not encode severity into the message code.
// Example: HT:1:1
// EQ = Category - Each code should reference the specific category for the error.
// EQ = Equipment General
// VL = Valves
// HT = Heater
// PMP = Pump
// MISC = Miscellaneous
// SYS = System
// ...etc. Standardizing on the equipment category will allow searching and filtering messages within the array
// 1 = Id of the equipment. If this is not applicable then the id should be 0.
// 1 = The message identifier. This allows uniqueness within the message categories. Care should be taken to ensure this is unique
// within the category and equipment id.
export class EquipmentMessages extends EqStateCollection {
public createItem(data: any): EquipmentMessage { return new EquipmentMessage(data, undefined, this); }
public getItemByCode(code: string, add?: boolean, data?: any): EquipmentMessage {
for (let i = 0; i < this.data.length; i++)
if (typeof this.data[i].code !== 'undefined' && this.data[i].code === code) {
return this.createItem(this.data[i]);
}
if (typeof add !== 'undefined' && add)
return this.add(data || { code: code });
return this.createItem(data || { code: code });
}
public getItemByIndex(ndx: number, add?: boolean): EquipmentMessage {
return (this.data.length > ndx) ? this.createItem(this.data[ndx]) : (typeof (add) !== 'undefined' && add) ? this.add(this.createItem({ code: `UNK:0:${ndx + 1}` })) : this.createItem({ code: `UNK:0:${ndx + 1}` });
}
public removeItemByCode(code: string): EquipmentMessage {
let rem: EquipmentMessage;
for (let i = this.data.length - 1; i >= 0; i--) {
if (typeof (this.data[i].code) !== 'undefined' && this.data[i].code === code) {
rem = this.data.splice(i, 1);
}
}
if (typeof rem !== 'undefined') webApp.emitToClients('sysmessages', this.get(true));
return typeof rem !== 'undefined' ? new EquipmentMessage(rem, undefined, undefined) : undefined;
}
// For lack of a better term category includes the equipment identifier if supplied.
public removeItemByCategory(category: string) {
let rem: EquipmentMessage[] = [];
let cmr = EquipmentMessage.parseMessageCode(category);
let hasChanges = false;
for (let i = this.data.length - 1; i >= 0; i--) {
if (typeof (this.data[i].code) !== 'undefined') {
let cm = EquipmentMessage.parseMessageCode(this.data.code);
if (cm.category === cmr.category) {
if (typeof cmr.equipmentId === 'undefined' || cm.equipmentId === cmr.equipmentId) {
if (typeof cmr.messageId === 'undefined' || cm.messageId === cmr.messageId) {
let data = this.data.splice(i, 1);
rem.push(new EquipmentMessage(data, undefined, undefined));
hasChanges = true;
}
}
}
}
}
if (hasChanges) webApp.emitToClients('sysmessages', this.get(true));
return rem;
}
public setMessageByCode(code: string, severity: string | number, message: string): EquipmentMessage {
let msg = this.getItemByCode(code, true);
msg.severity = sys.board.valueMaps.eqMessageSeverities.encode(severity, 0);
msg.message = message;
webApp.emitToClients('sysmessages', this.get(true));
return msg;
}
public clearAll() {
if (this.data.length > 0) {
this.data.length = 0;
webApp.emitToClients('sysmessages', this.get(true));
}
}
}
export class EquipmentMessage extends ChildEqState {
public initData() {
if (typeof this.data.createDate === 'undefined') this.createDate = new Date();
else this._createDate = new Date(this.data.createDate);
if (isNaN(this._createDate.getTime())) this._createDate = new Date();
}
private _createDate: Date = new Date();
public static parseMessageCode(code: string): { category?: string, equipmentId?: number, messageId?: number } {
let c: { category?: string, equipmentId?: number, messageId?: number } = {};
let arr = code.split(':');
c.category = arr.length > 0 ? arr[0] : 'UNK';
c.equipmentId = arr.length > 1 ? parseInt(arr[1], 10) : undefined;
c.messageId = arr.length > 2 ? parseInt(arr[2], 10) : undefined;
return c;
}
public get createDate(): Date { return this._createDate; }
public set createDate(val: Date) { this._createDate = val; this._saveCreateDate(); }
private _saveCreateDate() { this.setDataVal('createDate', Timestamp.toISOLocal(this.createDate)); }
public get severity(): number { return typeof (this.data.severity) !== 'undefined' ? this.data.severity.val : 0; }
public set severity(val: number) {
if (this.severity !== val) {
this.data.type = sys.board.valueMaps.eqMessageSeverities.transform(val);
this.hasChanged = true;
}
this.hasChanged = true;
}
public get code(): string { return this.data.code; }
public set code(val: string) { this.data.code = val; }
public get message(): string { return this.data.message; }
public set message(val: string) { this.data.message = val; }
}
export class PumpStateCollection extends EqStateCollection {
public createItem(data: any): PumpState { return new PumpState(data); }
public getPumpByAddress(address: number, add?: boolean, data?: any) {
let pmp = this.find(elem => elem.address === address);
if (typeof pmp !== 'undefined') return this.createItem(pmp);
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 cleanupState() {
for (let i = this.data.length - 1; i >= 0; i--) {
if (isNaN(this.data[i].id)) this.data.splice(i, 1);
else {
if (typeof sys.pumps.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
}
}
let cfg = sys.pumps.toArray();
for (let i = 0; i < cfg.length; i++) {
let c = cfg[i];
let s = this.getItemById(cfg[i].id, true);
s.type = c.type;
s.name = c.name;
if (typeof c.isActive === 'undefined') c.isActive = true;
s.isActive = c.isActive;
}
}
}
export class PumpState extends EqState {
public dataName: string = 'pump';
public initData() {
if (typeof this.data.status === 'undefined') {
this.data.status = { name: 'ok', desc: 'Ok', val: 0 };
}
if (typeof this.data.pumpOnDelay === 'undefined') this.data.pumpOnDelay = false;
}
private _pumpOnDelayTimer: NodeJS.Timeout;
private _threshold = 0.05;
private exceedsThreshold(origVal: number, newVal: number) {
return Math.abs((newVal - origVal) / origVal) > this._threshold;
}
public get id(): number { return this.data.id; }
public set id(val: number) { this.data.id = 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 rpm(): number { return this.data.rpm; }
public set rpm(val: number) { this.setDataVal('rpm', val); }
//public set rpm(val: number) { this.setDataVal('rpm', val, this.exceedsThreshold(this.data.rpm, val)); }
public get relay(): number { return this.data.relay; }
public set relay(val: number) { this.setDataVal('relay', val); }
public get program(): number { return this.data.program; }
public set program(val: number) { this.setDataVal('program', val); }
public get watts(): number { return this.data.watts; }
public set watts(val: number) { this.setDataVal('watts', val); }
//public set watts(val: number) { this.setDataVal('watts', val, this.exceedsThreshold(this.data.watts, val)); }
public get flow(): number { return this.data.flow; }
public set flow(val: number) { this.setDataVal('flow', val); }
//public set flow(val: number) { this.setDataVal('flow', val, this.exceedsThreshold(this.data.flow, val)); }
public get mode(): number { return this.data.mode; }
public set mode(val: number) { this.setDataVal('mode', val); }
public get driveState(): number { return this.data.driveState; }
public set driveState(val: number) { this.setDataVal('driveState', val); }
public get command(): number { return this.data.command; }
public set command(val: number) { this.setDataVal('command', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get ppc(): number { return this.data.ppc; } // I think this is actually the filter % for vf and vsf. Pump Pressure determines how much backpressure.
public set ppc(val: number) { this.setDataVal('ppc', val); }
public get status(): number { return typeof (this.data.status) !== 'undefined' ? this.data.status.val : -1; }
public set status(val: number) {
// quick fix for #172
if (this.status !== val) {
if (sys.board.valueMaps.pumpTypes.getName(this.type) === 'vsf' && val === 0) {
this.data.status = { name: 'ok', desc: 'Ok', val: 0 };
}
else this.data.status = sys.board.valueMaps.pumpStatus.transform(val);
this.hasChanged = true;
}
}
/* public get virtualControllerStatus(): number {
return typeof (this.data.virtualControllerStatus) !== 'undefined' ? this.data.virtualControllerStatus.val : -1;
}
public set virtualControllerStatus(val: number) {
if (this.virtualControllerStatus !== val) {
this.data.virtualControllerStatus = sys.board.valueMaps.virtualControllerStatus.transform(val);
this.hasChanged = true;
}
} */
public get targetSpeed(): number { return this.data.targetSpeed; } // used for virtual controller
public set targetSpeed(val: number) { this.setDataVal('targetSpeed', val); }
public get type() { return typeof (this.data.type) !== 'undefined' ? this.data.type.val : -1; }
public set type(val: number) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.pumpTypes.transform(val);
this.hasChanged = true;
}
}
public get time(): number { return this.data.time; }
public set time(val: number) { this.setDataVal('time', val, false); }
public get pumpOnDelay() { return this.data.pumpOnDelay; }
public set pumpOnDelay(val: boolean) {
if (val === false) {
if (typeof this._pumpOnDelayTimer !== 'undefined') clearTimeout(this._pumpOnDelayTimer);
this._pumpOnDelayTimer = undefined;
}
this.setDataVal('pumpOnDelay', val);
}
public setPumpOnDelayTimeout(delay: number) {
this.pumpOnDelay = true;
logger.info(`Pump ON Delay ${this.name} for ${delay / 1000} seconds`);
this._pumpOnDelayTimer = setTimeout(() => {
logger.info(`Pump ON Delay ${this.name} expired`);
this.pumpOnDelay = false;
}, delay);
}
public getExtended() {
let pump = this.get(true);
let cpump = sys.pumps.getItemById(pump.id);
if (typeof (cpump.minSpeed) !== 'undefined') pump.minSpeed = cpump.minSpeed;
if (typeof (cpump.maxSpeed) !== 'undefined') pump.maxSpeed = cpump.maxSpeed;
if (typeof (cpump.minFlow) !== 'undefined') pump.minFlow = cpump.minFlow;
if (typeof (cpump.maxFlow) !== 'undefined') pump.maxFlow = cpump.maxFlow;
pump.speedStepSize = cpump.speedStepSize;
pump.flowStepSize = cpump.flowStepSize;
pump.circuits = [];
for (let i = 0; i < cpump.circuits.length; i++) {
let c = cpump.circuits.getItemByIndex(i).get(true);
c.circuit = state.circuits.getInterfaceById(c.circuit).get(true);
switch (pump.type.name) {
case 'vf':
c.units = sys.board.valueMaps.pumpUnits.transformByName('gpm');
break;
case 'hwvs':
case 'vssvrs':
case 'vs':
case 'regalmodbus':
case 'neptunemodbus':
c.units = sys.board.valueMaps.pumpUnits.transformByName('rpm');
break;
case 'ss':
case 'ds':
case 'sf':
case 'hwrly':
c.units = 'undefined';
break;
default:
c.units = sys.board.valueMaps.pumpUnits.transform(c.units || 0);
break;
}
// RKS: 04-08-22 - This is just wrong. If the user did not define circuits then they should not be sent down and it creates a whole host of issues.
//if (typeof c.circuit.id === 'undefined' || typeof c.circuit.name === 'undefined') {
// // return "blank" circuit if none defined
// c.circuit.id = 0;
// c.circuit.name = 'Not Used';
// if (sys.board.valueMaps.pumpTypes.getName(cpump.type) === 'vf') {
// c.units = sys.board.valueMaps.pumpUnits.getValue('gpm');
// c.circuit.flow = 0;
// }
// else {
// c.units = sys.board.valueMaps.pumpUnits.getValue('rpm');
// c.circuit.speed = 0;
// }
//}
//c.units = sys.board.valueMaps.pumpUnits.transform(c.units);
pump.circuits.push(c);
}
pump.circuits.sort((a, b) => { return a.id > b.id ? 1 : -1; });
/* for (let i = 0; i < cpump.circuits.length; i++) {
let c = cpump.circuits.getItemByIndex(i).get(true);
c.circuit = state.circuits.getInterfaceById(c.circuit).get(true);
c.units = sys.board.valueMaps.pumpUnits.transform(c.units);
pump.circuits.push(c);
} */
return pump;
}
}
export class ScheduleStateCollection extends EqStateCollection {
public createItem(data: any): ScheduleState { return new ScheduleState(data); }
public getActiveSchedules(): ScheduleState[] {
let activeScheds: ScheduleState[] = [];
for (let i = 0; i < this.length; i++) {
let ssched = this.getItemByIndex(i);
let st = ssched.scheduleTime;
let sched = sys.schedules.getItemById(ssched.id);
// rsg st.startTime is null when the schedule has No Days <-- WRONG. ssched.scheduleDays should be checked.
// original fix #879; updated fix #1033
if (!sched.isActive || ssched.disabled || ssched.scheduleDays === 0) {
continue;
}
st.calcSchedule(state.time, sys.schedules.getItemById(ssched.id));
if (typeof st.startTime === 'undefined') continue;
if (ssched.isOn || st.shouldBeOn || (st.startTime && st.startTime.getTime() > new Date().getTime())) activeScheds.push(ssched);
}
return activeScheds;
}
}
export class ScheduleTime extends ChildEqState {
public initData() { if (typeof this.data.times !== 'undefined') delete this.data.times; }
public get calculatedDate(): Date { return typeof this.data.calculatedDate !== 'undefined' && this.data.calculatedDate !== '' ? new Date(this.data.calculatedDate) : new Date(1970, 0, 1); }
public set calculatedDate(val: Date) { this._saveTimestamp(val, 'calculatedDate', false); }
public get startTime(): Date { return typeof this.data.startTime !== 'undefined' && this.data.startTime !== '' ? new Date(this.data.startTime) : null; }
public set startTime(val: Date) { this._saveTimestamp(val, 'startTime'); }
public get endTime(): Date { return typeof this.data.endTime !== 'undefined' && this.data.endTime !== '' ? new Date(this.data.endTime) : null; }
public set endTime(val: Date) { this._saveTimestamp(val, 'endTime'); }
private _saveTimestamp(dt, prop, persist:boolean = true) {
if (typeof dt === 'undefined' || !dt) this.setDataVal(prop, '');
else this.setDataVal(prop, Timestamp.toISOLocal(dt));
}
private _calcShouldBeOn(time: number) : boolean {
let tmStart = this.startTime ? this.startTime.getTime() : NaN;
let tmEnd = this.endTime ? this.endTime.getTime() : NaN;
if (isNaN(tmStart) || isNaN(tmEnd) || time < tmStart || time > tmEnd) return false;
return true;
}
public get shouldBeOn(): boolean {
let shouldBeOn = this._calcShouldBeOn(state.time.getTime());
if (this.data.shouldBeOn !== shouldBeOn) this.setDataVal('shouldBeOn', shouldBeOn);
return this.data.shouldBeOn || false;
}
protected set shouldBeOn(val: boolean) { this.setDataVal('shouldBeOn', val); }
public get calculated(): boolean { return this.data.calculated; }
public set calculated(val: boolean) { this.setDataVal('calculated', val); }
public calcScheduleDate(ts: Timestamp, sched: Schedule): { startTime: Date, endTime: Date } {
let times: { startTime: Date, endTime: Date } = { startTime: null, endTime: null };
try {
let sod = ts.clone().startOfDay();
let ysod = ts.clone().addHours(-24).startOfDay();
let nsod = ts.clone().addHours(24).startOfDay();
let ytimes: { startTime: Date, endTime: Date } = { startTime: null, endTime: null }; // Yesterday
let ttimes: { startTime: Date, endTime: Date } = { startTime: null, endTime: null }; // Today
let ntimes: { startTime: Date, endTime: Date } = { startTime: null, endTime: null }; // Tomorrow
let tt = sys.board.valueMaps.scheduleTimeTypes.transform(sched.startTimeType);
// Add the range for today and yesterday.
switch (tt.name) {
case 'sunrise': {
let sr = state.heliotrope.calcAdjustedTimes(sod.toDate(), 0, sched.startTimeOffset);
if (!sr.isValid) return times;
ytimes.startTime = sr.prevSunrise;
ttimes.startTime = sr.sunrise;
ntimes.startTime = sr.nextSunrise;
break;
}
case 'sunset': {
let ss = state.heliotrope.calcAdjustedTimes(sod.toDate(), 0, sched.startTimeOffset);
if (!ss.isValid) return times;
ytimes.startTime = ss.prevSunset;
ttimes.startTime = ss.sunset;
ntimes.startTime = ss.nextSunset;
break;
}
default:
ytimes.startTime = ysod.clone().addMinutes(sched.startTime).toDate();
ttimes.startTime = sod.clone().addMinutes(sched.startTime).toDate();
ntimes.startTime = nsod.clone().addMinutes(sched.startTime).toDate();
break;
}
tt = sys.board.valueMaps.scheduleTimeTypes.transform(sched.endTimeType);
switch (tt.name) {
case 'sunrise': {
let sr = state.heliotrope.calcAdjustedTimes(sod.toDate(), 0, sched.endTimeOffset);
if (!sr.isValid) return times;
ytimes.endTime = ytimes.startTime >= sr.prevSunrise ? sr.sunrise : sr.prevSunrise;
ttimes.endTime = ttimes.startTime >= sr.sunrise ? sr.nextSunrise : sr.sunrise;
ntimes.endTime = ntimes.startTime >= sr.nextSunrise ? new Timestamp(sr.nextSunrise).addHours(24).toDate() : sr.nextSunrise;
break;
}
case 'sunset': {
let ss = state.heliotrope.calcAdjustedTimes(sod.toDate(), 0, sched.endTimeOffset);
if (!ss.isValid) return times;
ytimes.endTime = ytimes.startTime >= ss.prevSunset ? ss.sunset : ss.prevSunset;
ttimes.endTime = ttimes.startTime >= ss.sunset ? ss.nextSunset : ss.nextSunset;
ntimes.endTime = ntimes.startTime >= ss.nextSunset ? new Timestamp(ss.nextSunset).addHours(24).toDate() : ss.nextSunset;
break;
}
default:
ytimes.endTime = ysod.clone().addMinutes(sched.endTime).toDate();
if (ytimes.endTime <= ytimes.startTime) ytimes.endTime = ysod.clone().addHours(24).addMinutes(sched.endTime).toDate();
ttimes.endTime = sod.clone().addMinutes(sched.endTime).toDate();
if (ttimes.endTime <= ttimes.startTime) ttimes.endTime = sod.clone().addHours(24).addMinutes(sched.endTime).toDate();
ntimes.endTime = nsod.clone().addMinutes(sched.endTime).toDate();
if (ntimes.endTime <= ntimes.startTime) ntimes.endTime = nsod.clone().addHours(24).addMinutes(sched.endTime).toDate();
break;
}
ttimes.startTime.setSeconds(0, 0); // Set the start time to the beginning of the minute.
ttimes.endTime.setSeconds(59, 999); // Set the end time to the end of the minute.
ytimes.startTime.setSeconds(0, 0);
ytimes.endTime.setSeconds(59, 999);
ntimes.startTime.setSeconds(0, 0);
ntimes.endTime.setSeconds(59, 999);
// Now check the dow for each range. If the start time for the dow matches then include it. If not then do not.
let schedDays = sys.board.valueMaps.scheduleDays.toArray();
let fnInRange = (time, times) => {
let tmStart = times.startTime ? times.startTime.getTime() : NaN;
let tmEnd = times.endTime ? times.endTime.getTime() : NaN;
if (isNaN(tmStart) || isNaN(tmEnd) || time < tmStart || time > tmEnd) return false;
return true;
}
let tm = ts.getTime();
if (fnInRange(tm, ttimes)) {
// Check the dow.
let sd = schedDays.find(elem => elem.dow === ttimes.startTime.getDay());
if (typeof sd !== 'undefined' && (sched.scheduleDays & sd.bitval) !== 0) {
times.startTime = ttimes.startTime;
times.endTime = ttimes.endTime;
return times;
}
}
// First check if we are still running yesterday. This will ensure we have
// the first runtime.
if (fnInRange(tm, ytimes)) {
// Check the dow.
let sd = schedDays.find(elem => elem.dow === ytimes.startTime.getDay());
if (typeof sd !== 'undefined' && (sched.scheduleDays & sd.bitval) !== 0) {
times.startTime = ytimes.startTime;
times.endTime = ytimes.startTime;
return times;
}
}
// Then check if we are running today. If we have already run then get net next run
// time.
if (tm <= ttimes.startTime.getTime()) {
let sd = schedDays.find(elem => elem.dow === ttimes.startTime.getDay());
if (typeof sd !== 'undefined' && (sched.scheduleDays & sd.bitval) !== 0) {
times.startTime = ttimes.startTime;
times.endTime = ttimes.endTime;
return times;
}
}
// Then look for tomorrow.
if (tm <= ntimes.startTime.getTime()) {
let sd = schedDays.find(elem => elem.dow === ntimes.startTime.getDay());
if (typeof sd !== 'undefined' && (sched.scheduleDays & sd.bitval) !== 0) {
times.startTime = ntimes.startTime;
times.endTime = ntimes.endTime;
return times;
}
}
return times;
} catch (err) {
logger.error(`Error calculating date for schedule ${sched.id}: ${err.message}`);
}
finally { return times; }
}
public calcSchedule(currentTime: Timestamp, sched: Schedule): boolean {
try {
let sod = currentTime.clone().startOfDay();
// There are 3 conditions where the schdedule will be recalculated. The first
// 1. The calculated flag is false
// 2. The calculated flag is true and the calculated date < the current start of day
// 3. Regardless of the calculated date the current end time has passed and the start time is
// from a prior date. This will happen when the schedule is complete and we need to calculate the
// next run time.
let dtCalc = typeof this.calculatedDate !== 'undefined' && typeof this.calculatedDate.getTime === 'function' ? new Date(this.calculatedDate.getTime()).setHours(0, 0, 0, 0) : new Date(1970, 0, 1, 0, 0).getTime();
let recalc = !this.calculated;
if (!recalc && sod.getTime() !== dtCalc) recalc = true;
let schedType = sys.board.valueMaps.scheduleTypes.transform(sched.scheduleType);
if (!recalc && schedType.name !== 'runonce' && (this.endTime && this.endTime.getTime() < new Date().getTime() && this.startTime && this.startTime.getTime() < dtCalc)) {
recalc = true;
logger.info(`Recalculating expired schedule ${sched.id}`);
}
if (!recalc) return this.shouldBeOn;
//if (this.calculated && sod.getTime() === dtCalc) return this.shouldBeOn;
this.calculatedDate = new Date(new Date().setHours(0, 0, 0, 0));
if (sched.isActive === false || sched.disabled) return false;
let tt = sys.board.valueMaps.scheduleTimeTypes.transform(sched.startTimeType);
let times = schedType.name === 'runonce' ? this.calcScheduleDate(new Timestamp(sched.startDate), sched) : this.calcScheduleDate(state.time.clone(), sched);
if (times.startTime && times.endTime && times.endTime.getTime() > currentTime.getTime()) {
// Check to see if it should be on.
this.startTime = times.startTime;
this.endTime = times.endTime;
this.calculated = true;
return this.shouldBeOn;
}
else {
// Chances are that the current dow is not valid. Fast forward until we get a day that works. That will
// be the next scheduled run date.
if (schedType.name !== 'runonce' && sched.scheduleDays > 0) {
let schedDays = sys.board.valueMaps.scheduleDays.toArray();
let day = sod.clone().addHours(24);
let dow = day.getDay();
while (dow !== sod.getDay()) {
let sd = schedDays.find(elem => elem.dow === day.getDay());
if (typeof sd !== 'undefined' && (sched.scheduleDays & sd.bitval) !== 0) {
times = this.calcScheduleDate(day, sched);
break;
}
else day.addHours(24);
}
}
this.startTime = times.startTime;
this.endTime = times.endTime;
this.calculated = true;
}
return this.shouldBeOn;
} catch (err) {
this.calculated = true;
this.calculatedDate = new Date(new Date().setHours(0, 0, 0, 0));
this.startTime = null;
this.endTime = null;
}
}
}
export class ScheduleState extends EqState {
constructor(data: any, dataName?: string) { super(data, dataName); }
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 = sys.board.valueMaps.scheduleTimeTypes.transform(0);
if (typeof this.data.endTimeType === 'undefined') this.data.endTimeType = sys.board.valueMaps.scheduleTimeTypes.transform(0);
if (typeof this.data.display === 'undefined') this.data.display = sys.board.valueMaps.scheduleDisplayTypes.transform(0);
}
private _startDate: Date = new Date();
public get startDate(): Date { return this._startDate; }
public set startDate(val: Date) { this._startDate = val; this._saveStartDate(); }
private _saveStartDate() {
if (typeof this._startDate === 'undefined') this._startDate = new Date();
this.startDate.setHours(0, 0, 0, 0);
this.setDataVal('startDate', Timestamp.toISOLocal(this.startDate));
}
public dataName: string = 'schedule';
public get id(): number { return this.data.id; }
public set id(val: number) { this.data.id = val; }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', 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 circuit(): number { return this.data.circuit; }
public set circuit(val: number) { this.setDataVal('circuit', val); }
public get disabled(): boolean { return this.data.disabled; }
public set disabled(val: boolean) { this.setDataVal('disabled', val); }
public get triggered(): boolean { return this.data.triggered || false; }
public set triggered(val: boolean) { this.setDataVal('triggered', val); }
public get scheduleType(): number { return typeof (this.data.scheduleType) !== 'undefined' ? this.data.scheduleType.val : undefined; }
public set scheduleType(val: number) {
if (this.scheduleType !== val) {
this.data.scheduleType = sys.board.valueMaps.scheduleTypes.transform(val);
this.hasChanged = true;
}
}
public get schedGroup(): number { return this.data.schedGroup || 0; }
public set schedGroup(val: number) { this.setDataVal('schedGroup', val); }
public get startTimeType(): number { return typeof (this.data.startTimeType) !== 'undefined' ? this.data.startTimeType.val : -1; }
public set startTimeType(val: number) {
if (this.startTimeType !== val) {
this.data.startTimeType = sys.board.valueMaps.scheduleTimeTypes.transform(val);
this.hasChanged = true;
}
}
public get endTimeType(): number { return typeof (this.data.endTimeType) !== 'undefined' ? this.data.endTimeType.val : -1; }
public set endTimeType(val: number) {
if (this.endTimeType !== val) {
this.data.endTimeType = sys.board.valueMaps.scheduleTimeTypes.transform(val);
this.hasChanged = true;
}
}
public get scheduleDays(): number { return typeof (this.data.scheduleDays) !== 'undefined' ? this.data.scheduleDays.val : undefined; }
public set scheduleDays(val: number) {
if (this.scheduleDays !== val) {
this.data.scheduleDays = sys.board.valueMaps.scheduleDays.transform(val);
this.hasChanged = true;
}
}
public get heatSource(): number { return typeof (this.data.heatSource) !== 'undefined' ? this.data.heatSource.val : undefined; }
public set heatSource(val: number) {
if (this.heatSource !== val) {
this.data.heatSource = sys.board.valueMaps.heatSources.transform(val);
this.hasChanged = true;
}
}
public get display(): number { return typeof (this.data.display) !== 'undefined' ? this.data.display.val : undefined; }
public set display(val: number) {
if (this.display !== val) {
this.data.display = sys.board.valueMaps.scheduleDisplayTypes.transform(val);
this.hasChanged = true;
}
}
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 isOn(): boolean { return this.data.isOn; }
public set isOn(val: boolean) { this.setDataVal('isOn', val); }
public get manualPriorityActive(): boolean { return this.data.manualPriorityActive; }
public set manualPriorityActive(val: boolean) { this.setDataVal('manualPriorityActive', val); }
public get scheduleTime(): ScheduleTime { return new ScheduleTime(this.data, 'scheduleTime', this); }
public recalculate(force?: boolean) {
if (force === true) this.scheduleTime.calculated = false;
this.scheduleTime.calcSchedule(state.time, sys.schedules.getItemById(this.id));
}
public getExtended() {
let sched = this.get(true); // Always operate on a copy.
//if (typeof this.circuit !== 'undefined')
sched.circuit = state.circuits.getInterfaceById(this.circuit).get(true);
sched.clockMode = sys.board.valueMaps.clockModes.transform(sys.general.options.clockMode) || {};
if (typeof sched.schedGroup === 'undefined') {
let cfgSched = sys.schedules.getItemById(this.id, false);
sched.schedGroup = cfgSched ? cfgSched.schedGroup : 0;
}
return sched;
}
public emitEquipmentChange() {
// For schedules always emit the complete information
if (typeof (webApp) !== 'undefined' && webApp) {
if (this.hasChanged) this.emitData(this.dataName, this.getExtended());
this.hasChanged = false;
state._dirtyList.removeEqState(this);
}
}
}
export interface ICircuitGroupState {
id: number;
type: number;
name: string;
nameId?: number;
endTime: Timestamp;
isOn: boolean;
isActive: boolean;
dataName: string;
lightingTheme?: number;
showInFeatures?: boolean;
manualPriorityActive?: boolean;
get(bCopy?: boolean);
emitEquipmentChange();
}
export class CircuitGroupStateCollection extends EqStateCollection {
public createItem(data: any): CircuitGroupState { return new CircuitGroupState(data); }
public getInterfaceById(id: number) {
let iGroup: ICircuitGroupState = this.getItemById(id, false, { id: id, isActive: false });
if (iGroup.isActive === false) iGroup = state.lightGroups.getItemById(id, false, { id: id, isActive: false });
return iGroup;
}
public cleanupState() {
for (let i = this.data.length - 1; i >= 0; i--) {
if (isNaN(this.data[i].id)) this.data.splice(i, 1);
else {
if (typeof sys.circuitGroups.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
}
}
let cfg = sys.circuitGroups.toArray();
for (let i = 0; i < cfg.length; i++) {
let c = cfg[i];
let s = this.getItemById(cfg[i].id, true);
s.type = c.type;
s.name = c.name;
s.nameId = c.nameId;
s.showInFeatures = c.showInFeatures;
s.isActive = c.isActive;
}
}
}
export class CircuitGroupState extends EqState implements ICircuitGroupState, ICircuitState {
public dataName: string = 'circuitGroup';
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 nameId(): number { return this.data.nameId; }
public set nameId(val: number) { this.setDataVal('nameId', val); }
public get type(): number { return typeof this.data.type !== 'undefined' ? this.data.type.val : 0; }
public set type(val: number) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.circuitGroupTypes.transform(val);
this.hasChanged = true;
}
}
public get isOn(): boolean { return this.data.isOn; }
public set isOn(val: boolean) { this.setDataVal('isOn', val); }
public get priority(): string { return this.data.priority || 'manual' }
public set priority(val: string) { this.setDataVal('priority', val); }
public get endTime(): Timestamp {
if (typeof this.data.endTime === 'undefined') return undefined;
return new Timestamp(this.data.endTime);
}
public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('endTime', undefined); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get showInFeatures(): boolean { return typeof this.data.showInFeatures === 'undefined' ? true : this.data.showInFeatures; }
public set showInFeatures(val: boolean) { this.setDataVal('showInFeatures', val); }
public get manualPriorityActive(): boolean { return this.data.manualPriorityActive; }
public set manualPriorityActive(val: boolean) { this.setDataVal('manualPriorityActive', val); }
public getExtended() {
let sgrp = this.get(true); // Always operate on a copy.
if (typeof sgrp.showInFeatures === 'undefined') sgrp.showInFeatures = true;
let cgrp = sys.circuitGroups.getItemById(this.id);
sgrp.showInFeatures = this.showInFeatures = cgrp.showInFeatures;
sgrp.isActive = this.isActive = cgrp.isActive;
sgrp.circuits = [];
for (let i = 0; i < cgrp.circuits.length; i++) {
let cgc = cgrp.circuits.getItemByIndex(i).get(true);
cgc.circuit = state.circuits.getInterfaceById(cgc.circuit).get(true);
sgrp.circuits.push(cgc);
}
return sgrp;
}
public emitEquipmentChange() {
if (typeof (webApp) !== 'undefined' && webApp) {
if (this.hasChanged) this.emitData(this.dataName, this.getExtended());
this.hasChanged = false;
state._dirtyList.removeEqState(this);
}
}
public get(bcopy?: boolean): any {
let d = super.get(bcopy);
let cg = sys.circuitGroups.getItemById(this.id);
if (!cg.isActive) d.isActive = false;
else d.isActive = undefined;
return d;
}
}
export class LightGroupStateCollection extends EqStateCollection {
public createItem(data: any): LightGroupState { return new LightGroupState(data); }
public cleanupState() {
for (let i = this.data.length - 1; i >= 0; i--) {
if (isNaN(this.data[i].id)) this.data.splice(i, 1);
else {
if (typeof sys.lightGroups.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
}
}
let cfg = sys.lightGroups.toArray();
for (let i = 0; i < cfg.length; i++) {
let c = cfg[i];
let s = this.getItemById(cfg[i].id, true);
s.type = c.type;
s.name = c.name;
s.isActive = c.isActive;
s.action = 0;
s.endTime = undefined;
}
}
}
export class LightGroupState extends EqState implements ICircuitGroupState, ICircuitState {
public dataName = 'lightGroup';
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 action(): number { return typeof this.data.action !== 'undefined' ? this.data.action.val : 0; }
public set action(val: number) {
if (this.action !== val || typeof this.data.action === 'undefined') {
this.data.action = sys.board.valueMaps.circuitActions.transform(val);
this.hasChanged = true;
}
}
public get type(): number { return typeof this.data.type !== 'undefined' ? this.data.type.val : 0; }
public set type(val: number) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.circuitGroupTypes.transform(val);
this.hasChanged = true;
}
}
public get lightingTheme(): number { return typeof this.data.lightingTheme !== 'undefined' ? this.data.lightingTheme.val : 0; }
public set lightingTheme(val: number) {
if (this.lightingTheme !== val) {
this.data.lightingTheme = sys.board.valueMaps.lightThemes.transform(val);
this.hasChanged = true;
}
}
public get priority(): string { return this.data.priority || 'manual' }
public set priority(val: string) { this.setDataVal('priority', val); }
public get endTime(): Timestamp {
if (typeof this.data.endTime === 'undefined') return undefined;
return new Timestamp(this.data.endTime);
}
public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('endTime', undefined); }
public get isOn(): boolean { return this.data.isOn; }
public set isOn(val: boolean) { this.setDataVal('isOn', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get manualPriorityActive(): boolean { return this.data.manualPriorityActive; }
public set manualPriorityActive(val: boolean) { this.setDataVal('manualPriorityActive', val); }
public async setThemeAsync(val: number) { return sys.board.circuits.setLightThemeAsync; }
public getExtended() {
let sgrp = this.get(true); // Always operate on a copy.
sgrp.circuits = [];
if (typeof sgrp.lightingTheme === 'undefined') sgrp.lightingTheme = sys.board.valueMaps.lightThemes.transformByName('white');
if (typeof sgrp.action === 'undefined') sgrp.action = sys.board.valueMaps.circuitActions.transform(0);
let cgrp = sys.circuitGroups.getItemById(this.id);
for (let i = 0; i < cgrp.circuits.length; i++) {
let lgc = cgrp.circuits.getItemByIndex(i).get(true);
lgc.circuit = state.circuits.getInterfaceById(lgc.circuit).get(true);
sgrp.circuits.push(lgc);
}
return sgrp;
}
public emitEquipmentChange() {
if (typeof (webApp) !== 'undefined' && webApp) {
if (this.hasChanged) this.emitData(typeof this.dataName !== 'undefined' ? this.dataName : 'lightGroup', this.getExtended());
this.hasChanged = false;
state._dirtyList.removeEqState(this);
}
}
}
export class BodyTempStateCollection extends EqStateCollection {
public createItem(data: any): BodyTempState { return new BodyTempState(data); }
public getBodyIsOn() {
for (let i = 0; i < this.data.length; i++) {
if (this.data[i].isOn) return this.createItem(this.data[i]);
}
return undefined;
}
public getBodyByCircuitId(circuitId: number) {
let b = this.data.find(x => x.circuit === circuitId);
if (typeof b === 'undefined') {
let circ = sys.circuits.getItemById(circuitId);
// Find our body by circuit function.
let cfn = sys.board.valueMaps.circuitFunctions.get(circ.type);
if (typeof cfn.body !== 'undefined') b = this.data.find(x => x.id === cfn.body);
}
return typeof b !== 'undefined' ? this.createItem(b) : undefined;
}
public cleanupState() {
for (let i = this.data.length - 1; i >= 0; i--) {
if (isNaN(this.data[i].id)) this.data.splice(i, 1);
else {
if (typeof sys.bodies.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
}
}
this.sortById();
}
}
// RKS: This is an interesting object. We are doing some gymnastics with it to comply
// with type safety.
export class BodyHeaterTypeStateCollection extends EqStateCollection {
public createItem(data: any): BodyHeaterTypeState { return new BodyHeaterTypeState(data); }
}
export class BodyHeaterTypeState extends EqState {
public get typeId(): number { return this.data.typeId; }
public set typeId(val: number) { this.setDataVal('typeId', val); }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
}
export class BodyTempState extends EqState {
public dataName = 'bodyTempState';
public initData() {
if (typeof this.data.heaterOptions === 'undefined') this.data.heaterOptions = { total: 0 };
if (typeof this.data.isCovered === 'undefined') this.data.isCovered = false;
if (typeof this.heaterCooldownDelay === 'undefined') this.data.heaterCooldownDelay = false;
if (typeof this.data.startDelay === 'undefined') this.data.startDelay = false;
if (typeof this.data.stopDelay === 'undefined') this.data.stopDelay = false;
if (typeof this.data.showInDashboard === 'undefined') this.data.showInDashboard = true;
if (typeof this.data.heatMode === 'undefined') this.data.heatMode = sys.board.valueMaps.heatModes.transform(0);
}
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 name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get temp(): number { return this.data.temp; }
public set temp(val: number) { this.setDataVal('temp', val); }
public get type(): number { return typeof (this.data.type) !== 'undefined' ? this.data.type.val : -1; }
public set type(val: number) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.bodyTypes.transform(val);
this.hasChanged = true;
}
}
public get heatMode(): number { return typeof (this.data.heatMode) !== 'undefined' ? this.data.heatMode.val : -1; }
public set heatMode(val: number) {
if (this.heatMode !== val) {
this.data.heatMode = sys.board.valueMaps.heatModes.transform(val);
this.hasChanged = true;
}
}
public get heatStatus(): number { return typeof (this.data.heatStatus) !== 'undefined' ? this.data.heatStatus.val : -1; }
public set heatStatus(val: number) {
if (this.heatStatus !== val) {
this.data.heatStatus = sys.board.valueMaps.heatStatus.transform(val);
this.hasChanged = true;
}
}
public get setPoint(): number { return this.data.setPoint; }
public set setPoint(val: number) { this.setDataVal('setPoint', 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 isOn(): boolean { return this.data.isOn; }
public set isOn(val: boolean) { this.setDataVal('isOn', val); }
public get startDelay(): boolean { return this.data.startDelay; }
public set startDelay(val: boolean) { this.setDataVal('startDelay', val); }
public get stopDelay(): boolean { return this.data.stopDelay; }
public set stopDelay(val: boolean) { this.setDataVal('stopDelay', val); }
public get showInDashboard(): boolean { return this.data.showInDashboard; }
public set showInDashboard(val: boolean) { this.setDataVal('showInDashboard', val); }
public get isCovered(): boolean { return this.data.isCovered; }
public set isCovered(val: boolean) { this.setDataVal('isCovered', val); }
// RKS: Heater cooldown delays force the current valve and body configuration until the
// heater cooldown expires. This occurs at the pool level but it is triggered by the heater attached
// to the body. Unfortunately, I think we can only detect this condition in Nixie as there really isn't an
// indicator with Pentair OCPs. This is triggered in NixieBoard and managed by the delayMgr.
public get heaterCooldownDelay(): boolean { return this.data.heaterCooldownDelay; }
public set heaterCooldownDelay(val: boolean) { this.setDataVal('heaterCooldownDelay', val); }
public get manualFreezeOverride(): boolean { return this.data.manualFreezeOverride || false; }
public set manualFreezeOverride(val: boolean) { this.setDataVal('manualFreezeOverride', val); }
public emitData(name: string, data: any) { webApp.emitToClients('body', this.data); }
// RKS: This is a very interesting object because we have a varied object. Type safety rules should not apply
// here as the heater types are specific to the installed equipment. The reason is because it has no meaning without the body and the calculation of it should
// be performed when the body or heater options change. However, it shouldn't emit unless
// there truly is a change but the emit needs to occur at the body temp state level.
public get heaterOptions(): any { return typeof this.data.heaterOptions === 'undefined' ? this.setDataVal('heaterOptions', { total: 0 }) : this.data.heaterOptions; }
public set heaterOptions(val: any) {
// We are doing this simply to maintain the proper automatic emits. We don't want the emit to happen unnecessarily so lets
// get creative on the object and dirty up the body only when needed.
let opts = this.heaterOptions; // Calling this here will make sure we have a data object. The getter adds it if it doesn't exist.
for (let s in val) {
if (opts[s] !== val[s]) {
opts[s] = val[s];
this.hasChanged = true;
}
}
// Spin it around and run it the other way and remove properties that are not in the incoming. Theorhetically we could
// simply set the attribute but we have more control this way. This also expects that we are doing counts in the
// output and the setting object must coordinate with this code.
for (let s in opts) {
if (typeof val[s] === 'undefined') delete opts[s];
}
}
}
export class TemperatureState extends EqState {
public initData() {
if (typeof this.data.units === 'undefined') this.data.units = sys.board.valueMaps.tempUnits.transform(0);
}
public get waterSensor1(): number { return this.data.waterSensor1; }
public set waterSensor1(val: number) { this.setDataVal('waterSensor1', val); }
public get waterSensor2(): number { return this.data.waterSensor2; }
public set waterSensor2(val: number) { this.setDataVal('waterSensor2', val); }
public get waterSensor3(): number { return this.data.waterSensor3; }
public set waterSensor3(val: number) { this.setDataVal('waterSensor3', val); }
public get waterSensor4(): number { return this.data.waterSensor4; }
public set waterSensor4(val: number) { this.setDataVal('waterSensor4', val); }
public get solarSensor1(): number { return this.data.solar; }
public set solarSensor1(val: number) { this.setDataVal('solar', val); }
public get solarSensor2(): number { return this.data.solarSensor2; }
public set solarSensor2(val: number) { this.setDataVal('solarSensor2', val); }
public get solarSensor3(): number { return this.data.solarSensor3; }
public set solarSensor3(val: number) { this.setDataVal('solarSensor3', val); }
public get solarSensor4(): number { return this.data.solarSensor4; }
public set solarSensor4(val: number) { this.setDataVal('solarSensor4', val); }
public get bodies(): BodyTempStateCollection { return new BodyTempStateCollection(this.data, 'bodies'); }
public get air(): number { return this.data.air; }
public set air(val: number) { this.setDataVal('air', val); }
public get solar(): number { return this.data.solar; }
public set solar(val: number) { this.setDataVal('solar', val); }
public get units(): number { return typeof this.data.units !== 'undefined' ? this.data.units.val : -1; }
public set units(val: number) {
if (this.units !== val) {
this.data.units = sys.board.valueMaps.tempUnits.transform(val);
this.hasChanged = true;
}
}
public cleanupState() {
this.bodies.cleanupState();
}
}
export class HeaterStateCollection extends EqStateCollection {
public createItem(data: any): HeaterState { return new HeaterState(data); }
public cleanupState() {
for (let i = this.data.length - 1; i >= 0; i--) {
if (isNaN(this.data[i].id)) {
logger.info(`Removed Invalid Heater ${this.data[i].id}-${this.data[i].name}`);
this.data.splice(i, 1);
}
else {
if (typeof sys.heaters.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
}
}
let cfg = sys.heaters.toArray();
for (let i = 0; i < cfg.length; i++) {
let c = cfg[i];
let s = this.getItemById(cfg[i].id, true);
s.name = c.name;
s.type = c.type;
}
}
}
export class HeaterState extends EqState {
public dataName: string = 'heater';
public initData() {
if (typeof this.data.startupDelay === 'undefined') this.data.startupDelay = false;
if (typeof this.data.shutdownDelay === 'undefined') this.data.shutdownDelay = false;
}
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 isOn(): boolean { return this.data.isOn; }
public set isOn(val: boolean) {
if (val !== this.data.isOn) {
if (val) this.startTime = new Timestamp();
else this.endTime = new Timestamp();
}
this.setDataVal('isOn', val);
}
public get startTime(): Timestamp {
if (typeof this.data.startTime === 'undefined') return undefined;
return new Timestamp(this.data.startTime);
}
public set startTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('startTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('startTime', undefined); }
public get endTime(): Timestamp {
if (typeof this.data.endTime === 'undefined') return undefined;
return new Timestamp(this.data.endTime);
}
public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('endTime', undefined); }
public get isCooling(): boolean { return this.data.isCooling; }
public set isCooling(val: boolean) { this.setDataVal('isCooling', val); }
public get type(): number | any { return typeof this.data.type !== 'undefined' ? this.data.type.val : 0; }
public set type(val: number | any) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.heaterTypes.transform(val);
this.hasChanged = true;
}
}
public get commStatus(): number { return this.data.commStatus; }
public set commStatus(val: number) {
if (this.commStatus !== val) {
this.data.commStatus = sys.board.valueMaps.equipmentCommStatus.transform(val);
this.hasChanged = true;
}
}
public get prevHeaterOffTemp(): number { return this.data.prevHeaterOffTemp; }
public set prevHeaterOffTemp(val: number) {
if (this.prevHeaterOffTemp !== val) {
this.data.prevHeaterOffTemp = val;
if (typeof val === 'undefined') delete this.data.prevHeaterOffTemp;
}
}
public get startupDelay(): boolean { return this.data.startupDelay; }
public set startupDelay(val: boolean) { this.setDataVal('startupDelay', val); }
public get shutdownDelay(): boolean { return this.data.shutdownDelay; }
public set shutdownDelay(val: boolean) { this.setDataVal('shutdownDelay', val); }
public get bodyId(): number { return this.data.bodyId || 0 }
public set bodyId(val: number) { this.setDataVal('bodyId', val); }
}
export class FeatureStateCollection extends EqStateCollection {
public createItem(data: any): FeatureState { return new FeatureState(data); }
public async setFeatureStateAsync(id: number, val: boolean) { return sys.board.features.setFeatureStateAsync(id, val); }
public async toggleFeatureStateAsync(id: number) { return sys.board.features.toggleFeatureStateAsync(id); }
public cleanupState() {
for (let i = this.data.length - 1; i >= 0; i--) {
if (isNaN(this.data[i].id)) this.data.splice(i, 1);
else {
if (typeof sys.features.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
}
}
let cfg = sys.features.toArray();
for (let i = 0; i < cfg.length; i++) {
let c = cfg[i];
let s = this.getItemById(cfg[i].id, true);
s.type = c.type;
s.name = c.name;
s.nameId = c.nameId;
s.showInFeatures = c.showInFeatures;
}
}
}
export class FeatureState extends EqState implements ICircuitState {
public dataName: string = 'feature';
public initData() {
if (typeof this.data.freezeProtect === 'undefined') this.data.freezeProtect = false;
}
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 nameId(): number { return this.data.nameId; }
public set nameId(val: number) { this.setDataVal('nameId', val); }
public get isOn(): boolean { return this.data.isOn; }
public set isOn(val: boolean) { this.setDataVal('isOn', val); }
public get type() { return typeof this.data.type !== 'undefined' ? this.data.type.val : -1; }
public set type(val: number) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.featureFunctions.transform(val);
this.hasChanged = true;
}
}
public get showInFeatures(): boolean { return this.data.showInFeatures; }
public set showInFeatures(val: boolean) { this.setDataVal('showInFeatures', val); }
public get priority(): string { return this.data.priority || 'manual' }
public set priority(val: string) { this.setDataVal('priority', val); }
public get endTime(): Timestamp {
if (typeof this.data.endTime === 'undefined') return undefined;
return new Timestamp(this.data.endTime);
}
public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('endTime', undefined); }
// This property will be set if the system has turn this feature on for freeze protection reasons. We have no way of knowing when Pentair does this but
// need to know (so we can shut it off) if we have done this.
public get freezeProtect(): boolean { return this.data.freezeProtect; }
public set freezeProtect(val: boolean) { this.setDataVal('freezeProtect', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
public get manualPriorityActive(): boolean { return this.data.manualPriorityActive; }
public set manualPriorityActive(val: boolean) { this.setDataVal('manualPriorityActive', val); }
}
export class VirtualCircuitState extends EqState implements ICircuitState {
public dataName: string = 'virtualCircuit';
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 nameId(): number { return this.data.nameId; }
public set nameId(val: number) { this.setDataVal('nameId', val); }
public get isOn(): boolean { return this.data.isOn; }
public set isOn(val: boolean) { this.setDataVal('isOn', val); }
public get priority(): string { return 'manual' } // These are always manual priority
public set priority(val: string) { ; }
public get type() { return typeof this.data.type !== 'undefined' ? this.data.type.val : -1; }
public set type(val: number) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.virtualCircuits.transform(val);
this.hasChanged = true;
}
}
public get endTime(): Timestamp {
if (typeof this.data.endTime === 'undefined') return undefined;
return new Timestamp(this.data.endTime);
}
public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('endTime', undefined); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
}
export class VirtualCircuitStateCollection extends EqStateCollection {
public createItem(data: any): VirtualCircuitState { return new VirtualCircuitState(data); }
}
export class CircuitStateCollection extends EqStateCollection {
public createItem(data: any): CircuitState { return new CircuitState(data); }
public setCircuitStateAsync(id: number, val: boolean): Promise { return sys.board.circuits.setCircuitStateAsync(id, val); }
public async toggleCircuitStateAsync(id: number) { return sys.board.circuits.toggleCircuitStateAsync(id); }
public async setLightThemeAsync(id: number, theme: number) { return sys.board.circuits.setLightThemeAsync(id, theme); }
public getInterfaceById(id: number, add?: boolean): ICircuitState {
let iCircuit: ICircuitState = null;
if (sys.board.equipmentIds.virtualCircuits.isInRange(id))
iCircuit = state.virtualCircuits.getItemById(id, add);
else if (sys.board.equipmentIds.circuitGroups.isInRange(id)) {
iCircuit = state.circuitGroups.getInterfaceById(id);
}
else if (sys.board.equipmentIds.features.isInRange(id))
iCircuit = state.features.getItemById(id, add);
else
iCircuit = state.circuits.getItemById(id, add);
return iCircuit;
}
public cleanupState() {
for (let i = this.data.length - 1; i >= 0; i--) {
if (isNaN(this.data[i].id)) this.data.splice(i, 1);
else {
if (typeof sys.circuits.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
}
}
let cfg = sys.circuits.toArray();
for (let i = 0; i < cfg.length; i++) {
let c = cfg[i];
let s = this.getItemById(cfg[i].id, true);
s.type = c.type;
s.name = c.name;
s.nameId = c.nameId;
s.showInFeatures = c.showInFeatures;
}
}
}
export class CircuitState extends EqState implements ICircuitState {
public dataName = 'circuit';
public initData() {
if (typeof this.data.freezeProtect === 'undefined') this.data.freezeProtect = false;
if (typeof this.data.action === 'undefined') this.data.action = sys.board.valueMaps.circuitActions.transform(0);
if (typeof this.data.type === 'undefined') this.data.type = sys.board.valueMaps.circuitFunctions.transform(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 nameId(): number { return this.data.nameId; }
public set nameId(val: number) { this.setDataVal('nameId', val); }
public get action(): number { return typeof this.data.action !== 'undefined' ? this.data.action.val : 0; }
public set action(val: number) {
if (this.action !== val || typeof this.data.action === 'undefined') {
this.data.action = sys.board.valueMaps.circuitActions.transform(val);
this.hasChanged = true;
}
}
public get priority(): string { return this.data.priority; }
public set priority(val: string) { this.setDataVal('priority', val); }
public get showInFeatures(): boolean { return this.data.showInFeatures; }
public set showInFeatures(val: boolean) { this.setDataVal('showInFeatures', val); }
public get isOn(): boolean { return this.data.isOn; }
public set isOn(val: boolean) {
if (val && !this.data.isOn) this.startTime = new Timestamp();
else if (!val) this.startTime = undefined;
this.setDataVal('isOn', val);
}
public get type() { return typeof (this.data.type) !== 'undefined' ? this.data.type.val : -1; }
public set type(val: number) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.circuitFunctions.transform(val);
this.hasChanged = true;
}
}
public get lightingTheme(): number { return typeof this.data.lightingTheme !== 'undefined' ? this.data.lightingTheme.val : 255; }
public set lightingTheme(val: number) {
if (this.lightingTheme !== val) {
// Force this to undefined when we are a circuit without a theme.
if (typeof val === 'undefined') this.data.lightingTheme = undefined;
else this.data.lightingTheme = sys.board.valueMaps.lightThemes.transform(val);
this.hasChanged = true;
}
}
public get level(): number { return this.data.level; }
public set level(val: number) { this.setDataVal('level', val); }
public get color(): { red: number; green: number; blue: number } { return this.data.color; }
public set color(val: { red: number; green: number; blue: number }) {
if (typeof val === 'undefined' || val === null) this.setDataVal('color', undefined);
else this.setDataVal('color', { red: val.red, green: val.green, blue: val.blue });
}
public get commStatus(): number { return this.data.commStatus; }
public set commStatus(val: number) {
if (this.commStatus !== val) {
this.data.commStatus = sys.board.valueMaps.equipmentCommStatus.transform(val);
this.hasChanged = true;
}
}
public get scheduled(): boolean { return this.data.scheduled || false }
public set scheduled(val: boolean) { this.setDataVal('scheduled', val); }
public get startTime(): Timestamp {
if (typeof this.data.startTime === 'undefined') return undefined;
return new Timestamp(this.data.startTime);
}
public set startTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('startTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('startTime', undefined); }
public get endTime(): Timestamp {
if (typeof this.data.endTime === 'undefined') return undefined;
return new Timestamp(this.data.endTime);
}
public set endTime(val: Timestamp) { typeof val !== 'undefined' ? this.setDataVal('endTime', Timestamp.toISOLocal(val.toDate())) : this.setDataVal('endTime', undefined); }
// This property will be set if the system has turn this circuit on for freeze protection reasons. We have no way of knowing when Pentair does this but
// need to know (so we can shut it off) if we have done this.
public get freezeProtect(): boolean { return this.data.freezeProtect; }
public set freezeProtect(val: boolean) { this.setDataVal('freezeProtect', val); }
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
// The properties below are for delays and lockouts. Manual or scheduled
// actions cannot be performed when the flags below are set.
public get startDelay(): boolean { return this.data.startDelay; }
public set startDelay(val: boolean) { this.setDataVal('startDelay', val); }
public get stopDelay(): boolean { return this.data.stopDelay; }
public set stopDelay(val: boolean) { this.setDataVal('stopDelay', val); }
public get lockoutOn(): boolean { return this.data.lockoutOn; }
public set lockoutOn(val: boolean) { this.setDataVal('lockoutOn', val); }
public get lockoutOff(): boolean { return this.data.lockoutOff; }
public set lockoutOff(val: boolean) { this.setDataVal('lockoutOff', val); }
public get manualPriorityActive(): boolean { return this.data.manualPriorityActive; }
public set manualPriorityActive(val: boolean) { this.setDataVal('manualPriorityActive', val); }
}
export class ValveStateCollection extends EqStateCollection {
public createItem(data: any): ValveState { return new ValveState(data); }
public cleanupState() {
for (let i = this.data.length - 1; i >= 0; i--) {
if (isNaN(this.data[i].id)) this.data.splice(i, 1);
else {
if (typeof sys.valves.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
}
}
let cfg = sys.valves.toArray();
for (let i = 0; i < cfg.length; i++) {
let c = cfg[i];
let s = this.getItemById(cfg[i].id, true);
s.name = c.name;
s.type = c.type;
}
}
}
export class ValveState extends EqState {
public dataName: string = 'valve';
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 type(): number { return typeof this.data.type !== 'undefined' ? this.data.type.val : -1; }
public set type(val: number) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.valveTypes.transform(val);
this.hasChanged = true;
}
}
public get isDiverted(): boolean { return utils.makeBool(this.data.isDiverted); }
public set isDiverted(val: boolean) { this.setDataVal('isDiverted', val); }
public get commStatus(): number { return this.data.commStatus; }
public set commStatus(val: number) {
if (this.commStatus !== val) {
this.data.commStatus = sys.board.valueMaps.equipmentCommStatus.transform(val);
this.hasChanged = true;
}
}
public getExtended(): any {
let valve = sys.valves.getItemById(this.id);
let vstate = this.get(true);
if (valve.circuit !== 256) vstate.circuit = state.circuits.getInterfaceById(valve.circuit).get(true);
vstate.isIntake = utils.makeBool(valve.isIntake);
vstate.isReturn = utils.makeBool(valve.isReturn);
// vstate.isVirtual = utils.makeBool(valve.isVirtual);
vstate.isActive = utils.makeBool(valve.isActive);
vstate.pinId = valve.pinId;
return vstate;
}
public emitEquipmentChange() {
if (typeof (webApp) !== 'undefined' && webApp) {
if (this.hasChanged) this.emitData(this.dataName, this.getExtended());
this.hasChanged = false;
state._dirtyList.removeEqState(this);
}
}
}
export class CoverStateCollection extends EqStateCollection {
public createItem(data: any): CoverState { return new CoverState(data); }
}
export class CoverState extends EqState {
public dataName: string = 'cover';
public initData() {
if (typeof this.data.isClosed === 'undefined') this.data.isClosed = false;
}
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 isClosed(): boolean { return this.data.isClosed; }
public set isClosed(val: boolean) { this.setDataVal('isClosed', val); }
// Rule 18: mirror config fields that dashPanel renders on the Controllers/Covers tab so the
// UI doesn't lag between a config PUT and the next OCP rebroadcast.
public get isActive(): boolean { return this.data.isActive; }
public set isActive(val: boolean) { this.setDataVal('isActive', 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 normallyOn(): boolean { return this.data.normallyOn; }
public set normallyOn(val: boolean) { this.setDataVal('normallyOn', val); }
public get chlorActive(): boolean { return this.data.chlorActive; }
public set chlorActive(val: boolean) { this.setDataVal('chlorActive', val); }
public get chlorOutput(): number { return this.data.chlorOutput; }
public set chlorOutput(val: number) { this.setDataVal('chlorOutput', val); }
}
export class ChlorinatorStateCollection extends EqStateCollection {
public superChlor: { id: number, lastDispatch: number, reference: number }[] = [];
public getSuperChlor(id: number): { id: number, lastDispatch: number, reference: number } {
let sc = this.superChlor.find(elem => id === elem.id);
if (typeof sc === 'undefined') {
sc = { id: id, lastDispatch: 0, reference: 0 };
this.superChlor.push(sc);
}
return sc;
}
public createItem(data: any): ChlorinatorState { return new ChlorinatorState(data); }
public cleanupState() {
for (let i = this.data.length - 1; i >= 0; i--) {
if (isNaN(this.data[i].id)) this.data.splice(i, 1);
else {
if (typeof sys.chlorinators.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
}
}
let cfg = sys.chlorinators.toArray();
for (let i = 0; i < cfg.length; i++) {
let c = cfg[i];
let s = this.getItemById(cfg[i].id, true);
s.type = c.type;
s.model = c.model;
s.name = c.name;
s.isActive = c.isActive;
}
}
}
export class ChlorinatorState extends EqState {
public initData() {
if (typeof this.data.disabled === 'undefined') this.data.disabled = false;
// This has been deprecated because Nixie is now in control of all "virtual" chlorinators.
if (typeof this.data.virtualControllerStatus !== 'undefined') delete this.data.virtualControllerStatus;
}
public dataName: string = 'chlorinator';
// The lastComm property has a fundamental flaw. Although, the structure is
// not dirtied where the emitter sends out a message on each lastComm, the persistence proxy is
// triggered by this. We need to find a way that the property change does not trigger persistence.
// RG - Fixed with "false" persistence flag. 2/10/2020
public get lastComm(): number { return this.data.lastComm; }
public set lastComm(val: number) { this.setDataVal('lastComm', val, false); }
public get id(): number { return this.data.id; }
public set id(val: number) { this.data.id = 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 currentOutput(): number { return this.data.currentOutput || 0; }
public set currentOutput(val: number) { this.setDataVal('currentOutput', val); }
public get setPointForCurrentBody() {
let body = state.temps.bodies.getBodyIsOn();
if (typeof body !== 'undefined') {
if (body.circuit === 1) return this.spaSetpoint;
return this.poolSetpoint;
}
return 0;
}
public get targetOutput(): number { return this.data.targetOutput; }
public set targetOutput(val: number) { this.setDataVal('targetOutput', val); }
public get status(): number {
return typeof (this.data.status) !== 'undefined' ? this.data.status.val : -1;
}
public set status(val: number) {
if (this.status !== val) {
this.data.status = sys.board.valueMaps.chlorinatorStatus.transform(val);
this.hasChanged = true;
}
}
public get type(): number { return typeof (this.data.type) !== 'undefined' ? this.data.type.val : -1; }
public set type(val: number) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.chlorinatorType.transform(val);
this.hasChanged = true;
}
}
public get model(): number { return typeof (this.data.model) !== 'undefined' ? this.data.model.val : 0; }
public set model(val: number) {
if (this.model !== val) {
this.data.model = sys.board.valueMaps.chlorinatorModel.transform(val);
this.hasChanged = true;
}
}
public get body(): number { return typeof (this.data.body) !== 'undefined' ? this.data.body.val : -1; }
public set body(val: number) {
if (this.body !== val) {
this.data.body = sys.board.valueMaps.bodies.transform(val);
this.hasChanged = true;
}
}
public get 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) { this.setDataVal('saltTarget', val); }
public get saltRequired(): number { return this.data.saltRequired; }
public get saltLevel(): number { return this.data.saltLevel; }
public set saltLevel(val: number) {
if (this.saltLevel !== val) {
this.setDataVal('saltLevel', val);
this.calcSaltRequired();
}
}
public get superChlor(): boolean { return this.data.superChlor; }
public set superChlor(val: boolean) {
this.setDataVal('superChlor', val);
if (!val && this.superChlorRemaining > 0) this.superChlorRemaining = 0;
}
public get superChlorRemaining(): number { return this.data.superChlorRemaining || 0; }
public set superChlorRemaining(val: number) {
if (val === this.data.superChlorRemaining) return;
let remaining: number;
let sc = state.chlorinators.getSuperChlor(this.id);
let chlor = sys.chlorinators.getItemById(this.id);
if (chlor.master === 1) {
// If we are 10 seconds different then we need to send it off and save the data.
if (Math.floor(val / 10) !== Math.floor(this.superChlorRemaining / 10)) {
this.hasChanged = true;
remaining = val;
sc.reference = Math.floor(new Date().getTime() / 1000);
this.setDataVal('superChlorRemaining', remaining);
}
else if (val <= 0)
remaining = 0;
else
remaining = this.superChlorRemaining;
}
else if (chlor.master === 2) {
// If we are 10 seconds different then we need to send it off and save the data.
if (Math.floor(val / 10) !== Math.floor(this.superChlorRemaining / 10)) {
this.hasChanged = true;
remaining = val;
sc.reference = Math.floor(new Date().getTime() / 1000);
this.setDataVal('superChlorRemaining', remaining);
}
}
else if (sys.controllerType === 'intellicenter') {
// Trim the seconds off both of these as we will be keeping the seconds separately since this
// only reports in minutes. That way our seconds become self healing.
if (Math.ceil(this.superChlorRemaining / 60) * 60 !== val) {
sc.reference = Math.floor(new Date().getTime() / 1000); // Get the epoc and strip the milliseconds.
this.hasChanged = true;
}
let secs = Math.floor(new Date().getTime() / 1000) - sc.reference;
remaining = Math.max(0, val - Math.min(secs, 60));
if (sc.lastDispatch - 5 > remaining) this.hasChanged = true;
this.data.superChlorRemaining = remaining;
}
else {
// *Touch only reports superchlor hours remaining.
// If we have the same hours as existing, retain the mins + secs
if (Math.ceil(this.superChlorRemaining / 3600) * 60 !== val / 60) {
sc.reference = Math.floor(new Date().getTime() / 1000); // Get the epoc and strip the milliseconds.
this.hasChanged = true;
}
let secs = Math.floor(new Date().getTime() / 1000) - sc.reference;
remaining = Math.max(0, val - Math.min(secs, 3600));
if (sc.lastDispatch - 5 > remaining) this.hasChanged = true;
this.data.superChlorRemaining = remaining;
}
if (this.hasChanged) sc.lastDispatch = remaining;
this.setDataVal('superChlor', remaining > 0);
chlor.superChlor = remaining > 0;
}
public calcSaltRequired(saltTarget?: number) : number {
if (typeof saltTarget === 'undefined') saltTarget = sys.chlorinators.getItemById(this.id, false).saltTarget || 0;
let saltRequired = 0;
//this.data.saltLevel = val;
// Calculate the salt required.
let capacity = 0;
for (let i = 0; i < sys.bodies.length; i++) {
let body = sys.bodies.getItemById(i + 1);
if (this.body === 32)
capacity = Math.max(body.capacity, capacity);
else if (this.body === 0 && body.id === 1)
capacity = Math.max(body.capacity, capacity);
else if (this.body === 1 && body.id === 2)
capacity = Math.max(body.capacity, capacity);
}
if (capacity > 0 && this.saltLevel < saltTarget) {
// Salt requirements calculation.
// Target - SaltLevel = NeededSalt = 3400 - 2900 = 500ppm
// So to raise 120ppm you need to add 1lb per 1000 gal.
// (NeededSalt/120ppm) * (MaxBody/1000) = (500/120) * (33000/1000) = 137.5lbs of salt required to hit target.
let dec = Math.pow(10, 2);
saltRequired = Math.round((((saltTarget - this.saltLevel) / 120) * (capacity / 1000)) * dec) / dec;
if (this.saltRequired < 0) saltRequired = 0;
}
this.setDataVal('saltRequired', saltRequired);
return saltRequired;
}
public getEmitData() { return this.getExtended(); }
public getExtended(): any {
let schlor = this.get(true);
let chlor = sys.chlorinators.getItemById(this.id, false);
schlor.saltTarget = chlor.saltTarget;
schlor.lockSetpoints = chlor.disabled || chlor.isDosing;
return schlor;
}
}
export class ChemControllerStateCollection extends EqStateCollection {
public createItem(data: any): ChemControllerState { return new ChemControllerState(data); }
public cleanupState() {
for (let i = this.data.length - 1; i >= 0; i--) {
if (isNaN(this.data[i].id)) this.data.splice(i, 1);
else {
if (typeof sys.chemControllers.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
}
}
// Make sure we have at least the items that exist in the config.
let cfg = sys.chemControllers.toArray();
for (let i = 0; i < cfg.length; i++) {
let c = cfg[i];
let s = this.getItemById(cfg[i].id, true);
s.address = c.address;
s.body = c.body;
s.name = c.name;
s.type = c.type;
s.isActive = c.isActive;
}
}
}
export class ChemDoserStateCollection extends EqStateCollection {
public createItem(data: any): ChemDoserState { return new ChemDoserState(data); }
public cleanupState() {
for (let i = this.data.length - 1; i >= 0; i--) {
if (isNaN(this.data[i].id)) this.data.splice(i, 1);
else {
if (typeof sys.chemControllers.find(elem => elem.id === this.data[i].id) === 'undefined') this.removeItemById(this.data[i].id);
}
}
// Make sure we have at least the items that exist in the config.
let cfg = sys.chemControllers.toArray();
for (let i = 0; i < cfg.length; i++) {
let c = cfg[i];
let s = this.getItemById(cfg[i].id, true);
s.body = c.body;
s.name = c.name;
s.type = c.type;
s.isActive = c.isActive;
}
}
}
export interface IChemControllerState {
flowDetected: boolean;
isBodyOn: boolean;
emitEquipmentChange: () => void;
}
export interface IChemicalState {
enabled: boolean;
currentDose: ChemicalDoseState;
manualDosing: boolean;
manualMixing: boolean;
dosingTimeRemaining: number;
dosingVolumeRemaining: number;
volumeDosed: number;
timeDosed: number;
endDose: (dtEnd?: Date, status?: string, volumeDosed?: number, timeDosed?: number) => ChemicalDoseState;
appendDose: (volumeDosed: number, timeDosed: number) => ChemicalDoseState;
tank: ChemicalTankState;
pump: ChemicalPumpState;
doseHistory: ChemicalDoseState[];
freezeProtect: boolean;
mixTimeRemaining: number;
delayTimeRemaining: number;
flowDelay: boolean;
dailyVolumeDosed: number;
chemType: string;
dosingStatus: number;
chemController: IChemControllerState;
chlor?: ChemicalChlorState;
}
export class ChemDoserState extends EqState implements IChemicalState, IChemControllerState {
public initData() {
if (typeof this.data.flowDetected === 'undefined') this.data.flowDetected = false;
if (typeof this.data.flowSensor === 'undefined') this.data.flowSensor = {};
if (typeof this.data.alarms === 'undefined') { let a = this.alarms; }
if (typeof this.data.warnings === 'undefined') { let w = this.warnings; }
if (typeof this.data.tank === 'undefined') this.data.tank = { capacity: 0, level: 0, units: 0 };
if (typeof this.data.pump === 'undefined') this.data.pump = { isDosing: false };
if (typeof this.data.dosingTimeRemaining === 'undefined') this.data.dosingTimeRemaining = 0;
if (typeof this.data.delayTimeRemaining === 'undefined') this.data.delayTimeRemaining = 0;
if (typeof this.data.dosingVolumeRemaining === 'undefined') this.data.dosingVolumeRemaining = 0;
if (typeof this.data.doseVolume === 'undefined') this.data.doseVolume = 0;
if (typeof this.data.doseTime === 'undefined') this.data.doseTime = 0;
if (typeof this.data.lockout === 'undefined') this.data.lockout = false;
if (typeof this.data.level === 'undefined') this.data.level = 0;
if (typeof this.data.mixTimeRemaining === 'undefined') this.data.mixTimeRemaining = 0;
if (typeof this.data.dailyLimitReached === 'undefined') this.data.dailyLimitReached = false;
if (typeof this.data.manualDosing === 'undefined') this.data.manualDosing = false;
if (typeof this.data.manualMixing === 'undefined') this.data.manualMixing = false;
if (typeof this.data.flowDelay === 'undefined') this.data.flowDelay = false;
if (typeof this.data.dosingStatus === 'undefined') this.dosingStatus = 2;
if (typeof this.data.enabled === 'undefined') this.data.enabled = true;
if (typeof this.data.freezeProtect === 'undefined') this.data.freezeProtect = false;
if (typeof this.data.setpoint === 'undefined') this.data.setpoint = 100;
if (typeof this.data.suspendDosing === 'undefined') this.data.suspendDosing = false;
}
public dataName: string = 'chemDoser';
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', 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 setpoint(): number { return this.data.setpoint; }
public set setpoint(val: number) { this.setDataVal('setpoint', val); }
public get lastComm(): number { return this.data.lastComm || 0; }
public set lastComm(val: number) { this.setDataVal('lastComm', val, false); }
public get isBodyOn(): boolean { return this.data.isBodyOn; }
public set isBodyOn(val: boolean) { this.data.isBodyOn = val; }
public get flowDetected(): boolean { return this.data.flowDetected; }
public set flowDetected(val: boolean) { this.data.flowDetected = val; }
public get status(): number { return typeof (this.data.status) !== 'undefined' ? this.data.status.val : -1; }
public set status(val: number) {
if (this.status !== val) {
this.data.status = sys.board.valueMaps.chemDoserStatus.transform(val);
this.hasChanged = true;
}
}
public get body(): number { return typeof (this.data.body) !== 'undefined' ? this.data.body.val : -1; }
public set body(val: number) {
if (this.body !== val) {
this.data.body = sys.board.valueMaps.bodies.transform(val);
this.hasChanged = true;
}
}
public get type(): number { return typeof (this.data.type) !== 'undefined' ? this.data.type.val : 0; }
public set type(val: number) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.chemDoserTypes.transform(val);
this.data.chemType = this.data.type.desc;
this.hasChanged = true;
}
}
public get enabled(): boolean { return this.data.enabled; }
public set enabled(val: boolean) { this.data.enabled = val; }
public get freezeProtect(): boolean { return this.data.freezeProtect; }
public set freezeProtect(val: boolean) { this.data.freezeProtect = val; }
public get suspendDosing(): boolean { return this.data.suspendDosing; }
public set suspendDosing(val: boolean) { this.data.suspendDosing = val; }
public get chemType(): string { return this.data.chemType; }
public get delayTimeRemaining(): number { return this.data.delayTimeRemaining; }
public set delayTimeRemaining(val: number) { this.setDataVal('delayTimeRemaining', val); }
public get doseTime(): number { return this.data.doseTime; }
public set doseTime(val: number) { this.setDataVal('doseTime', val); }
public get doseVolume(): number { return this.data.doseVolume; }
public set doseVolume(val: number) { this.setDataVal('doseVolume', val); }
public get dosingTimeRemaining(): number { return this.data.dosingTimeRemaining; }
public set dosingTimeRemaining(val: number) { this.setDataVal('dosingTimeRemaining', val); }
public get dosingVolumeRemaining(): number { return this.data.dosingVolumeRemaining; }
public set dosingVolumeRemaining(val: number) { this.setDataVal('dosingVolumeRemaining', val); }
public get volumeDosed(): number { return this.data.volumeDosed; }
public set volumeDosed(val: number) { this.setDataVal('volumeDosed', val); }
public get timeDosed(): number { return this.data.timeDosed; }
public set timeDosed(val: number) { this.setDataVal('timeDosed', val); }
public get dailyVolumeDosed(): number { return this.data.dailyVolumeDosed; }
public set dailyVolumeDosed(val: number) { this.setDataVal('dailyVolumeDosed', val); }
public get mixTimeRemaining(): number { return this.data.mixTimeRemaining; }
public set mixTimeRemaining(val: number) { this.setDataVal('mixTimeRemaining', val); }
public get dosingStatus(): number { return typeof (this.data.dosingStatus) !== 'undefined' ? this.data.dosingStatus.val : undefined; }
public set dosingStatus(val: number) {
if (this.dosingStatus !== val) {
logger.debug(`${this.chemType} dosing status changed from ${sys.board.valueMaps.chemControllerDosingStatus.getName(this.dosingStatus)} (${this.dosingStatus}) to ${sys.board.valueMaps.chemControllerDosingStatus.getName(val)}(${val})`);
this.data.dosingStatus = sys.board.valueMaps.chemControllerDosingStatus.transform(val);
this.hasChanged = true;
}
}
public get lockout(): boolean { return utils.makeBool(this.data.lockout); }
public set lockout(val: boolean) { this.setDataVal('lockout', val); }
public get flowDelay(): boolean { return utils.makeBool(this.data.flowDelay); }
public set flowDelay(val: boolean) { this.data.flowDelay = val; }
public get manualDosing(): boolean { return utils.makeBool(this.data.manualDosing); }
public set manualDosing(val: boolean) { this.setDataVal('manualDosing', val); }
public get manualMixing(): boolean { return utils.makeBool(this.data.manualMixing); }
public set manualMixing(val: boolean) { this.setDataVal('manualMixing', val); }
public get dailyLimitReached(): boolean { return utils.makeBool(this.data.dailyLimitReached); }
public set dailyLimitReached(val: boolean) { this.data.dailyLimitReached = val; }
public get tank(): ChemicalTankState { return new ChemicalTankState(this.data, 'tank', this); }
public get pump(): ChemicalPumpState { return new ChemicalPumpState(this.data, 'pump', this); }
public get flowSensor(): ChemicalFlowSensorState { return new ChemicalFlowSensorState(this.data, 'flowSensor', this); }
public get warnings(): ChemDoserStateWarnings { return new ChemDoserStateWarnings(this.data, 'warnings', this); }
public get alarms(): ChemDoserStateAlarms { return new ChemDoserStateAlarms(this.data, 'alarms', this); }
public calcDoseHistory(): number {
// The dose history records will already exist when the state is loaded. There are enough records to cover 24 hours in this
// instance. We need to prune off any records that are > 24 hours when we calculate.
let dailyVolumeDosed = 0;
let dt = new Date();
let dtMax = dt.setTime(dt.getTime() - (24 * 60 * 60 * 1000));
for (let i = this.doseHistory.length - 1; i >= 0; i--) {
let dh = this.doseHistory[i];
if (typeof dh.end !== 'undefined'
&& typeof dh.end.getTime == 'function'
&& dh.end.getTime() > dtMax
&& dh.volumeDosed > 0) dailyVolumeDosed += dh.volumeDosed;
else {
logger.info(`Removing dose history ${dh.chem} ${dh.end}`);
this.doseHistory.splice(i, 1);
}
}
return dailyVolumeDosed + (typeof this.currentDose !== 'undefined' ? this.currentDose.volumeDosed : 0);
}
public startDose(dtStart: Date = new Date(), method: string = 'auto', volume: number = 0, volumeDosed: number = 0, time: number = 0, timeDosed: number = 0): ChemicalDoseState {
this.currentDose = new ChemicalDoseState();
this.currentDose.type = this.chemType;
this.currentDose.id = this.id;
this.currentDose.start = dtStart;
this.currentDose.method = method;
this.currentDose.volumeDosed = volumeDosed;
this.doseVolume = this.currentDose.volume = volume;
this.currentDose.chem = this.chemType;
this.currentDose.time = time;
this.currentDose._timeDosed = timeDosed;
this.volumeDosed = this.currentDose.volumeDosed;
this.timeDosed = Math.round(timeDosed / 1000);
this.dosingTimeRemaining = this.currentDose.timeRemaining;
this.dosingVolumeRemaining = this.currentDose.volumeRemaining;
this.doseTime = Math.round(this.currentDose.time / 1000);
this.currentDose._isManual = method === 'manual';
this.currentDose.status = 'current';
//webApp.emitToClients(`chemicalDose`, this.currentDose);
return this.currentDose;
}
public endDose(dtEnd: Date = new Date(), status: string = 'completed', volumeDosed: number = 0, timeDosed: number = 0): ChemicalDoseState {
let dose = this.currentDose;
if (typeof dose !== 'undefined') {
dose.type = 'Peristaltic';
dose._timeDosed += timeDosed;
dose.volumeDosed += volumeDosed;
dose.end = dtEnd;
dose.timeDosed = dose._timeDosed / 1000;
dose.status = status;
this.volumeDosed = dose.volumeDosed;
this.timeDosed = Math.round(dose._timeDosed / 1000);
this.dosingTimeRemaining = 0;
this.dosingVolumeRemaining = 0;
if (dose.volumeDosed > 0 || dose.timeDosed > 0) {
this.dailyVolumeDosed = this.calcDoseHistory();
this.doseHistory.unshift(dose);
DataLogger.writeEnd(`chemDosage_${this.chemType}.log`, dose);
setImmediate(() => { webApp.emitToClients(`chemicalDose`, dose); });
}
this.currentDose = undefined;
}
return dose;
}
// Appends dose information to the current dose. The time here is in ms and our target will be in sec.
public appendDose(volumeDosed: number, timeDosed: number): ChemicalDoseState {
let dose = typeof this.currentDose !== 'undefined' ? this.currentDose : this.currentDose = this.startDose();
dose._timeDosed += timeDosed;
dose.volumeDosed += volumeDosed;
dose.timeDosed = dose._timeDosed / 1000;
dose.type = 'Peristaltic';
this.volumeDosed = dose.volumeDosed;
this.timeDosed = Math.round(dose._timeDosed / 1000);
this.dosingTimeRemaining = dose.timeRemaining;
this.dosingVolumeRemaining = dose.volumeRemaining;
if (dose.volumeDosed > 0 || timeDosed > 0) setImmediate(() => { webApp.emitToClients(`chemicalDose`, dose); });
return dose;
}
public get currentDose(): ChemicalDoseState {
if (typeof this.data.currentDose === 'undefined') return this.data.currentDose;
if (typeof this.data.currentDose.save !== 'function') this.data.currentDose = new ChemicalDoseState(this.data.currentDose);
return this.data.currentDose;
}
public set currentDose(val: ChemicalDoseState) {
this.setDataVal('currentDose', val);
}
public get doseHistory(): ChemicalDoseState[] {
if (typeof this.data.doseHistory === 'undefined') this.data.doseHistory = [];
if (this.data.doseHistory.length === 0) return this.data.doseHistory;
if (typeof this.data.doseHistory[0].save !== 'function') {
let arr: ChemicalDoseState[] = [];
for (let i = 0; i < this.data.doseHistory.length; i++) {
arr.push(new ChemicalDoseState(this.data.doseHistory[i]));
}
this.data.doseHistory = arr;
}
return this.data.doseHistory;
}
public set doseHistory(val: ChemicalDoseState[]) { this.setDataVal('doseHistory', val); }
public get chemController() { return this; }
public getExtended(): any {
let chem = sys.chemDosers.getItemById(this.id);
let obj = this.get(true);
obj = extend(true, obj, chem.getExtended());
return obj;
}
}
export class ChemControllerState extends EqState implements IChemControllerState {
public initData() {
if (typeof this.activeBodyId === 'undefined') this.data.activeBodyId = 0;
if (typeof this.data.saturationIndex === 'undefined') this.data.saturationIndex = 0;
if (typeof this.data.flowDetected === 'undefined') this.data.flowDetected = false;
if (typeof this.data.orp === 'undefined') this.data.orp = {};
if (typeof this.data.ph === 'undefined') this.data.ph = {};
if (typeof this.data.flowSensor === 'undefined') this.data.flowSensor = {};
if (typeof this.data.type === 'undefined') {
this.data.type = sys.board.valueMaps.chemControllerTypes.transform(1);
}
else if (typeof this.data.type.ph === 'undefined') {
this.data.type = sys.board.valueMaps.chemControllerTypes.transform(this.type);
}
if (typeof this.data.alarms === 'undefined') {
// Just get the alarms object it should then initialize.
let a = this.alarms;
}
if (typeof this.data.warnings === 'undefined') {
let w = this.warnings;
}
if (typeof this.data.siCalcType === 'undefined') {
this.data.siCalcType = sys.board.valueMaps.siCalcTypes.transform(0);
}
}
public dataName: string = 'chemController';
public get lastComm(): number { return this.data.lastComm || 0; }
public set lastComm(val: number) { this.setDataVal('lastComm', val, false); }
public get id(): number { return this.data.id; }
public set id(val: number) { this.setDataVal('id', 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 address(): number { return this.data.address; }
public set address(val: number) { this.setDataVal('address', val); }
public get isBodyOn(): boolean { return this.data.isBodyOn; }
public set isBodyOn(val: boolean) { this.data.isBodyOn = val; }
public get activeBodyId(): number { return this.data.activeBodyId || 0; }
public set activeBodyId(val: number) { this.data.activeBodyId = val; }
public get flowDetected(): boolean { return this.data.flowDetected; }
public set flowDetected(val: boolean) { this.data.flowDetected = val; }
public get status(): number {
return typeof (this.data.status) !== 'undefined' ? this.data.status.val : -1;
}
public set status(val: number) {
if (this.status !== val) {
this.data.status = sys.board.valueMaps.chemControllerStatus.transform(val);
this.hasChanged = true;
}
}
public get body(): number { return typeof (this.data.body) !== 'undefined' ? this.data.body.val : -1; }
public set body(val: number) {
if (this.body !== val) {
this.data.body = sys.board.valueMaps.bodies.transform(val);
this.hasChanged = true;
}
}
public get type(): number { return typeof (this.data.type) !== 'undefined' ? this.data.type.val : 0; }
public set type(val: number) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.chemControllerTypes.transform(val);
this.hasChanged = true;
}
}
public get saturationIndex(): number { return this.data.saturationIndex; }
public set saturationIndex(val: number) { this.setDataVal('saturationIndex', val); }
public get lsi(): number { return this.data.lsi; }
public set lsi(val: number) {
this.setDataVal('lsi', val || 0);
if (this.siCalcType === 0) this.saturationIndex = val || 0;
}
public get csi(): number { return this.data.csi; }
public set csi(val: number) {
this.setDataVal('csi', val || 0);
if (this.siCalcType === 1) this.saturationIndex = val || 0;
}
public calculateCSI(): number {
let chem = sys.chemControllers.getItemById(this.id);
let saltLevel = this.orp.probe.saltLevel || state.chlorinators.getItemById(1).saltLevel || 0;
let extraSalt = Math.max(0, saltLevel - 1.168 * chem.calciumHardness);
let ph = this.ph.level;
let t = this.ph.probe.temperature || (this.body != 1 ? state.temps.waterSensor1 : state.temps.waterSensor2);
let u = sys.board.valueMaps.tempUnits.getName(this.ph.probe.tempUnits || state.temps.units);
let tempK = utils.convert.temperature.convertUnits(t, u, 'K');
let I = ((1.5 * chem.calciumHardness + chem.alkalinity)) / 50045 + extraSalt / 58440;
let carbAlk = chem.alkalinity - 0.38772 * chem.cyanuricAcid / (1 + Math.pow(10, 6.83 - this.ph.level)) - 4.63 * chem.borates / (1 + Math.pow(10, 9.11 - ph));
let SI = Math.round((
ph
- 6.9395
+ Math.log10(chem.calciumHardness)
+ Math.log10(carbAlk)
- 2.56 * Math.sqrt(I) / (1 + 1.65 * Math.sqrt(I))
- 1412.5 / tempK
) * 1000) / 1000;
return isNaN(SI) ? undefined : SI;
}
private chFactor(ch: number): number {
if (ch <= 25) return 1.0;
else if (ch <= 50) return 1.3;
else if (ch <= 75) return 1.5;
else if (ch <= 100) return 1.6;
else if (ch <= 125) return 1.7;
else if (ch <= 150) return 1.8;
else if (ch <= 200) return 1.9;
else if (ch <= 250) return 2.0;
else if (ch <= 300) return 2.1;
else if (ch <= 400) return 2.2;
return 2.5;
}
private alkFactor(alk: number): number {
if (alk <= 25) return 1.4;
else if (alk <= 50) return 1.7;
else if (alk <= 75) return 1.9;
else if (alk <= 100) return 2.0;
else if (alk <= 125) return 2.1;
else if (alk <= 150) return 2.2;
else if (alk <= 200) return 2.3;
else if (alk <= 250) return 2.4;
else if (alk <= 300) return 2.5;
else if (alk <= 400) return 2.6;
return 2.9;
}
private tempFactor(tempC): number {
if (tempC <= 0) return 0.0;
else if (tempC <= 2.8) return 0.1;
else if (tempC <= 7.8) return 0.2;
else if (tempC <= 11.7) return 0.3;
else if (tempC <= 15.6) return 0.4;
else if (tempC <= 18.9) return 0.5;
else if (tempC <= 24.4) return 0.6;
else if (tempC <= 28.9) return 0.7;
else if (tempC <= 34.4) return 0.8;
return 0.9;
}
public calculateLSI(): number {
let t = this.ph.probe.temperature || (this.body != 1 ? state.temps.waterSensor1 : state.temps.waterSensor2);
let u = sys.board.valueMaps.tempUnits.getName(this.ph.probe.tempUnits || state.temps.units);
let tempC = utils.convert.temperature.convertUnits(t, u, 'C');
let chem = sys.chemControllers.getItemById(this.id);
let calciumHardnessFactor = this.chFactor(chem.calciumHardness);
let alkalinityFactor = this.alkFactor(chem.alkalinity);
let tempFactor = this.tempFactor(tempC);
let dssFactor = chem.orp.useChlorinator ? 12.2 : 12.1;
return Math.round((this.ph.level + calciumHardnessFactor + alkalinityFactor + tempFactor - dssFactor) * 1000) / 1000;
}
public calculateSaturationIndex(): number {
// We always calculate the indexes for CSI but if this is IntelliChem we use the LSI value returned in
// the 18 (status) message. Therefore the check below for type === 2.
this.csi = this.calculateCSI();
if (this.type !== 2) this.lsi = this.calculateLSI();
if (this.siCalcType === 0) this.saturationIndex = this.lsi;
else this.saturationIndex = this.csi;
return this.saturationIndex;
}
public get ph(): ChemicalPhState { return new ChemicalPhState(this.data, 'ph', this); }
public get orp(): ChemicalORPState { return new ChemicalORPState(this.data, 'orp', this); }
public get flowSensor(): ChemicalFlowSensorState { return new ChemicalFlowSensorState(this.data, 'flowSensor', this); }
public get warnings(): ChemControllerStateWarnings { return new ChemControllerStateWarnings(this.data, 'warnings', this); }
public get alarms(): ChemControllerStateAlarms { return new ChemControllerStateAlarms(this.data, 'alarms', this); }
public get siCalcType(): number { return typeof this.data.siCalcType === 'undefined' ? 0 : this.data.siCalcType.val; }
public set siCalcType(val: number) {
if (this.siCalcType !== val) {
this.data.siCalcType = sys.board.valueMaps.siCalcTypes.transform(val);
this.hasChanged = true;
}
}
public getEmitData(): any {
let chem = sys.chemControllers.getItemById(this.id);
let obj = this.get(true);
obj.address = chem.address;
obj.borates = chem.borates;
obj.saturationIndex = this.saturationIndex || 0;
obj.alkalinity = chem.alkalinity;
obj.calciumHardness = chem.calciumHardness;
obj.cyanuricAcid = chem.cyanuricAcid;
return obj;
}
public getExtended(): any {
let chem = sys.chemControllers.getItemById(this.id);
let obj = this.get(true);
obj.address = chem.address;
obj.borates = chem.borates;
obj.saturationIndex = this.saturationIndex || 0;
obj.alkalinity = chem.alkalinity;
obj.calciumHardness = chem.calciumHardness;
obj.cyanuricAcid = chem.cyanuricAcid;
obj.ph = this.ph.getExtended();
obj.orp = this.orp.getExtended();
obj = extend(true, obj, chem.getExtended());
return obj;
}
}
export class ChemicalState extends ChildEqState implements IChemicalState {
public initData() {
if (typeof this.data.probe === 'undefined') this.data.probe = {};
if (typeof this.data.tank === 'undefined') this.data.tank = { capacity: 0, level: 0, units: 0 };
if (typeof this.data.pump === 'undefined') this.data.pump = { isDosing: false };
if (typeof this.data.dosingTimeRemaining === 'undefined') this.data.dosingTimeRemaining = 0;
if (typeof this.data.delayTimeRemaining === 'undefined') this.data.delayTimeRemaining = 0;
if (typeof this.data.dosingVolumeRemaining === 'undefined') this.data.dosingVolumeRemaining = 0;
if (typeof this.data.doseVolume === 'undefined') this.data.doseVolume = 0;
if (typeof this.data.doseTime === 'undefined') this.data.doseTime = 0;
if (typeof this.data.lockout === 'undefined') this.data.lockout = false;
if (typeof this.data.level === 'undefined') this.data.level = 0;
if (typeof this.data.mixTimeRemaining === 'undefined') this.data.mixTimeRemaining = 0;
if (typeof this.data.dailyLimitReached === 'undefined') this.data.dailyLimitReached = false;
if (typeof this.data.manualDosing === 'undefined') this.data.manualDosing = false;
if (typeof this.data.manualMixing === 'undefined') this.data.manualMixing = false;
if (typeof this.data.flowDelay === 'undefined') this.data.flowDelay = false;
if (typeof this.data.dosingStatus === 'undefined') this.dosingStatus = 2;
if (typeof this.data.enabled === 'undefined') this.data.enabled = true;
if (typeof this.data.freezeProtect === 'undefined') this.data.freezeProtect = false;
}
public getConfig(): Chemical { return; }
public calcDoseHistory(): number {
// The dose history records will already exist when the state is loaded. There are enough records to cover 24 hours in this
// instance. We need to prune off any records that are > 24 hours when we calculate.
let dailyVolumeDosed = 0;
let dt = new Date();
let dtMax = dt.setTime(dt.getTime() - (24 * 60 * 60 * 1000));
for (let i = this.doseHistory.length - 1; i >= 0; i--) {
let dh = this.doseHistory[i];
if (typeof dh.end !== 'undefined'
&& typeof dh.end.getTime == 'function'
&& dh.end.getTime() > dtMax
&& dh.volumeDosed > 0) dailyVolumeDosed += dh.volumeDosed;
else {
logger.info(`Removing dose history ${dh.chem} ${dh.end}`);
this.doseHistory.splice(i, 1);
}
}
return dailyVolumeDosed + (typeof this.currentDose !== 'undefined' ? this.currentDose.volumeDosed : 0);
}
public startDose(dtStart: Date = new Date(), method: string = 'auto', volume: number = 0, volumeDosed: number = 0, time: number = 0, timeDosed: number = 0): ChemicalDoseState {
this.currentDose = new ChemicalDoseState();
this.currentDose.type = this.type;
this.currentDose.id = this.chemController.id;
this.currentDose.start = dtStart;
this.currentDose.method = method;
this.currentDose.volumeDosed = volumeDosed;
this.currentDose.level = this.level;
this.currentDose.demand = this.demand;
this.doseVolume = this.currentDose.volume = volume;
this.currentDose.chem = this.chemType;
this.currentDose.setpoint = this.setpoint;
this.currentDose.time = time;
this.currentDose._timeDosed = timeDosed;
this.volumeDosed = this.currentDose.volumeDosed;
this.timeDosed = Math.round(timeDosed / 1000);
this.dosingTimeRemaining = this.currentDose.timeRemaining;
this.dosingVolumeRemaining = this.currentDose.volumeRemaining;
this.doseTime = Math.round(this.currentDose.time / 1000);
this.currentDose._isManual = method === 'manual';
this.currentDose.status = 'current';
//webApp.emitToClients(`chemicalDose`, this.currentDose);
return this.currentDose;
}
public endDose(dtEnd: Date = new Date(), status: string = 'completed', volumeDosed: number = 0, timeDosed: number = 0): ChemicalDoseState {
let dose = this.currentDose;
if (typeof dose !== 'undefined') {
dose._timeDosed += timeDosed;
dose.volumeDosed += volumeDosed;
dose.end = dtEnd;
dose.timeDosed = dose._timeDosed / 1000;
dose.status = status;
this.volumeDosed = dose.volumeDosed;
this.timeDosed = Math.round(dose._timeDosed / 1000);
this.dosingTimeRemaining = 0;
this.dosingVolumeRemaining = 0;
if (dose.volumeDosed > 0 || dose.timeDosed > 0) {
this.dailyVolumeDosed = this.calcDoseHistory();
this.doseHistory.unshift(dose);
DataLogger.writeEnd(`chemDosage_${this.chemType}.log`, dose);
setImmediate(() => { webApp.emitToClients(`chemicalDose`, dose); });
}
this.currentDose = undefined;
}
return dose;
}
// Appends dose information to the current dose. The time here is in ms and our target will be in sec.
public appendDose(volumeDosed: number, timeDosed: number): ChemicalDoseState {
let dose = typeof this.currentDose !== 'undefined' ? this.currentDose : this.currentDose = this.startDose();
dose._timeDosed += timeDosed;
dose.volumeDosed += volumeDosed;
dose.timeDosed = dose._timeDosed/1000;
this.volumeDosed = dose.volumeDosed;
this.timeDosed = Math.round(dose._timeDosed / 1000);
this.dosingTimeRemaining = dose.timeRemaining;
this.dosingVolumeRemaining = dose.volumeRemaining;
if (dose.volumeDosed > 0 || timeDosed > 0) setImmediate(() => { webApp.emitToClients(`chemicalDose`, dose); });
return dose;
}
public get currentDose(): ChemicalDoseState {
if (typeof this.data.currentDose === 'undefined') return this.data.currentDose;
if (typeof this.data.currentDose.save !== 'function') this.data.currentDose = new ChemicalDoseState(this.data.currentDose);
return this.data.currentDose;
}
public set currentDose(val: ChemicalDoseState) {
this.setDataVal('currentDose', val);
}
public get doseHistory(): ChemicalDoseState[] {
if (typeof this.data.doseHistory === 'undefined') this.data.doseHistory = [];
if (this.data.doseHistory.length === 0) return this.data.doseHistory;
if (typeof this.data.doseHistory[0].save !== 'function') {
let arr: ChemicalDoseState[] = [];
for (let i = 0; i < this.data.doseHistory.length; i++) {
arr.push(new ChemicalDoseState(this.data.doseHistory[i]));
}
this.data.doseHistory = arr;
}
return this.data.doseHistory;
}
public set doseHistory(val: ChemicalDoseState[]) { this.setDataVal('doseHistory', val); }
public appendDemand(time: number, val: number) {
let dH = this.demandHistory;
dH.appendDemand(time, val);
}
public get type() { return this.data.type; };
public get demandHistory() { return new ChemicalDemandState(this.data, 'demandHistory', this) };
public get enabled(): boolean { return this.data.enabled; }
public set enabled(val: boolean) { this.data.enabled = val; }
public get freezeProtect(): boolean { return this.data.freezeProtect; }
public set freezeProtect(val: boolean) { this.data.freezeProtect = val; }
public get level(): number { return this.data.level; }
public set level(val: number) { this.setDataVal('level', val); }
public get setpoint(): number { return this.data.setpoint; }
public set setpoint(val: number) { this.setDataVal('setpoint', val); }
public get demand(): number { return this.data.demand || 0; }
public set demand(val: number) { this.setDataVal('demand', val); }
public get chemController(): ChemControllerState { return this.getParent() as ChemControllerState; }
public get chemType(): string { return this.data.chemType; }
public get delayTimeRemaining(): number { return this.data.delayTimeRemaining; }
public set delayTimeRemaining(val: number) { this.setDataVal('delayTimeRemaining', val); }
public get doseTime(): number { return this.data.doseTime; }
public set doseTime(val: number) { this.setDataVal('doseTime', val); }
public get doseVolume(): number { return this.data.doseVolume; }
public set doseVolume(val: number) { this.setDataVal('doseVolume', val); }
public get dosingTimeRemaining(): number { return this.data.dosingTimeRemaining; }
public set dosingTimeRemaining(val: number) { this.setDataVal('dosingTimeRemaining', val); }
public get dosingVolumeRemaining(): number { return this.data.dosingVolumeRemaining; }
public set dosingVolumeRemaining(val: number) { this.setDataVal('dosingVolumeRemaining', val); }
public get volumeDosed(): number { return this.data.volumeDosed; }
public set volumeDosed(val: number) { this.setDataVal('volumeDosed', val); }
public get timeDosed(): number { return this.data.timeDosed; }
public set timeDosed(val: number) { this.setDataVal('timeDosed', val); }
public get dailyVolumeDosed(): number { return this.data.dailyVolumeDosed; }
public set dailyVolumeDosed(val: number) { this.setDataVal('dailyVolumeDosed', val); }
public get mixTimeRemaining(): number { return this.data.mixTimeRemaining; }
public set mixTimeRemaining(val: number) { this.setDataVal('mixTimeRemaining', val); }
public get dosingStatus(): number { return typeof (this.data.dosingStatus) !== 'undefined' ? this.data.dosingStatus.val : undefined; }
public set dosingStatus(val: number) {
if (this.dosingStatus !== val) {
logger.debug(`${this.chemType} dosing status changed from ${sys.board.valueMaps.chemControllerDosingStatus.getName(this.dosingStatus)} (${this.dosingStatus}) to ${sys.board.valueMaps.chemControllerDosingStatus.getName(val)}(${val})`);
this.data.dosingStatus = sys.board.valueMaps.chemControllerDosingStatus.transform(val);
this.hasChanged = true;
}
}
public get lockout(): boolean { return utils.makeBool(this.data.lockout); }
public set lockout(val: boolean) { this.setDataVal('lockout', val); }
public get flowDelay(): boolean { return utils.makeBool(this.data.flowDelay); }
public set flowDelay(val: boolean) { this.data.flowDelay = val; }
public get manualDosing(): boolean { return utils.makeBool(this.data.manualDosing); }
public set manualDosing(val: boolean) { this.setDataVal('manualDosing', val); }
public get manualMixing(): boolean { return utils.makeBool(this.data.manualMixing); }
public set manualMixing(val: boolean) { this.setDataVal('manualMixing', val); }
public get dailyLimitReached(): boolean { return utils.makeBool(this.data.dailyLimitReached); }
public set dailyLimitReached(val: boolean) { this.data.dailyLimitReached = val; }
public get tank(): ChemicalTankState { return new ChemicalTankState(this.data, 'tank', this); }
public get pump(): ChemicalPumpState { return new ChemicalPumpState(this.data, 'pump', this); }
public get chlor(): ChemicalChlorState { return new ChemicalChlorState(this.data, 'chlor', this); }
public calcDemand(chem?: ChemController): number { return 0; }
public getExtended() {
let chem = this.get(true);
chem.tank = this.tank.getExtended();
chem.pump = this.pump.getExtended();
return chem;
}
}
export class ChemicalPhState extends ChemicalState {
public initData() {
super.initData();
if (typeof this.data.chemType === 'undefined') this.data.chemType = 'none';
if (typeof this.data.type === 'undefined') this.data.type = 'ph';
}
public getConfig() {
let schem = this.chemController;
if (typeof schem !== 'undefined') {
let chem = sys.chemControllers.getItemById(schem.id);
return typeof chem !== 'undefined' ? chem.ph : undefined;
}
}
public get chemType() { return this.data.chemType; }
public set chemType(val: string) { this.setDataVal('chemType', val); }
public get probe(): ChemicalProbePHState { return new ChemicalProbePHState(this.data, 'probe', this); }
public getExtended() {
let chem = super.getExtended();
chem.probe = this.probe.getExtended();
return chem;
}
public get suspendDosing(): boolean {
let cc = this.chemController;
return cc.alarms.comms !== 0 || cc.alarms.pHProbeFault !== 0 || cc.alarms.pHPumpFault !== 0 || cc.alarms.bodyFault !== 0;
}
public calcDemand(chem?: ChemController): number {
chem = typeof chem === 'undefined' ? sys.chemControllers.getItemById(this.chemController.id) : chem;
// Calculate how many mL are required to raise to our pH level.
// 1. Get the total gallons of water that the chem controller is in
// control of.
// 2. RSG 5-22-22 - If the spa is on, calc demand only based on the spa volume. Otherwise, long periods of spa usage
// will result in an overdose if pH is high.
let totalGallons = 0;
// The bodyIsOn code was throwing an exception whenver no bodies were on.
if (chem.body === 32 && sys.equipment.shared) {
// We are shared and when body 2 (spa) is on body 1 (pool) is off.
if (state.temps.bodies.getItemById(2).isOn === true) totalGallons = sys.bodies.getItemById(2).capacity;
else totalGallons = sys.bodies.getItemById(1).capacity + sys.bodies.getItemById(2).capacity;
}
else {
// These are all single body implementations so we simply match to the body.
totalGallons = sys.bodies.getItemById(chem.body + 1).capacity;
}
//if (chem.body === 0 || chem.body === 32 || sys.equipment.shared) totalGallons += sys.bodies.getItemById(1).capacity;
//let bodyIsOn = state.temps.bodies.getBodyIsOn();
//if (bodyIsOn.circuit === 1 && sys.circuits.getInterfaceById(bodyIsOn.circuit).type === sys.board.valueMaps.circuitFunctions.getValue('spa') && (chem.body === 1 || chem.body === 32 || sys.equipment.shared)) totalGallons = sys.bodies.getItemById(2).capacity;
//else if (chem.body === 1 || chem.body === 32 || sys.equipment.shared) totalGallons += sys.bodies.getItemById(2).capacity;
//if (chem.body === 2) totalGallons += sys.bodies.getItemById(3).capacity;
//if (chem.body === 3) totalGallons += sys.bodies.getItemById(4).capacity;
logger.verbose(`Chem begin calculating ${this.chemType} demand: ${this.level} setpoint: ${this.setpoint} total gallons: ${totalGallons}`);
let chg = this.setpoint - this.level;
let delta = chg * totalGallons;
let temp = (this.level + this.setpoint) / 2;
let adj = (192.1626 + -60.1221 * temp + 6.0752 * temp * temp + -0.1943 * temp * temp * temp) * (chem.alkalinity + 13.91) / 114.6;
let extra = (-5.476259 + 2.414292 * temp + -0.355882 * temp * temp + 0.01755 * temp * temp * temp) * (chem.borates || 0);
extra *= delta;
delta *= adj;
let dose = 0;
if (chem.ph.phSupply === 0) { // We are dispensing base so we need to calculate the demand here.
if (chg > 0) {
}
}
else {
if (chg < 0) {
let at = sys.board.valueMaps.acidTypes.transform(chem.ph.acidType);
dose = Math.round(utils.convert.volume.convertUnits((delta / -240.15 * at.dosingFactor) + (extra / -240.15 * at.dosingFactor), 'oz', 'mL'));
}
}
return dose;
}
}
export class ChemicalORPState extends ChemicalState {
public initData() {
if (typeof this.data.probe === 'undefined') this.data.probe = {};
if (typeof this.data.chemType === 'undefined') this.data.chemType = 'none';
if (typeof this.data.useChlorinator === 'undefined') this.data.useChlorinator = false;
if (typeof this.data.type === 'undefined') this.data.type = 'orp';
super.initData();
// Load up the 24 hours doseHistory.
//this.doseHistory = DataLogger.readFromEnd(`chemDosage_${this.chemType}.log`, ChemicalDoseState, (lineNumber: number, entry: ChemicalDoseState): boolean => {
// let dt = new Date();
// let dtMax = dt.setTime(dt.getTime() - (24 * 60 * 60 * 1000));
// // If we are reading back in time prior to 24 hours then we don't want the data.
// if (entry.end.getTime() < dtMax) return false;
// return true;
//});
}
public get chemType() { return 'orp'; }
public set chemType(val) { this.setDataVal('chemType', val); }
public get probe() { return new ChemicalProbeORPState(this.data, 'probe', this); }
// public get useChlorinator(): boolean { return utils.makeBool(this.data.useChlorinator); }
// public set useChlorinator(val: boolean) { this.setDataVal('useChlorinator', val); }
public get suspendDosing(): boolean {
let cc = this.chemController;
return cc.alarms.comms !== 0 || cc.alarms.orpProbeFault !== 0 || cc.alarms.orpPumpFault !== 0 || cc.alarms.bodyFault !== 0;
}
public getExtended() {
let chem = super.getExtended();
chem.probe = this.probe.getExtended();
return chem;
}
}
export class ChemicalFlowSensorState extends ChemicalState {
public initData() {
if (typeof this.data.state === 'undefined') this.data.state = 0;
}
public get state(): number { return this.data.state || 0; }
public set state(val: number) { this.setDataVal('state', val); }
}
export class ChemicalPumpState extends ChildEqState {
public initData() {
if (typeof this.data.isDosing === 'undefined') this.data.isDosing = false;
}
public get chemical(): ChemicalState { return this.getParent() as ChemicalState; }
public get chemController(): ChemControllerState {
let p = this.chemical;
return typeof p !== 'undefined' ? p.getParent() as ChemControllerState : undefined;
}
public get type(): number { return typeof (this.data.type) !== 'undefined' ? this.data.type.val : undefined; }
public set type(val: number) {
if (this.type !== val) {
this.data.type = sys.board.valueMaps.chemPumpTypes.transform(val);
this.hasChanged = true;
}
}
public get isDosing(): boolean { return utils.makeBool(this.data.isDosing); }
public set isDosing(val: boolean) { this.setDataVal('isDosing', val); }
public getExtended() {
let pump = this.get(true);
pump.type = sys.board.valueMaps.chemPumpTypes.transform(this.type);
return pump;
}
}
export class ChemicalChlorState extends ChildEqState {
public initData() {
if (typeof this.data.isDosing === 'undefined') this.data.isDosing = false;
}
public get chemical(): ChemicalState { return this.getParent() as ChemicalState; }
public get chemController(): ChemControllerState {
let p = this.chemical;
return typeof p !== 'undefined' ? p.getParent() as ChemControllerState : undefined;
}
public get isDosing(): boolean { return utils.makeBool(this.data.isDosing); }
public set isDosing(val: boolean) { this.setDataVal('isDosing', val); }
public getExtended() {
let chlor = this.get(true);
return chlor;
}
}
export class ChemicalProbeState extends ChildEqState {
public initData() {
if (typeof this.data.level === 'undefined') this.data.level = null;
}
public get chemical(): ChemicalState { return this.getParent() as ChemicalState; }
public get chemController(): ChemControllerState {
let p = this.chemical;
return typeof p !== 'undefined' ? p.getParent() as ChemControllerState : undefined;
}
public get level(): number { return this.data.level; }
public set level(val: number) { this.setDataVal('level', val); }
}
export class ChemicalProbeORPState extends ChemicalProbeState {
public initData() {
if (typeof this.data.saltLevel === 'undefined') this.data.saltLevel = 0;
super.initData();
}
public get saltLevel(): number { return this.data.saltLevel; }
public set saltLevel(val: number) { this.setDataVal('saltLevel', val); }
}
export class ChemicalProbePHState extends ChemicalProbeState {
public initData() {
if (typeof this.data.temperature === 'undefined') this.data.temperature = 0;
super.initData();
}
public get temperature(): number { return this.data.temperature; }
public set temperature(val: number) { this.setDataVal('temperature', val); }
public get tempUnits(): number {
return typeof (this.data.tempUnits) !== 'undefined' ? this.data.tempUnits.val : undefined;
}
public set tempUnits(val: number) {
if (this.tempUnits !== val) {
this.data.tempUnits = sys.board.valueMaps.tempUnits.transform(val);
this.hasChanged = true;
}
}
}
export class ChemicalTankState extends ChildEqState {
public initData() {
if (typeof this.data.level === 'undefined') this.data.level == 0;
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;
}
public get chemical(): ChemicalState { return this.getParent() as ChemicalState; }
public get chemController(): ChemControllerState {
let p = this.chemical;
return typeof p !== 'undefined' ? p.getParent() as ChemControllerState : undefined;
}
public get level(): number { return this.data.level; }
public set level(val: number) { this.setDataVal('level', val); }
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 typeof this.data.units !== 'undefined' ? this.data.units.val : undefined; }
public set units(val: number | any) {
let v = sys.board.valueMaps.volumeUnits.encode(val);
if (this.units !== v) {
this.data.units = sys.board.valueMaps.volumeUnits.transform(val);
this.hasChanged = true;
}
}
}
export class ChemicalDoseState extends DataLoggerEntry {
public _timeDosed: number; // _timeDosed is in ms.
public _lastLatch: number = 0;
public _isManual: boolean;
constructor(entry?: string | object) {
super();
if (typeof entry === 'object') entry = JSON.stringify(entry);
if (typeof entry === 'string') this.parse(entry);
// Javascript is idiotic in that the initialization of variables
// do not happen before the assignment so some of the values can be undefined.
if (typeof this.volumeDosed === 'undefined' || !this.volumeDosed) this.volumeDosed = 0;
if (typeof this.volume === 'undefined' || !this.volume) this.volume = 0;
if (typeof this._isManual === 'undefined') this._isManual = this.method === 'manual';
if (typeof this.timeDosed === 'undefined' || !this.timeDosed) this.timeDosed = 0;
if (typeof this._timeDosed === 'undefined') this._timeDosed = this.timeDosed * 1000;
if (typeof this.time === 'undefined' || !this.time) this.time = 0;
}
public id: number;
public method: string;
public start: Date;
public end: Date;
public chem: string;
public setpoint: number;
public demand: number;
public level: number;
public volume: number;
public status: string;
public volumeDosed: number;
public time: number;
public timeDosed: number;
public type: string;
public static createInstance(entry?: string): ChemicalDoseState { return new ChemicalDoseState(entry); }
public save() { DataLogger.writeEnd(`chemDosage_${this.chem}.log`, this); }
public get timeRemaining(): number { return Math.floor(Math.max(0, this.time - (this._timeDosed / 1000))); }
public get volumeRemaining(): number { return Math.max(0, this.volume - this.volumeDosed); }
public parse(entry: string) {
// let obj = typeof entry !== 'undefined' ? JSON.parse(entry, this.dateParser) : {};
let obj = typeof entry !== 'undefined' ? JSON.parse(entry) : {};
for (const prop in obj) {obj[prop] = this.dateParser(prop, obj[prop])}
if (typeof obj.setpoint !== 'undefined') this.setpoint = obj.setpoint;
if (typeof obj.method !== 'undefined') this.method = obj.method;
if (typeof obj.start !== 'undefined') this.start = obj.start;
if (typeof obj.end !== 'undefined') this.end = obj.end;
if (typeof obj.chem !== 'undefined') this.chem = obj.chem;
if (typeof obj.demand !== 'undefined') this.demand = obj.demand;
if (typeof obj.id !== 'undefined') this.id = obj.id;
if (typeof obj.level !== 'undefined') this.level = obj.level;
if (typeof obj.volume !== 'undefined') this.volume = obj.volume;
if (typeof obj.status !== 'undefined') this.status = obj.status;
if (typeof obj.volumeDosed !== 'undefined') this.volumeDosed = obj.volumeDosed;
if (typeof obj.time !== 'undefined') this.time = obj.time;
if (typeof obj.timeDosed !== 'undefined') this.timeDosed = obj.timeDosed;
// this.setProperties(obj);
}
protected setProperties(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 (typeof this[prop] === null || typeof data[prop] === null) continue;
this[prop] = data[prop];
}
}
}
}
export class ChemicalDemandState extends ChildEqState {
public initData() {
if (typeof this.data.time === 'undefined') this.data.time = [];
if (typeof this.data.value === 'undefined') this.data.value = [];
}
public appendDemand(time: number, val: number) {
while (this.data.time.length > 99) {
this.data.time.pop();
this.data.value.pop();
}
this.data.time.unshift(Math.round(time / 1000));
this.data.value.unshift(val);
// calculate the slope with each save
let slope = utils.slopeOfLeastSquares(this.data.time, this.data.value);
this.setDataVal('slope', slope); // will act as hasChanged=true;
}
public get demandHistory(): {} { return [this.data.time, this.data.value]; }
public get times(): number[] { return this.data.time; }
public get values(): number[] { return this.data.value; }
public set slope(val: number) { this.setDataVal('slope', val); }
public get slope():number { return this.data.slope; }
}
export class ChemControllerStateWarnings extends ChildEqState {
///ctor(data): ChemControllerStateWarnings { return new ChemControllerStateWarnings(data, name || 'warnings'); }
public dataName = 'chemControllerWarnings';
public initData() {
if (typeof this.data.waterChemistry === 'undefined') this.waterChemistry = 0;
if (typeof this.data.pHLockout === 'undefined') this.pHLockout = 0;
if (typeof this.data.pHDailyLimitReached === 'undefined') this.pHDailyLimitReached = 0;
if (typeof this.data.orpDailyLimitReached === 'undefined') this.orpDailyLimitReached = 0;
if (typeof this.data.invalidSetup === 'undefined') this.invalidSetup = 0;
if (typeof this.data.chlorinatorCommError === 'undefined') this.chlorinatorCommError = 0;
}
public get waterChemistry(): number { return typeof this.data.waterChemistry === 'undefined' ? undefined : this.data.waterChemistry.val; }
public set waterChemistry(val: number) {
if (this.waterChemistry !== val) {
this.data.waterChemistry = sys.board.valueMaps.chemControllerWarnings.transform(val);
this.hasChanged = true;
}
}
public get pHLockout(): number { return this.data.pHLockout; }
public set pHLockout(val: number) {
if (this.pHLockout !== val) {
this.data.pHLockout = sys.board.valueMaps.chemControllerLimits.transform(val);
this.hasChanged = true;
}
}
public get pHDailyLimitReached(): number { return this.data.pHDailyLimitReached; }
public set pHDailyLimitReached(val: number) {
if (this.pHDailyLimitReached !== val) {
this.data.pHDailyLimitReached = sys.board.valueMaps.chemControllerLimits.transform(val);
this.hasChanged = true;
}
}
public get orpDailyLimitReached(): number { return this.data.orpDailyLimitReached; }
public set orpDailyLimitReached(val: number) {
if (this.orpDailyLimitReached !== val) {
this.data.orpDailyLimitReached = sys.board.valueMaps.chemControllerLimits.transform(val);
this.hasChanged = true;
}
}
public get invalidSetup(): number { return this.data.invalidSetup; }
public set invalidSetup(val: number) {
if (this.invalidSetup !== val) {
this.data.invalidSetup = sys.board.valueMaps.chemControllerLimits.transform(val);
this.hasChanged = true;
}
}
public get chlorinatorCommError(): number { return this.data.chlorinatorCommError; }
public set chlorinatorCommError(val: number) {
if (this.chlorinatorCommError !== val) {
this.data.chlorinatorCommError = sys.board.valueMaps.chemControllerWarnings.transform(val);
this.hasChanged = true;
}
}
}
export class ChemDoserStateWarnings extends ChildEqState {
///ctor(data): ChemControllerStateWarnings { return new ChemControllerStateWarnings(data, name || 'warnings'); }
public dataName = 'chemDoserWarnings';
public initData() {
if (typeof this.data.lockout === 'undefined') this.lockout = 0;
if (typeof this.data.pHDailyLimitReached === 'undefined') this.dailyLimitReached = 0;
if (typeof this.data.invalidSetup === 'undefined') this.invalidSetup = 0;
if (typeof this.data.chlorinatorCommError === 'undefined') this.chlorinatorCommError = 0;
}
public get lockout(): number { return this.data.lockout; }
public set lockout(val: number) {
if (this.lockout !== val) {
this.data.lockout = sys.board.valueMaps.chemDoserLimits.transform(val);
this.hasChanged = true;
}
}
public get dailyLimitReached(): number { return this.data.dailyLimitReached; }
public set dailyLimitReached(val: number) {
if (this.dailyLimitReached !== val) {
this.data.dailyLimitReached = sys.board.valueMaps.chemDoserLimits.transform(val);
this.hasChanged = true;
}
}
public get invalidSetup(): number { return this.data.invalidSetup; }
public set invalidSetup(val: number) {
if (this.invalidSetup !== val) {
this.data.invalidSetup = sys.board.valueMaps.chemDoserLimits.transform(val);
this.hasChanged = true;
}
}
public get chlorinatorCommError(): number { return this.data.chlorinatorCommError; }
public set chlorinatorCommError(val: number) {
if (this.chlorinatorCommError !== val) {
this.data.chlorinatorCommError = sys.board.valueMaps.chemDoserWarnings.transform(val);
this.hasChanged = true;
}
}
}
export class ChemDoserStateAlarms extends ChildEqState {
public dataName = 'chemControllerAlarms';
public initData() {
if (typeof this.data.flow === 'undefined') this.data.flow = sys.board.valueMaps.chemDoserAlarms.transform(0);
if (typeof this.data.tank === 'undefined') this.data.tank = sys.board.valueMaps.chemDoserAlarms.transform(0);
if (typeof this.data.pumpFault === 'undefined') this.data.pumpFault = sys.board.valueMaps.chemDoserHardwareFaults.transform(0);
if (typeof this.data.bodyFault === 'undefined') this.data.bodyFault = sys.board.valueMaps.chemDoserHardwareFaults.transform(0);
if (typeof this.data.flowSensorFault === 'undefined') this.data.flowSensorFault = sys.board.valueMaps.chemDoserHardwareFaults.transform(0);
if (typeof this.data.comms === 'undefined') this.data.comms = sys.board.valueMaps.chemDoserStatus.transform(0);
if (typeof this.data.freezeProtect === 'undefined') this.data.freezeProtect = sys.board.valueMaps.chemDoserAlarms.transform(0);
}
public get flow(): number { return typeof this.data.flow === 'undefined' ? undefined : this.data.flow.val; }
public set flow(val: number) {
if (this.flow !== val) {
this.data.flow = sys.board.valueMaps.chemDoserAlarms.transform(val);
this.hasChanged = true;
}
}
public get tank(): number { return typeof this.data.pHTank === 'undefined' ? undefined : this.data.tank.val; }
public set tank(val: number) {
if (this.tank !== val) {
this.data.tank = sys.board.valueMaps.chemDoserAlarms.transform(val);
this.hasChanged = true;
}
}
public get pumpFault(): number { return typeof this.data.pumpFault === 'undefined' ? undefined : this.data.pumpFault.val; }
public set pumpFault(val: number) {
if (this.pumpFault !== val) {
this.data.pumpFault = sys.board.valueMaps.chemDoserHardwareFaults.transform(val);
this.hasChanged = true;
}
}
public get bodyFault(): number { return typeof this.data.bodyFault === 'undefined' ? undefined : this.data.bodyFault.val; }
public set bodyFault(val: number) {
if (this.bodyFault !== val) {
this.data.bodyFault = sys.board.valueMaps.chemDoserHardwareFaults.transform(val);
this.hasChanged = true;
}
}
public get flowSensorFault(): number { return typeof this.data.flowSensorFault === 'undefined' ? undefined : this.data.flowSensorFault.val; }
public set flowSensorFault(val: number) {
if (this.flowSensorFault !== val) {
this.data.flowSensorFault = sys.board.valueMaps.chemDoserHardwareFaults.transform(val);
this.hasChanged = true;
}
}
public get comms(): number { return typeof this.data.comms === 'undefined' ? undefined : this.data.comms.val; }
public set comms(val: number) {
if (this.comms !== val) {
this.data.comms = sys.board.valueMaps.chemDoserStatus.transform(val);
this.hasChanged = true;
}
}
public get freezeProtect(): number { return typeof this.data.freezeProtect === 'undefined' ? undefined : this.data.freezeProtect.val; }
public set freezeProtect(val: number) {
if (this.freezeProtect !== val) {
this.data.freezeProtect = sys.board.valueMaps.chemDoserAlarms.transform(val);
this.hasChanged = true;
}
}
}
export class ChemControllerStateAlarms extends ChildEqState {
//ctor(data): ChemControllerStateWarnings { return new ChemControllerStateWarnings(data, name || 'alarms'); }
public dataName = 'chemControllerAlarms';
public initData() {
if (typeof this.data.flow === 'undefined') this.data.flow = sys.board.valueMaps.chemControllerAlarms.transform(0);
if (typeof this.data.pH === 'undefined') this.data.pH = sys.board.valueMaps.chemControllerAlarms.transform(0);
if (typeof this.data.orp === 'undefined') this.data.orp = sys.board.valueMaps.chemControllerAlarms.transform(0);
if (typeof this.data.pHTank === 'undefined') this.data.pHTank = sys.board.valueMaps.chemControllerAlarms.transform(0);
if (typeof this.data.orpTank === 'undefined') this.data.orpTank = sys.board.valueMaps.chemControllerAlarms.transform(0);
if (typeof this.data.probeFault === 'undefined') this.data.probeFault = sys.board.valueMaps.chemControllerAlarms.transform(0);
if (typeof this.data.pHProbeFault === 'undefined') this.data.pHProbeFault = sys.board.valueMaps.chemControllerAlarms.transform(0);
if (typeof this.data.orpProbeFault === 'undefined') this.data.orpProbeFault = sys.board.valueMaps.chemControllerAlarms.transform(0);
if (typeof this.data.pHPumpFault === 'undefined') this.data.pHPumpFault = sys.board.valueMaps.chemControllerHardwareFaults.transform(0);
if (typeof this.data.orpPumpFault === 'undefined') this.data.orpPumpFault = sys.board.valueMaps.chemControllerHardwareFaults.transform(0);
if (typeof this.data.chlorFault === 'undefined') this.data.chlorFault = sys.board.valueMaps.chemControllerHardwareFaults.transform(0);
if (typeof this.data.bodyFault === 'undefined') this.data.bodyFault = sys.board.valueMaps.chemControllerHardwareFaults.transform(0);
if (typeof this.data.flowSensorFault === 'undefined') this.data.flowSensorFault = sys.board.valueMaps.chemControllerHardwareFaults.transform(0);
if (typeof this.data.comms === 'undefined') this.data.comms = sys.board.valueMaps.chemControllerStatus.transform(0);
if (typeof this.data.freezeProtect === 'undefined') this.data.freezeProtect = sys.board.valueMaps.chemControllerAlarms.transform(0);
}
public get flow(): number { return typeof this.data.flow === 'undefined' ? undefined : this.data.flow.val; }
public set flow(val: number) {
if (this.flow !== val) {
this.data.flow = sys.board.valueMaps.chemControllerAlarms.transform(val);
this.hasChanged = true;
}
}
public get pH(): number { return typeof this.data.pH === 'undefined' ? undefined : this.data.pH.val; }
public set pH(val: number) {
if (this.pH !== val) {
this.data.pH = sys.board.valueMaps.chemControllerAlarms.transform(val);
this.hasChanged = true;
}
}
public get orp(): number { return typeof this.data.orp === 'undefined' ? undefined : this.data.orp.val; }
public set orp(val: number) {
if (this.orp !== val) {
this.data.orp = sys.board.valueMaps.chemControllerAlarms.transform(val);
this.hasChanged = true;
}
}
public get pHTank(): number { return typeof this.data.pHTank === 'undefined' ? undefined : this.data.pHTank.val; }
public set pHTank(val: number) {
if (this.pHTank !== val) {
this.data.pHTank = sys.board.valueMaps.chemControllerAlarms.transform(val);
this.hasChanged = true;
}
}
public get orpTank(): number { return typeof this.data.orpTank === 'undefined' ? undefined : this.data.orpTank.val; }
public set orpTank(val: number) {
if (this.orpTank !== val) {
this.data.orpTank = sys.board.valueMaps.chemControllerAlarms.transform(val);
this.hasChanged = true;
}
}
public get probeFault(): number { return typeof this.data.probeFault === 'undefined' ? undefined : this.data.probeFault.val; }
public set probeFault(val: number) {
if (this.probeFault !== val) {
this.data.probeFault = sys.board.valueMaps.chemControllerAlarms.transform(val);
this.hasChanged = true;
}
}
public get pHPumpFault(): number { return typeof this.data.pHPumpFault === 'undefined' ? undefined : this.data.pHPumpFault.val; }
public set pHPumpFault(val: number) {
if (this.pHPumpFault !== val) {
this.data.pHPumpFault = sys.board.valueMaps.chemControllerHardwareFaults.transform(val);
this.hasChanged = true;
}
}
public get orpPumpFault(): number { return typeof this.data.orpPumpFault === 'undefined' ? undefined : this.data.orpPumpFault.val; }
public set orpPumpFault(val: number) {
if (this.orpPumpFault !== val) {
this.data.orpPumpFault = sys.board.valueMaps.chemControllerHardwareFaults.transform(val);
this.hasChanged = true;
}
}
public get pHProbeFault(): number { return typeof this.data.pHProbeFault === 'undefined' ? undefined : this.data.pHProbeFault.val; }
public set pHProbeFault(val: number) {
if (this.pHProbeFault !== val) {
this.data.pHProbeFault = sys.board.valueMaps.chemControllerHardwareFaults.transform(val);
this.hasChanged = true;
}
}
public get orpProbeFault(): number { return typeof this.data.orpProbeFault === 'undefined' ? undefined : this.data.orpProbeFault.val; }
public set orpProbeFault(val: number) {
if (this.orpProbeFault !== val) {
this.data.orpProbeFault = sys.board.valueMaps.chemControllerHardwareFaults.transform(val);
this.hasChanged = true;
}
}
public get chlorFault(): number { return typeof this.data.chlorFault === 'undefined' ? undefined : this.data.chlorFault.val; }
public set chlorFault(val: number) {
if (this.chlorFault !== val) {
this.data.chlorFault = sys.board.valueMaps.chemControllerHardwareFaults.transform(val);
this.hasChanged = true;
}
}
public get bodyFault(): number { return typeof this.data.bodyFault === 'undefined' ? undefined : this.data.bodyFault.val; }
public set bodyFault(val: number) {
if (this.bodyFault !== val) {
this.data.bodyFault = sys.board.valueMaps.chemControllerHardwareFaults.transform(val);
this.hasChanged = true;
}
}
public get flowSensorFault(): number { return typeof this.data.flowSensorFault === 'undefined' ? undefined : this.data.flowSensorFault.val; }
public set flowSensorFault(val: number) {
if (this.flowSensorFault !== val) {
this.data.flowSensorFault = sys.board.valueMaps.chemControllerHardwareFaults.transform(val);
this.hasChanged = true;
}
}
public get comms(): number { return typeof this.data.comms === 'undefined' ? undefined : this.data.comms.val; }
public set comms(val: number) {
if (this.comms !== val) {
this.data.comms = sys.board.valueMaps.chemControllerStatus.transform(val);
this.hasChanged = true;
}
}
public get freezeProtect(): number { return typeof this.data.freezeProtect === 'undefined' ? undefined : this.data.freezeProtect.val; }
public set freezeProtect(val: number) {
if (this.freezeProtect !== val) {
this.data.freezeProtect = sys.board.valueMaps.chemControllerAlarms.transform(val);
this.hasChanged = true;
}
}
}
export class AppVersionState extends EqState {
public get nextCheckTime(): string { return this.data.nextCheckTime; }
public set nextCheckTime(val: string) { this.setDataVal('nextCheckTime', val); }
public get isDismissed(): boolean { return this.data.isDismissed; }
public set isDismissed(val: boolean) { this.setDataVal('isDismissed', val); }
public get installed(): string { return this.data.installed; }
public set installed(val: string) { this.setDataVal('installed', val); }
public get githubRelease(): string { return this.data.githubRelease; }
public set githubRelease(val: string) { this.setDataVal('githubRelease', val); }
public get status(): number { return typeof this.data.status === 'undefined' ? undefined : this.data.status.val; }
public set status(val: number) {
if (this.status !== val) {
this.data.status = sys.board.valueMaps.appVersionStatus.transform(val);
this.hasChanged = true;
}
}
public get gitLocalBranch() { return this.data.gitLocalBranch; }
public set gitLocalBranch(val: string) { this.data.gitLocalBranch = val; }
public get gitLocalCommit() { return this.data.gitLocalCommit; }
public set gitLocalCommit(val: string) { this.data.gitLocalCommit = val; }
}
export class CommsState {
public keepAlives: number;
}
export class FilterStateCollection extends EqStateCollection {
public createItem(data: any): FilterState { return new FilterState(data); }
}
export class FilterState extends EqState {
public dataName: string = 'filter';
public get id(): number { return this.data.id; }
public set id(val: number) { this.data.id = val; }
public get name(): string { return this.data.name; }
public set name(val: string) { this.setDataVal('name', val); }
public get body(): number { return typeof (this.data.body) !== 'undefined' ? this.data.body.val : -1; }
public set body(val: number) {
if (this.body !== val) {
this.data.body = sys.board.valueMaps.bodies.transform(val);
this.hasChanged = true;
}
}
public get filterType(): number { return typeof this.data.filterType === 'undefined' ? undefined : this.data.filterType.val; }
public set filterType(val: number) {
if (this.filterType !== val) {
this.data.filterType = sys.board.valueMaps.filterTypes.transform(val);
this.hasChanged = true;
}
}
public get pressureUnits(): number { return typeof this.data.pressureUnits === 'undefined' ? 0 : this.data.pressureUnits.val; }
public set pressureUnits(val: number) {
if (this.pressureUnits !== val || typeof this.data.pressureUnits === 'undefined') {
this.setDataVal('pressureUnits', sys.board.valueMaps.pressureUnits.transform(val));
this.hasChanged = true;
}
}
public get pressure(): number { return this.data.pressure; }
public set pressure(val: number) { this.setDataVal('pressure', val); }
public get refPressure(): number { return this.data.refPressure; }
public set refPressure(val: number) {
if (val !== this.refPressure) {
this.setDataVal('refPressure', val);
this.calcCleanPercentage();
}
else { this.setDataVal('refPressure', val); }
}
public get cleanPercentage(): number { return this.data.cleanPercentage; }
public set cleanPercentage(val: number) { this.setDataVal('cleanPercentage', val); }
public get lastCleanDate(): Timestamp { return this.data.lastCleanDate; }
public set lastCleanDate(val: Timestamp) { this.setDataVal('lastCleanDate', val); }
public get isOn(): boolean { return utils.makeBool(this.data.isOn); }
public set isOn(val: boolean) { this.setDataVal('isOn', val); }
public calcCleanPercentage() {
if (typeof this.refPressure === 'undefined') return;
let filter = sys.filters.find(elem => elem.id == this.id);
// 8 to 10
let cp = filter.cleanPressure || 0;
let dp = filter.dirtyPressure || 1;
this.cleanPercentage = (cp - dp != 0) ? Math.round(Math.max(0, (1 - (this.refPressure - cp) / (dp - cp)) * 100) * 100)/100 : 0;
}
}
export var state = new State();