nodejs-poolcontroller 8.0.1 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.docker/Dockerfile.armv6 +29 -0
  2. package/.docker/Dockerfile.armv7 +29 -0
  3. package/.docker/Dockerfile.linux +62 -0
  4. package/.docker/Dockerfile.windows +43 -0
  5. package/.docker/docker-compose.yml +47 -0
  6. package/.docker/ecosystem.config.js +35 -0
  7. package/.github/workflows/docker-publish-njsPC-linux.yml +81 -0
  8. package/.github/workflows/docker-publish-njsPC-windows.yml +41 -0
  9. package/Dockerfile +4 -3
  10. package/README.md +4 -1
  11. package/config/Config.ts +1 -1
  12. package/controller/Constants.ts +164 -67
  13. package/controller/Equipment.ts +78 -18
  14. package/controller/Lockouts.ts +15 -0
  15. package/controller/State.ts +280 -7
  16. package/controller/boards/EasyTouchBoard.ts +225 -101
  17. package/controller/boards/IntelliCenterBoard.ts +67 -18
  18. package/controller/boards/IntelliTouchBoard.ts +2 -4
  19. package/controller/boards/NixieBoard.ts +84 -27
  20. package/controller/boards/SunTouchBoard.ts +8 -2
  21. package/controller/boards/SystemBoard.ts +3259 -3242
  22. package/controller/comms/ScreenLogic.ts +47 -44
  23. package/controller/comms/messages/Messages.ts +4 -4
  24. package/controller/comms/messages/config/ChlorinatorMessage.ts +10 -3
  25. package/controller/comms/messages/config/ExternalMessage.ts +4 -1
  26. package/controller/comms/messages/config/PumpMessage.ts +8 -7
  27. package/controller/comms/messages/config/RemoteMessage.ts +48 -43
  28. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +8 -2
  29. package/controller/comms/messages/status/EquipmentStateMessage.ts +9 -4
  30. package/controller/nixie/NixieEquipment.ts +1 -1
  31. package/controller/nixie/bodies/Body.ts +1 -1
  32. package/controller/nixie/chemistry/ChemController.ts +37 -28
  33. package/controller/nixie/circuits/Circuit.ts +36 -0
  34. package/controller/nixie/heaters/Heater.ts +24 -5
  35. package/controller/nixie/pumps/Pump.ts +155 -97
  36. package/controller/nixie/schedules/Schedule.ts +207 -126
  37. package/defaultConfig.json +3 -3
  38. package/logger/DataLogger.ts +7 -7
  39. package/package.json +2 -2
  40. package/sendSocket.js +32 -0
  41. package/web/Server.ts +17 -11
  42. package/web/bindings/homeassistant.json +2 -2
  43. package/web/interfaces/mqttInterface.ts +18 -18
  44. package/web/services/config/Config.ts +34 -1
  45. package/web/services/state/State.ts +10 -3
  46. package/web/services/state/StateSocket.ts +7 -3
  47. package/web/services/utilities/Utilities.ts +3 -3
@@ -18,10 +18,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
18
  import { EventEmitter } from 'events';
19
19
  import { logger } from "../logger/Logger";
20
20
  import * as util from 'util';
21
- export class Heliotrope {
22
- constructor() {
23
- this.isCalculated = false;
24
- this._zenith = 90 + 50 / 60;
21
+ class HeliotropeContext {
22
+ constructor(longitude: number, latitude: number, zenith: number) {
23
+ this._zenith = typeof zenith !== 'undefined' ? zenith : 90 + 50 / 60;
24
+ this._longitude = longitude;
25
+ this._latitude = latitude;
25
26
  }
26
27
  private dMath = {
27
28
  sin: function (deg) { return Math.sin(deg * (Math.PI / 180)); },
@@ -31,42 +32,14 @@ export class Heliotrope {
31
32
  acos: function (x) { return (180 / Math.PI) * Math.acos(x); },
32
33
  atan: function (x) { return (180 / Math.PI) * Math.atan(x); }
33
34
  }
34
- public isCalculated: boolean = false;
35
- public isValid: boolean = false;
36
- public get date() { return this.dt; }
37
- public set date(dt: Date) {
38
- if (typeof this.dt === 'undefined' ||
39
- this.dt.getFullYear() !== dt.getFullYear() ||
40
- this.dt.getMonth() !== dt.getMonth() ||
41
- this.dt.getDate() !== dt.getDate()) {
42
- this.isCalculated = false;
43
- // Always store a copy since we don't want to create instances where the change doesn't get reflected. This
44
- // also could hold onto references that we don't want held for garbage cleanup.
45
- this.dt = typeof dt !== 'undefined' && typeof dt.getMonth === 'function' ? new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(),
46
- dt.getHours(), dt.getMinutes(), dt.getSeconds(), dt.getMilliseconds()) : undefined;
47
- }
48
- }
49
- public get longitude() { return this._longitude; }
50
- public set longitude(lon: number) {
51
- if (this._longitude !== lon) this.isCalculated = false;
52
- this._longitude = lon;
53
- }
54
- public get latitude() { return this._latitude; }
55
- public set latitude(lat: number) {
56
- if (this._latitude !== lat) this.isCalculated = false;
57
- this._latitude = lat;
58
- }
59
- public get zenith() { return this._zenith; }
60
- public set zenith(zen: number) {
61
- if (this._zenith !== zen) this.isCalculated = false;
62
- this._zenith = zen;
63
- }
64
35
  private dt: Date;
65
36
  private _longitude: number;
66
37
  private _latitude: number;
67
38
  private _zenith: number;
68
- private _dtSunrise: Date;
69
- private _dtSunset: Date;
39
+ public get longitude() { return this._longitude; }
40
+ public get latitude() { return this._latitude; }
41
+ public get zenith() { return this._zenith; }
42
+ public get isValid(): boolean { return typeof this._longitude === 'number' && typeof this._latitude === 'number'; }
70
43
  private get longitudeHours(): number { return this.longitude / 15.0; }
71
44
  private get doy(): number { return Math.ceil((this.dt.getTime() - new Date(this.dt.getFullYear(), 0, 1).getTime()) / 8.64e7); }
72
45
  private get sunriseApproxTime(): number { return this.doy + ((6.0 - this.longitudeHours) / 24.0); }
@@ -125,57 +98,175 @@ export class Heliotrope {
125
98
  dtLocal.setFullYear(this.dt.getFullYear(), this.dt.getMonth(), this.dt.getDate());
126
99
  return dtLocal;
127
100
  }
128
- public get isNight(): boolean {
129
- let times = this.calculatedTimes;
101
+ public calculate(dt: Date): { dt: Date, sunrise: Date, sunset: Date } {
102
+ let times = { dt: this.dt = dt, sunrise: undefined, sunset: undefined };
130
103
  if (this.isValid) {
131
- let time = new Date().getTime();
132
- if (time >= times.sunset.getTime() && time < times.sunrise.getTime()) return true;
133
- }
134
- return false;
135
- }
136
- public calculate() {
137
- if (typeof this.dt !== 'undefined'
138
- && typeof this._latitude !== 'undefined'
139
- && typeof this._longitude !== 'undefined'
140
- && typeof this._zenith !== 'undefined') {
141
104
  let sunriseLocal = this.sunriseLocalTime;
142
105
  let sunsetLocal = this.sunsetLocalTime;
143
106
  if (typeof sunriseLocal !== 'undefined') {
144
107
  sunriseLocal = (sunriseLocal - this.longitudeHours);
145
108
  while (sunriseLocal >= 24) sunriseLocal -= 24;
146
109
  while (sunriseLocal < 0) sunriseLocal += 24;
147
- this._dtSunrise = this.toLocalTime(sunriseLocal);
110
+ times.sunrise = this.toLocalTime(sunriseLocal);
148
111
  }
149
- else this._dtSunrise = undefined;
112
+ else times.sunrise = undefined;
150
113
  if (typeof sunsetLocal !== 'undefined') {
151
114
  sunsetLocal = (sunsetLocal - this.longitudeHours);
152
115
  while (sunsetLocal >= 24) sunsetLocal -= 24;
153
116
  while (sunsetLocal < 0) sunsetLocal += 24;
154
- this._dtSunset = this.toLocalTime(sunsetLocal);
117
+ times.sunset = this.toLocalTime(sunsetLocal);
155
118
  }
156
- else this._dtSunset = undefined;
157
- logger.verbose(`sunriseLocal:${sunriseLocal} sunsetLocal:${sunsetLocal} Calculating Heliotrope Valid`);
158
- this.isValid = typeof this._dtSunrise !== 'undefined' && typeof this._dtSunset !== 'undefined';
159
- this.isCalculated = true;
119
+ else times.sunset = undefined;
160
120
  }
161
- else {
162
- logger.warn(`dt:${this.dt} lat:${this._latitude} lon:${this._longitude} Not enough information to calculate Heliotrope. See https://github.com/tagyoureit/nodejs-poolController/issues/245`);
163
- this.isValid = false;
164
- this._dtSunset = undefined;
165
- this._dtSunrise = undefined;
121
+ return times;
122
+ }
123
+
124
+ }
125
+ export class Heliotrope {
126
+ constructor() {
127
+ this.isCalculated = false;
128
+ this._zenith = 90 + 50 / 60;
129
+ }
130
+ public isCalculated: boolean = false;
131
+ public get isValid(): boolean { return typeof this.dt !== 'undefined' && typeof this.dt.getMonth === 'function' && typeof this._longitude === 'number' && typeof this._latitude === 'number'; }
132
+ public get date() { return this.dt; }
133
+ public set date(dt: Date) {
134
+ if (typeof this.dt === 'undefined' ||
135
+ this.dt.getFullYear() !== dt.getFullYear() ||
136
+ this.dt.getMonth() !== dt.getMonth() ||
137
+ this.dt.getDate() !== dt.getDate()) {
166
138
  this.isCalculated = false;
139
+ // Always store a copy since we don't want to create instances where the change doesn't get reflected. This
140
+ // also could hold onto references that we don't want held for garbage cleanup.
141
+ this.dt = typeof dt !== 'undefined' && typeof dt.getMonth === 'function' ? new Date(dt.getFullYear(), dt.getMonth(), dt.getDate(),
142
+ dt.getHours(), dt.getMinutes(), dt.getSeconds(), dt.getMilliseconds()) : undefined;
167
143
  }
168
-
144
+ }
145
+ public get longitude() { return this._longitude; }
146
+ public set longitude(lon: number) {
147
+ if (this._longitude !== lon) {
148
+ this.isCalculated = false;
149
+ }
150
+ this._longitude = lon;
151
+ }
152
+ public get latitude() { return this._latitude; }
153
+ public set latitude(lat: number) {
154
+ if (this._latitude !== lat) {
155
+ this.isCalculated = false;
156
+ }
157
+ this._latitude = lat;
158
+ }
159
+ public get zenith() { return this._zenith; }
160
+ public set zenith(zen: number) {
161
+ if (this._zenith !== zen) this.isCalculated = false;
162
+ this._zenith = zen;
163
+ }
164
+ private dt: Date;
165
+ private _longitude: number;
166
+ private _latitude: number;
167
+ private _zenith: number;
168
+ private _dtSunrise: Date;
169
+ private _dtSunset: Date;
170
+ private _dtNextSunrise: Date;
171
+ private _dtNextSunset: Date;
172
+ private _dtPrevSunrise: Date;
173
+ private _dtPrevSunset: Date;
174
+ public get isNight(): boolean {
175
+ let times = this.calculatedTimes;
176
+ if (this.isValid) {
177
+ let time = new Date().getTime();
178
+ if (time >= times.sunset.getTime()) // We are after sunset.
179
+ return time < times.nextSunrise.getTime(); // It is night so long as we are less than the next sunrise.
180
+ // Normally this would be updated on 1 min after midnight so it should never be true.
181
+ else
182
+ return time < times.sunrise.getTime(); // If the Heliotrope is updated then we need to declare pre-sunrise to be night.
183
+ // This is the normal condition where it would be night since at 1 min after midnight the sunrise/sunset
184
+ // will get updated. So it will be before sunrise that it will still be night.
185
+ }
186
+ return false;
187
+ }
188
+ public calculate(dt: Date): { isValid: boolean, dt: Date, sunrise: Date, sunset: Date, nextSunrise: Date, nextSunset: Date, prevSunrise: Date, prevSunset: Date } {
189
+ let ctx = new HeliotropeContext(this.longitude, this.latitude, this.zenith);
190
+ let ret = { isValid: ctx.isValid, dt: dt, sunrise: undefined, sunset: undefined, nextSunrise: undefined, nextSunset: undefined, prevSunrise: undefined, prevSunset: undefined };
191
+ if (ctx.isValid) {
192
+ let tToday = ctx.calculate(dt);
193
+ let tTom = ctx.calculate(new Date(dt.getTime() + 86400000));
194
+ let tYesterday = ctx.calculate(new Date(dt.getTime() - 86400000));
195
+ ret.sunrise = tToday.sunrise;
196
+ ret.sunset = tToday.sunset;
197
+ ret.nextSunrise = tTom.sunrise;
198
+ ret.nextSunset = tTom.sunset;
199
+ ret.prevSunrise = tYesterday.sunrise;
200
+ ret.prevSunset = tYesterday.sunset;
201
+ }
202
+ return ret;
203
+ }
204
+ private calcInternal() {
205
+ if (this.isValid) {
206
+ let times = this.calculate(this.dt);
207
+ this._dtSunrise = times.sunrise;
208
+ this._dtSunset = times.sunset;
209
+ this._dtNextSunrise = times.nextSunrise;
210
+ this._dtNextSunset = times.nextSunset;
211
+ this._dtPrevSunrise = times.prevSunrise;
212
+ this._dtPrevSunset = times.prevSunset;
213
+ this.isCalculated = true;
214
+ logger.verbose(`Calculated Heliotrope: sunrise:${Timestamp.toISOLocal(this._dtSunrise)} sunset:${Timestamp.toISOLocal(this._dtSunset)}`);
215
+ }
216
+ else
217
+ logger.warn(`dt:${this.dt} lat:${this._latitude} lon:${this._longitude} Not enough information to calculate Heliotrope. See https://github.com/tagyoureit/nodejs-poolController/issues/245`);
169
218
  }
170
219
  public get sunrise(): Date {
171
- if (!this.isCalculated) this.calculate();
220
+ if (!this.isCalculated) this.calcInternal();
172
221
  return this._dtSunrise;
173
222
  }
174
223
  public get sunset(): Date {
175
- if (!this.isCalculated) this.calculate();
224
+ if (!this.isCalculated) this.calcInternal();
176
225
  return this._dtSunset;
177
226
  }
178
- public get calculatedTimes(): any { return { sunrise: this.sunrise, sunset: this.sunset, isValid: this.isValid }; }
227
+ public get nextSunrise(): Date {
228
+ if (!this.isCalculated) this.calcInternal();
229
+ return this._dtNextSunrise;
230
+ }
231
+ public get nextSunset(): Date {
232
+ if (!this.isCalculated) this.calcInternal();
233
+ return this._dtNextSunset;
234
+ }
235
+ public get prevSunrise(): Date {
236
+ if (!this.isCalculated) this.calcInternal();
237
+ return this._dtPrevSunrise;
238
+ }
239
+ public get prevSunset(): Date {
240
+ if (!this.isCalculated) this.calcInternal();
241
+ return this._dtPrevSunset;
242
+ }
243
+ public get calculatedTimes(): { sunrise?: Date, sunset?: Date, nextSunrise?: Date, nextSunset?: Date, prevSunrise?: Date, prevSunset: Date, isValid: boolean } { return { sunrise: this.sunrise, sunset: this.sunset, nextSunrise: this.nextSunrise, nextSunset: this.nextSunset, prevSunrise: this.prevSunrise, prevSunset: this.prevSunset, isValid: this.isValid }; }
244
+ public calcAdjustedTimes(dt: Date, hours = 0, min = 0): { sunrise?: Date, sunset?: Date, nextSunrise?: Date, nextSunset?: Date, prevSunrise?: Date, prevSunset: Date, isValid: boolean } {
245
+ if (this.dt.getFullYear() === dt.getFullYear() && this.dt.getMonth() === dt.getMonth() && this.dt.getDate() === dt.getDate()) return this.getAdjustedTimes(hours, min);
246
+ let ms = (hours * 3600000) + (min * 60000);
247
+ let times = this.calculate(dt);
248
+ return {
249
+ sunrise: new Date(times.sunrise.getTime() + ms),
250
+ sunset: new Date(times.sunset.getTime() + ms),
251
+ nextSunrise: new Date(times.nextSunrise.getTime() + ms),
252
+ nextSunset: new Date(times.nextSunset.getTime() + ms),
253
+ prevSunrise: new Date(times.prevSunrise.getTime() + ms),
254
+ prevSunset: new Date(times.prevSunset.getTime() + ms),
255
+ isValid: this.isValid
256
+ }
257
+ }
258
+ public getAdjustedTimes(hours = 0, min = 0): { sunrise?: Date, sunset?: Date, nextSunrise?: Date, nextSunset?: Date, prevSunrise?: Date, prevSunset: Date, isValid: boolean } {
259
+ let ms = (hours * 3600000) + (min * 60000);
260
+ return {
261
+ sunrise: new Date(this.sunrise.getTime() + ms),
262
+ sunset: new Date(this.sunset.getTime() + ms),
263
+ nextSunrise: new Date(this.nextSunrise.getTime() + ms),
264
+ nextSunset: new Date(this.nextSunset.getTime() + ms),
265
+ prevSunrise: new Date(this.prevSunrise.getTime() + ms),
266
+ prevSunset: new Date(this.prevSunset.getTime() + ms),
267
+ isValid: this.isValid
268
+ }
269
+ }
179
270
  }
180
271
  export class Timestamp {
181
272
  private static dateTextISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/;
@@ -185,11 +276,15 @@ export class Timestamp {
185
276
  constructor(dt?: Date | string) {
186
277
  if (typeof dt === 'string') this._dt = new Date(dt);
187
278
  else this._dt = dt || new Date();
279
+ if (!this.isValid) this._dt = new Date();
188
280
  this.emitter = new EventEmitter();
189
281
  }
190
282
  private _isUpdating: boolean = false;
191
283
  public static get now(): Timestamp { return new Timestamp(); }
192
- public toDate() { return this._dt; }
284
+ public toDate(): Date { return this._dt; }
285
+ public get isValid() {
286
+ return this._dt instanceof Date && !isNaN(this._dt.getTime());
287
+ }
193
288
  public set isUpdating(val: boolean) { this._isUpdating = val; }
194
289
  public get isUpdating(): boolean { return this._isUpdating; }
195
290
  public get hours(): number { return this._dt.getHours(); }
@@ -221,7 +316,8 @@ export class Timestamp {
221
316
  public set fullYear(val: number) { this._dt.setFullYear(val); }
222
317
  public get year(): number { return this._dt.getFullYear(); }
223
318
  public set year(val: number) {
224
- let y = val < 100 ? (Math.floor(this._dt.getFullYear() / 100) * 100) + val : val;
319
+ let dt = new Date();
320
+ let y = val < 100 ? (Math.floor(dt.getFullYear() / 100) * 100) + val : val;
225
321
  if (y !== this.year) {
226
322
  this._dt.setFullYear(y);
227
323
  this.emitter.emit('change');
@@ -244,7 +340,8 @@ export class Timestamp {
244
340
  public getDay(): number { return this._dt.getDay(); }
245
341
  public getTime() { return this._dt.getTime(); }
246
342
  public format(): string { return Timestamp.toISOLocal(this._dt); }
247
- public static toISOLocal(dt): string {
343
+ public static toISOLocal(dt: Date): string {
344
+ if (typeof dt === 'undefined' || typeof dt.getTime !== 'function' || isNaN(dt.getTime())) return '';
248
345
  let tzo = dt.getTimezoneOffset();
249
346
  var pad = function (n) {
250
347
  var t = Math.floor(Math.abs(n));
@@ -75,10 +75,11 @@ interface IPoolSystem {
75
75
  export class PoolSystem implements IPoolSystem {
76
76
  public _hasChanged: boolean = false;
77
77
  public isReady: boolean = false;
78
+ private _startOCPTimer: NodeJS.Timeout;
78
79
  constructor() {
79
80
  this.cfgPath = path.posix.join(process.cwd(), '/data/poolConfig.json');
80
81
  }
81
- public getAvailableControllerTypes(include:string[] = ['easytouch', 'intellitouch', 'intellicenter', 'nixie']) {
82
+ public getAvailableControllerTypes(include:string[] = ['easytouch', 'intellitouch', 'intellicenter', 'suntouch', 'nixie']) {
82
83
  let arr = [];
83
84
  if (include.indexOf('easytouch')>=0) arr.push({
84
85
  type: 'easytouch', name: 'EasyTouch',
@@ -124,7 +125,14 @@ export class PoolSystem implements IPoolSystem {
124
125
  { val: 7, name: 'i10D', part: '523029Z', desc: 'IntelliCenter i10D', bodies: 2, valves: 2, circuits: 11, shared: false, dual: true, chlorinators: 2, chemControllers: 2 },
125
126
  ]
126
127
  });
127
- if (include.indexOf('nixie')>=0) arr.push({
128
+ if (include.indexOf('suntouch') >= 0) arr.push({
129
+ type: 'suntouch', name: 'SunTouch',
130
+ models: [
131
+ { val: 41, name: 'stshared', part: '520820', desc: 'Pool and Spa controller', bodies: 2, valves: 4, circuits: 5, single: false, shared: true, dual: false, features: 4, chlorinators: 1, chemControllers: 1 },
132
+ { val: 40, name: 'stsingle', part: '520819', desc: 'Pool or Spa controller', bodies: 2, valves: 4, circuits: 5, single: true, shared: true, dual: false, features: 4, chlorinators: 1, chemControllers: 1 }
133
+ ]
134
+ })
135
+ if (include.indexOf('nixie') >= 0) arr.push({
128
136
  type: 'nixie', name: 'Nixie', canChange: true,
129
137
  models: [
130
138
  { val: 0, name: 'nxp', part: 'NXP', desc: 'Nixie Single Body', bodies: 1, shared: false, dual: false },
@@ -143,7 +151,9 @@ export class PoolSystem implements IPoolSystem {
143
151
  if (this.controllerType === 'unknown' || typeof this.controllerType === 'undefined') {
144
152
  // Delay for 7.5 seconds to give any OCPs a chance to start emitting messages.
145
153
  logger.info(`Listening for any installed OCPs`);
146
- setTimeout(() => { self.initNixieController(); }, 7500);
154
+ if (this._startOCPTimer) clearTimeout(this._startOCPTimer);
155
+ this._startOCPTimer = null;
156
+ this._startOCPTimer = setTimeout(() => { self.initNixieController(); }, 7500);
147
157
  }
148
158
  else
149
159
  this.initNixieController();
@@ -217,15 +227,40 @@ export class PoolSystem implements IPoolSystem {
217
227
  }
218
228
 
219
229
  public resetSystem() {
220
- conn.pauseAll();
221
- this.resetData();
222
- state.resetData();
223
- this.data.controllerType === 'unknown';
224
- state.equipment.controllerType = ControllerType.Unknown;
225
- this.controllerType = ControllerType.Unknown;
226
- state.status = 0;
227
- this.board = BoardFactory.fromControllerType(ControllerType.Unknown, this);
228
- setTimeout(function () { state.status = 0; conn.resumeAll(); }, 0);
230
+ logger.info(`Resetting System to initial defaults`);
231
+ (async () => {
232
+ await this.board.closeAsync();
233
+ logger.info(`Closed ${this.controllerType} board`);
234
+ this.controllerType = ControllerType.Unknown;
235
+ })();
236
+
237
+ /*
238
+ let self = this;
239
+ if (this.controllerType === ControllerType.Nixie) {
240
+ (async () => {
241
+ await this.board.closeAsync();
242
+ this.board = BoardFactory.fromControllerType(ControllerType.Nixie, this);
243
+ let board = sys.board as NixieBoard;
244
+ await board.initNixieBoard();
245
+ })();
246
+ state.status = 0;
247
+
248
+ logger.info(`Listening for any installed OCPs`);
249
+ setTimeout(() => { self.initNixieController(); }, 7500);
250
+
251
+ }
252
+ else {
253
+ conn.pauseAll();
254
+ this.resetData();
255
+ state.resetData();
256
+ this.data.controllerType === 'unknown';
257
+ state.equipment.controllerType = ControllerType.Unknown;
258
+ this.controllerType = ControllerType.Unknown;
259
+ state.status = 0;
260
+ this.board = BoardFactory.fromControllerType(ControllerType.Unknown, this);
261
+ setTimeout(function () { state.status = 0; conn.resumeAll(); }, 0);
262
+ }
263
+ */
229
264
  }
230
265
  public get anslq25ControllerType(): ControllerType { return this.data.anslq25.controllerType as ControllerType; }
231
266
  public set anslq25ControllerType(val: ControllerType) {
@@ -250,11 +285,17 @@ export class PoolSystem implements IPoolSystem {
250
285
  // Only go in here if there is a change to the controller type.
251
286
  this.resetData(); // Clear the configuration data.
252
287
  state.resetData(); // Clear the state data.
288
+ state.status = 0; // We are performing a re-initialize.
253
289
  this.data.controllerType = val;
254
290
  EquipmentStateMessage.initDefaults();
291
+
255
292
  // We are actually changing the config so lets clear out all the data.
256
293
  this.board = BoardFactory.fromControllerType(val, this);
257
- if (this.data.controllerType === ControllerType.Unknown) setTimeout(() => { self.initNixieController(); }, 7500);
294
+ if (this.data.controllerType === ControllerType.Unknown) {
295
+ if (this._startOCPTimer) clearTimeout(this._startOCPTimer);
296
+ this._startOCPTimer = null;
297
+ this._startOCPTimer = setTimeout(() => { self.initNixieController(); }, 7500);
298
+ }
258
299
  }
259
300
  }
260
301
  public resetData() {
@@ -1144,6 +1185,8 @@ export class Schedule extends EqItem {
1144
1185
  if (typeof this.data.startTimeType === 'undefined') this.data.startTimeType = 0;
1145
1186
  if (typeof this.data.endTimeType === 'undefined') this.data.endTimeType = 0;
1146
1187
  if (typeof this.data.display === 'undefined') this.data.display = 0;
1188
+ if (typeof this.data.startTimeOffset === 'undefined') this.data.startTimeOffset = 0;
1189
+ if (typeof this.data.endTimeOffset === 'undefined') this.data.endTimeOffset = 0;
1147
1190
  }
1148
1191
 
1149
1192
  // todo: investigate schedules having startDate and _startDate
@@ -1155,6 +1198,11 @@ export class Schedule extends EqItem {
1155
1198
  public set startTime(val: number) { this.setDataVal('startTime', val); }
1156
1199
  public get endTime(): number { return this.data.endTime; }
1157
1200
  public set endTime(val: number) { this.setDataVal('endTime', val); }
1201
+ public get startTimeOffset(): number { return this.data.startTimeOffset || 0; }
1202
+ public set startTimeOffset(val: number) { this.setDataVal('startTimeOffset', val); }
1203
+ public get endTimeOffset(): number { return this.data.endTimeOffset || 0; }
1204
+ public set endTimeOffset(val: number) { this.setDataVal('endTimeOffset', val); }
1205
+
1158
1206
  public get scheduleDays(): number { return this.data.scheduleDays; }
1159
1207
  public set scheduleDays(val: number) { this.setDataVal('scheduleDays', val); }
1160
1208
  public get circuit(): number { return this.data.circuit; }
@@ -1177,7 +1225,10 @@ export class Schedule extends EqItem {
1177
1225
  public set startDay(val: number) { if (typeof this._startDate === 'undefined') this._startDate = new Date(); this._startDate.setDate(val); this._saveStartDate(); }
1178
1226
  public get startYear(): number { if (typeof this._startDate === 'undefined') this._startDate = new Date(); return this._startDate.getFullYear(); }
1179
1227
  public set startYear(val: number) { if (typeof this._startDate === 'undefined') this._startDate = new Date(); this._startDate.setFullYear(val < 100 ? val + 2000 : val); this._saveStartDate(); }
1180
- public get startDate(): Date { return typeof this._startDate === 'undefined' ? this._startDate = new Date() : this._startDate; }
1228
+ public get startDate(): Date {
1229
+ this._startDate = typeof this._startDate === 'undefined' ? new Date(this.data.startDate) : this._startDate;
1230
+ return typeof this._startDate === 'undefined' || isNaN(this._startDate.getTime()) ? new Date() : this._startDate;
1231
+ }
1181
1232
  public set startDate(val: Date) { this._startDate = val; }
1182
1233
  public get scheduleType(): number | any { return this.data.scheduleType; }
1183
1234
  public set scheduleType(val: number | any) { this.setDataVal('scheduleType', sys.board.valueMaps.scheduleTypes.encode(val)); }
@@ -1390,13 +1441,20 @@ export class PumpCollection extends EqItemCollection<Pump> {
1390
1441
  constructor(data: any, name?: string) { super(data, name || "pumps"); }
1391
1442
  public createItem(data: any): Pump { return new Pump(data); }
1392
1443
  public getDualSpeed(add?: boolean): Pump {
1393
- return this.getItemById(10, add, { id: 10, type: 65, name: 'Two Speed' });
1444
+ let ds = sys.board.valueMaps.pumpTypes.getValue('ds');
1445
+ let pump = this.find(x => x.type === ds);
1446
+ return typeof pump !== 'undefined' ? pump : this.getItemById(10, add, { id: 10, type: ds, name: 'Two Speed', master: 0 });
1394
1447
  }
1395
- public getPumpByAddress(address: number, add?: boolean, data?: any) {
1448
+ public removePumpByAddress(address: number) {
1396
1449
  let pmp = this.find(elem => elem.address === address);
1397
- if (typeof pmp !== 'undefined') return this.createItem(pmp);
1450
+ if (typeof pmp !== 'undefined') return this.removeItemById(pmp.id);
1451
+ return pmp;
1452
+ }
1453
+ public getPumpByAddress(address: number, add?: boolean, data?: any) : Pump {
1454
+ let pmp = this.find(elem => elem.address === address);
1455
+ if (typeof pmp !== 'undefined') return pmp;
1398
1456
  if (typeof add !== 'undefined' && add) return this.add(data || { id: this.data.length + 1, address: address });
1399
- return this.createItem(data || { id: this.data.length + 1, address: address });
1457
+ return this.createItem(data || { id: this.getNextEquipmentId(), address: address });
1400
1458
  }
1401
1459
  public getNextEquipmentId(range?: EquipmentIdRange, exclude?: number[]): number { return super.getNextEquipmentId(typeof range === 'undefined' ? sys.board.equipmentIds.pumps : range, exclude); }
1402
1460
  }
@@ -2440,6 +2498,7 @@ export class ChemicalPh extends Chemical {
2440
2498
  public getExtended() {
2441
2499
  let chem = super.getExtended();
2442
2500
  chem.probe = this.probe.getExtended();
2501
+ chem.tank = this.tank.getExtended();
2443
2502
  chem.phSupply = sys.board.valueMaps.phSupplyTypes.transform(this.phSupply);
2444
2503
  chem.doserType = sys.board.valueMaps.phDoserTypes.transform(this.doserType);
2445
2504
  return chem;
@@ -2470,6 +2529,7 @@ export class ChemicalORP extends Chemical {
2470
2529
  public getExtended() {
2471
2530
  let chem = super.getExtended();
2472
2531
  chem.probe = this.probe.getExtended();
2532
+ chem.tank = this.tank.getExtended();
2473
2533
  chem.doserType = sys.board.valueMaps.orpDoserTypes.transform(this.doserType);
2474
2534
  return chem;
2475
2535
  }
@@ -135,6 +135,21 @@ export class ManualPriorityDelay extends EquipmentDelay {
135
135
  logger.info(`Manual Operation Priority cancelled for ${this.circuitState.name}`);
136
136
  this._delayTimer = undefined;
137
137
  this.circuitState.manualPriorityActive = false;
138
+ // Rip through all the schedules and clear the manual priority.
139
+ let sscheds = state.schedules.getActiveSchedules();
140
+ let circIds = [];
141
+ for (let i = 0; i < sscheds.length; i++) {
142
+ let ssched = sscheds[i];
143
+ ssched.manualPriorityActive = false;
144
+ if (!circIds.includes(ssched.circuit)) circIds.push(ssched.circuit);
145
+ }
146
+ for (let i = 0; i < circIds.length; i++) {
147
+ let circ = sys.circuits.getInterfaceById(circIds[i]);
148
+ if (!circ.isActive) continue;
149
+ let cstate = state.circuits.getInterfaceById(circ.id);
150
+ sys.board.circuits.setEndTime(circ, cstate, cstate.isOn, true);
151
+ }
152
+
138
153
  delayMgr.deleteDelay(this.id);
139
154
  }
140
155
  public clearDelay() {