nodejs-poolcontroller 8.1.2 → 8.4.0

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 (106) hide show
  1. package/.eslintrc.json +36 -36
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -84
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -12
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -28
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  6. package/.github/copilot-instructions.md +63 -0
  7. package/.github/workflows/ghcr-publish.yml +67 -0
  8. package/AGENTS.md +597 -0
  9. package/CONTRIBUTING.md +74 -74
  10. package/Changelog +292 -257
  11. package/Dockerfile +62 -19
  12. package/Gruntfile.js +40 -40
  13. package/LICENSE +661 -661
  14. package/README.md +318 -191
  15. package/anslq25/MessagesMock.ts +221 -221
  16. package/anslq25/boards/MockBoardFactory.ts +49 -49
  17. package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
  18. package/anslq25/boards/MockSystemBoard.ts +216 -216
  19. package/anslq25/chemistry/MockChlorinator.ts +98 -98
  20. package/anslq25/pumps/MockPump.ts +83 -83
  21. package/app.ts +115 -115
  22. package/config/Config.ts +57 -7
  23. package/config/VersionCheck.ts +63 -35
  24. package/controller/Constants.ts +809 -805
  25. package/controller/Equipment.ts +2688 -2664
  26. package/controller/Errors.ts +181 -181
  27. package/controller/Lockouts.ts +549 -549
  28. package/controller/State.ts +3738 -3690
  29. package/controller/boards/AquaLinkBoard.ts +1003 -1003
  30. package/controller/boards/BoardFactory.ts +53 -53
  31. package/controller/boards/EasyTouchBoard.ts +3202 -3202
  32. package/controller/boards/IntelliCenterBoard.ts +4393 -3899
  33. package/controller/boards/IntelliComBoard.ts +69 -69
  34. package/controller/boards/IntelliTouchBoard.ts +382 -382
  35. package/controller/boards/NixieBoard.ts +1944 -1929
  36. package/controller/boards/SunTouchBoard.ts +400 -400
  37. package/controller/boards/SystemBoard.ts +5268 -5268
  38. package/controller/comms/Comms.ts +1272 -1214
  39. package/controller/comms/ScreenLogic.ts +1665 -1665
  40. package/controller/comms/messages/Messages.ts +1433 -1243
  41. package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
  42. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  43. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  44. package/controller/comms/messages/config/ConfigMessage.ts +6 -0
  45. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  46. package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
  47. package/controller/comms/messages/config/EquipmentMessage.ts +216 -210
  48. package/controller/comms/messages/config/ExternalMessage.ts +96 -10
  49. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  50. package/controller/comms/messages/config/GeneralMessage.ts +0 -0
  51. package/controller/comms/messages/config/HeaterMessage.ts +0 -0
  52. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  53. package/controller/comms/messages/config/OptionsMessage.ts +194 -174
  54. package/controller/comms/messages/config/PumpMessage.ts +0 -0
  55. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  56. package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
  57. package/controller/comms/messages/config/SecurityMessage.ts +0 -0
  58. package/controller/comms/messages/config/ValveMessage.ts +0 -0
  59. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
  60. package/controller/comms/messages/status/EquipmentStateMessage.ts +1158 -822
  61. package/controller/comms/messages/status/HeaterStateMessage.ts +135 -135
  62. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
  63. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
  64. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  65. package/controller/comms/messages/status/RegalModbusStateMessage.ts +411 -0
  66. package/controller/comms/messages/status/VersionMessage.ts +103 -41
  67. package/controller/nixie/Nixie.ts +173 -173
  68. package/controller/nixie/NixieEquipment.ts +104 -104
  69. package/controller/nixie/bodies/Body.ts +120 -120
  70. package/controller/nixie/bodies/Filter.ts +135 -135
  71. package/controller/nixie/chemistry/ChemController.ts +2724 -2724
  72. package/controller/nixie/chemistry/ChemDoser.ts +806 -806
  73. package/controller/nixie/chemistry/Chlorinator.ts +367 -367
  74. package/controller/nixie/circuits/Circuit.ts +478 -478
  75. package/controller/nixie/heaters/Heater.ts +834 -834
  76. package/controller/nixie/pumps/Pump.ts +1194 -996
  77. package/controller/nixie/schedules/Schedule.ts +401 -401
  78. package/controller/nixie/valves/Valve.ts +170 -170
  79. package/defaultConfig.json +352 -347
  80. package/docker-compose.yml +32 -0
  81. package/logger/DataLogger.ts +448 -448
  82. package/logger/Logger.ts +448 -436
  83. package/package.json +58 -60
  84. package/sendSocket.js +32 -32
  85. package/tsconfig.json +25 -25
  86. package/types/express-multer.d.ts +32 -0
  87. package/web/Server.ts +1937 -1927
  88. package/web/bindings/aqualinkD.json +559 -559
  89. package/web/bindings/influxDB.json +1066 -1066
  90. package/web/bindings/mqtt.json +721 -721
  91. package/web/bindings/mqttAlt.json +746 -746
  92. package/web/bindings/rulesManager.json +54 -54
  93. package/web/bindings/smartThings-Hubitat.json +31 -31
  94. package/web/bindings/valveRelays.json +20 -20
  95. package/web/bindings/vera.json +25 -25
  96. package/web/interfaces/baseInterface.ts +188 -188
  97. package/web/interfaces/httpInterface.ts +148 -148
  98. package/web/interfaces/influxInterface.ts +283 -283
  99. package/web/interfaces/mqttInterface.ts +695 -695
  100. package/web/interfaces/ruleInterface.ts +101 -87
  101. package/web/services/config/Config.ts +1063 -1053
  102. package/web/services/config/ConfigSocket.ts +0 -0
  103. package/web/services/state/State.ts +0 -0
  104. package/web/services/state/StateSocket.ts +0 -0
  105. package/web/services/utilities/Utilities.ts +233 -233
  106. package/.github/workflows/docker-publish-njsPC-linux.yml +0 -50
@@ -1,806 +1,810 @@
1
- /* nodejs-poolController. An application to control pool equipment.
2
- Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
- Russell Goldin, tagyoureit. russ.goldin@gmail.com
4
-
5
- This program is free software: you can redistribute it and/or modify
6
- it under the terms of the GNU Affero General Public License as
7
- published by the Free Software Foundation, either version 3 of the
8
- License, or (at your option) any later version.
9
-
10
- This program is distributed in the hope that it will be useful,
11
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- GNU Affero General Public License for more details.
14
-
15
- You should have received a copy of the GNU Affero General Public License
16
- along with this program. If not, see <http://www.gnu.org/licenses/>.
17
- */
18
- import { EventEmitter } from 'events';
19
- import { logger } from "../logger/Logger";
20
- import * as util from 'util';
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;
26
- }
27
- private dMath = {
28
- sin: function (deg) { return Math.sin(deg * (Math.PI / 180)); },
29
- cos: function (deg) { return Math.cos(deg * (Math.PI / 180)); },
30
- tan: function (deg) { return Math.tan(deg * (Math.PI / 180)); },
31
- asin: function (x) { return (180 / Math.PI) * Math.asin(x); },
32
- acos: function (x) { return (180 / Math.PI) * Math.acos(x); },
33
- atan: function (x) { return (180 / Math.PI) * Math.atan(x); }
34
- }
35
- private dt: Date;
36
- private _longitude: number;
37
- private _latitude: number;
38
- private _zenith: number;
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'; }
43
- private get longitudeHours(): number { return this.longitude / 15.0; }
44
- private get doy(): number { return Math.ceil((this.dt.getTime() - new Date(this.dt.getFullYear(), 0, 1).getTime()) / 8.64e7); }
45
- private get sunriseApproxTime(): number { return this.doy + ((6.0 - this.longitudeHours) / 24.0); }
46
- private get sunsetApproxTime(): number { return this.doy + ((18.0 - this.longitudeHours) / 24.0); }
47
- private get sunriseAnomaly(): number { return (this.sunriseApproxTime * 0.9856) - 3.289; }
48
- private get sunsetAnomaly(): number { return (this.sunsetApproxTime * 0.9856) - 3.289; }
49
- private calcTrueLongitude(anomaly: number) {
50
- let tl = anomaly + (1.916 * this.dMath.sin(anomaly)) + (0.020 * this.dMath.sin(2 * anomaly)) + 282.634;
51
- while (tl >= 360.0) tl -= 360.0;
52
- while (tl < 0) tl += 360.0;
53
- return tl;
54
- }
55
- private get sunriseLongitude(): number { return this.calcTrueLongitude(this.sunriseAnomaly); } // Check
56
- private get sunsetLongitude(): number { return this.calcTrueLongitude(this.sunsetAnomaly); }
57
- private calcRightAscension(trueLongitude) {
58
- let asc = this.dMath.atan(0.91764 * this.dMath.tan(trueLongitude));
59
- while (asc >= 360.0) asc -= 360.0;
60
- while (asc < 0) asc += 360.0;
61
- let lQuad = Math.floor(trueLongitude / 90.0) * 90.0;
62
- let ascQuad = Math.floor(asc / 90.0) * 90.0;
63
- return (asc + (lQuad - ascQuad)) / 15.0;
64
- }
65
- private get sunriseAscension(): number { return this.calcRightAscension(this.sunriseLongitude); }
66
- private get sunsetAscension(): number { return this.calcRightAscension(this.sunsetLongitude); }
67
- private calcSinDeclination(trueLongitude: number): number { return 0.39782 * this.dMath.sin(trueLongitude); }
68
- private calcCosDeclination(sinDeclination: number): number { return this.dMath.cos(this.dMath.asin(sinDeclination)); }
69
- private get sunriseSinDeclination(): number { return this.calcSinDeclination(this.sunriseLongitude); }
70
- private get sunsetSinDeclination(): number { return this.calcSinDeclination(this.sunsetLongitude); }
71
- private get sunriseCosDeclination(): number { return this.calcCosDeclination(this.sunriseSinDeclination); }
72
- private get sunsetCosDeclination(): number { return this.calcCosDeclination(this.sunsetSinDeclination); }
73
- private calcLocalHourAngle(sinDeclination: number, cosDeclination: number): number { return (this.dMath.cos(this.zenith) - (sinDeclination * this.dMath.sin(this.latitude))) / (cosDeclination * this.dMath.cos(this.latitude)); }
74
- private get sunriseLocalTime(): number {
75
- let ha = this.calcLocalHourAngle(this.sunriseSinDeclination, this.sunriseCosDeclination);
76
- if (ha >= -1 && ha <= 1) {
77
- let h = (360 - this.dMath.acos(ha)) / 15;
78
- return (h + this.sunriseAscension - (0.06571 * this.sunriseApproxTime) - 6.622);
79
- }
80
- // The sun never rises here.
81
- return;
82
- }
83
- private get sunsetLocalTime(): number {
84
- let ha = this.calcLocalHourAngle(this.sunsetSinDeclination, this.sunsetCosDeclination);
85
- if (ha >= -1 && ha <= 1) {
86
- let h = this.dMath.acos(ha) / 15;
87
- return (h + this.sunsetAscension - (0.06571 * this.sunsetApproxTime) - 6.622);
88
- }
89
- // The sun never sets here.
90
- return;
91
- }
92
- private toLocalTime(time: number) {
93
- let off = -(this.dt.getTimezoneOffset() * 60 * 1000);
94
- let utcHours = Math.floor(time);
95
- let utcMins = Math.floor(60 * (time - utcHours));
96
- let utcSecs = Math.floor(3600 * (time - utcHours - (utcMins / 60)));
97
- let dtLocal = new Date(new Date(this.dt.getFullYear(), this.dt.getMonth(), this.dt.getDate(), utcHours, utcMins, utcSecs).getTime() + off);
98
- dtLocal.setFullYear(this.dt.getFullYear(), this.dt.getMonth(), this.dt.getDate());
99
- return dtLocal;
100
- }
101
- public calculate(dt: Date): { dt: Date, sunrise: Date, sunset: Date } {
102
- let times = { dt: this.dt = dt, sunrise: undefined, sunset: undefined };
103
- if (this.isValid) {
104
- let sunriseLocal = this.sunriseLocalTime;
105
- let sunsetLocal = this.sunsetLocalTime;
106
- if (typeof sunriseLocal !== 'undefined') {
107
- sunriseLocal = (sunriseLocal - this.longitudeHours);
108
- while (sunriseLocal >= 24) sunriseLocal -= 24;
109
- while (sunriseLocal < 0) sunriseLocal += 24;
110
- times.sunrise = this.toLocalTime(sunriseLocal);
111
- }
112
- else times.sunrise = undefined;
113
- if (typeof sunsetLocal !== 'undefined') {
114
- sunsetLocal = (sunsetLocal - this.longitudeHours);
115
- while (sunsetLocal >= 24) sunsetLocal -= 24;
116
- while (sunsetLocal < 0) sunsetLocal += 24;
117
- times.sunset = this.toLocalTime(sunsetLocal);
118
- }
119
- else times.sunset = undefined;
120
- }
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()) {
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;
143
- }
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`);
218
- }
219
- public get sunrise(): Date {
220
- if (!this.isCalculated) this.calcInternal();
221
- return this._dtSunrise;
222
- }
223
- public get sunset(): Date {
224
- if (!this.isCalculated) this.calcInternal();
225
- return this._dtSunset;
226
- }
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
- }
270
- }
271
- export class Timestamp {
272
- private static dateTextISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/;
273
- private static dateTextAjax = /^\/Date\((d|-|.*)\)[\/|\\]$/;
274
- private _dt: Date;
275
- public emitter: EventEmitter;
276
- constructor(dt?: Date | string) {
277
- if (typeof dt === 'string') this._dt = new Date(dt);
278
- else this._dt = dt || new Date();
279
- if (!this.isValid) this._dt = new Date();
280
- this.emitter = new EventEmitter();
281
- }
282
- private _isUpdating: boolean = false;
283
- public static get now(): Timestamp { return new Timestamp(); }
284
- public toDate(): Date { return this._dt; }
285
- public get isValid() {
286
- return this._dt instanceof Date && !isNaN(this._dt.getTime());
287
- }
288
- public set isUpdating(val: boolean) { this._isUpdating = val; }
289
- public get isUpdating(): boolean { return this._isUpdating; }
290
- public get hours(): number { return this._dt.getHours(); }
291
- public set hours(val: number) {
292
- if (this.hours !== val) {
293
- this._dt.setHours(val);
294
- this.emitter.emit('change');
295
- }
296
- }
297
- public get minutes(): number { return this._dt.getMinutes(); }
298
- public set minutes(val: number) {
299
- if (this.minutes !== val) {
300
- this._dt.setMinutes(val);
301
- this.emitter.emit('change');
302
- }
303
- }
304
- public get seconds(): number { return this._dt.getSeconds(); }
305
- public set seconds(val: number) {
306
- if (this.seconds !== val) {
307
- this._dt.setSeconds(val);
308
- // No need to emit this change as Intellicenter only
309
- // reports to the minute.
310
- //this.emitter.emit('change');
311
- }
312
- }
313
- public get milliseconds(): number { return this._dt.getMilliseconds(); }
314
- public set milliseconds(val: number) { this._dt.setMilliseconds(val); }
315
- public get fullYear(): number { return this._dt.getFullYear(); }
316
- public set fullYear(val: number) { this._dt.setFullYear(val); }
317
- public get year(): number { return this._dt.getFullYear(); }
318
- public set year(val: number) {
319
- let dt = new Date();
320
- let y = val < 100 ? (Math.floor(dt.getFullYear() / 100) * 100) + val : val;
321
- if (y !== this.year) {
322
- this._dt.setFullYear(y);
323
- this.emitter.emit('change');
324
- }
325
- }
326
- public get month(): number { return this._dt.getMonth() + 1; }
327
- public set month(val: number) {
328
- if (this.month !== val) {
329
- this._dt.setMonth(val - 1);
330
- this.emitter.emit('change');
331
- }
332
- }
333
- public get date(): number { return this._dt.getDate(); }
334
- public set date(val: number) {
335
- if (this.date !== val) {
336
- this._dt.setDate(val);
337
- this.emitter.emit('change');
338
- }
339
- }
340
- public getDay(): number { return this._dt.getDay(); }
341
- public getTime() { return this._dt.getTime(); }
342
- public format(): string { return Timestamp.toISOLocal(this._dt); }
343
- public static toISOLocal(dt: Date): string {
344
- if (typeof dt === 'undefined' || typeof dt.getTime !== 'function' || isNaN(dt.getTime())) return '';
345
- let tzo = dt.getTimezoneOffset();
346
- var pad = function (n) {
347
- var t = Math.floor(Math.abs(n));
348
- return (t < 10 ? '0' : '') + t;
349
- };
350
- return new Date(dt.getTime() - (tzo * 60000)).toISOString().slice(0, -1) + (tzo > 0 ? '-' : '+') + pad(tzo / 60) + pad(tzo % 60)
351
- }
352
- public setTimeFromSystemClock() {
353
- let dt = this._dt;
354
- this._dt = new Date();
355
- // RKS: This was emitting down to the millisecond. We are only concerned with time to the minute.
356
- if (typeof dt === 'undefined' ||
357
- dt.getMinutes() !== this._dt.getMinutes() ||
358
- dt.getHours() !== this._dt.getHours() ||
359
- dt.getDate() !== this._dt.getDate() ||
360
- dt.getMonth() !== this._dt.getMonth() ||
361
- dt.getFullYear() !== this._dt.getFullYear())
362
- this.emitter.emit('change');
363
- }
364
- public calcTZOffset(): { tzOffset: number, adjustDST: boolean } {
365
- let obj = { tzOffset: 0, adjustDST: false };
366
- let dateJan = new Date(this._dt.getFullYear(), 0, 1, 2);
367
- let dateJul = new Date(this._dt.getFullYear(), 6, 1, 2);
368
- obj.tzOffset = dateJan.getTimezoneOffset() / 60 * -1;
369
- obj.adjustDST = dateJan.getTimezoneOffset() - dateJul.getTimezoneOffset() > 0;
370
- return obj;
371
- }
372
- public addHours(hours: number, minutes: number = 0, seconds: number = 0, milliseconds: number = 0) {
373
- let interval = hours * 3600000;
374
- interval += minutes * 60000;
375
- interval += seconds * 1000;
376
- interval += milliseconds;
377
- this._dt.setMilliseconds(this._dt.getMilliseconds() + interval);
378
- return this;
379
- }
380
- public addMinutes(minutes: number, seconds?: number, milliseconds?: number): Timestamp { return this.addHours(0, minutes, seconds, this.milliseconds); }
381
- public addSeconds(seconds: number, milliseconds: number = 0): Timestamp { return this.addHours(0, 0, seconds, milliseconds); }
382
- public addMilliseconds(milliseconds: number): Timestamp { return this.addHours(0, 0, 0, milliseconds); }
383
- public static today() {
384
- let dt = new Date();
385
- dt.setHours(0, 0, 0, 0);
386
- return new Timestamp(dt);
387
- }
388
- public startOfDay() {
389
- // This makes the returned timestamp immutable.
390
- let dt = new Date(this._dt.getTime());
391
- dt.setHours(0, 0, 0, 0);
392
- return new Timestamp(dt);
393
- }
394
- public clone() { return new Timestamp(new Date(this._dt)); }
395
- public static locale() { return Intl.DateTimeFormat().resolvedOptions().locale; }
396
- public static parseISO(val: string): RegExpExecArray { return typeof val !== 'undefined' && val ? Timestamp.dateTextISO.exec(val) : null; }
397
- public static parseAjax(val: string): RegExpExecArray { return typeof val !== 'undefined' && val ? Timestamp.dateTextAjax.exec(val) : null; }
398
- public static dayOfWeek(time: Timestamp): number {
399
- // for IntelliTouch set date/time
400
- if (time.toDate().getUTCDay() === 0)
401
- return 0;
402
- else
403
- return Math.pow(2, time.toDate().getUTCDay() - 1);
404
- }
405
- }
406
- export enum ControllerType {
407
- IntelliCenter = 'intellicenter',
408
- IntelliTouch = 'intellitouch',
409
- IntelliCom = 'intellicom',
410
- EasyTouch = 'easytouch',
411
- Unknown = 'unknown',
412
- // Virtual = 'virtual',
413
- Nixie = 'nixie',
414
- AquaLink = 'aqualink',
415
- SunTouch = 'suntouch',
416
- None = 'none'
417
- }
418
-
419
- export class Utils {
420
- public makeBool(val) {
421
- if (typeof (val) === 'boolean') return val;
422
- if (typeof (val) === 'undefined') return false;
423
- if (typeof (val) === 'number') return val >= 1;
424
- if (typeof (val) === 'string') {
425
- if (val === '' || typeof val === 'undefined') return false;
426
- switch (val.toLowerCase().trim()) {
427
- case 'on':
428
- case 'true':
429
- case 'yes':
430
- case 'y':
431
- return true;
432
- case 'off':
433
- case 'false':
434
- case 'no':
435
- case 'n':
436
- return false;
437
- }
438
- if (!isNaN(parseInt(val, 10))) return parseInt(val, 10) >= 1;
439
- }
440
- return false;
441
- }
442
- public static jsonReviver = (key, value) => {
443
- if (typeof value === 'string') {
444
- let d = Timestamp.parseISO(value);
445
- // By parsing the date and then creating a new date from that we will get
446
- // the date in the proper timezone.
447
- if (d) return new Date(Date.parse(value));
448
- d = Timestamp.parseAjax(value);
449
- if (d) {
450
- // Not sure we will be seeing ajax dates but this is
451
- // something that we may see from external sources.
452
- let a = d[1].split(/[-+,.]/);
453
- return new Date(a[0] ? +a[0] : 0 - +a[1]);
454
- }
455
- }
456
- return value;
457
- }
458
- public static jsonReplacer = (key, value) => {
459
- // Add in code to change Timestamp into a string.
460
- if (typeof value !== 'undefined' && value) {
461
- if (typeof value.format === 'function') return value.format();
462
- else if (typeof value.getTime === 'function') return Timestamp.toISOLocal(value);
463
- }
464
- return value;
465
- }
466
- public static parseJSON(json: string) { return JSON.parse(json, Utils.jsonReviver); }
467
- public static stringifyJSON(obj: any) { return JSON.stringify(obj, Utils.jsonReplacer); }
468
- public uuid(a?, b?) { for (b = a = ''; a++ < 36; b += a * 51 & 52 ? (a ^ 15 ? 8 ^ Math.random() * (a ^ 20 ? 16 : 4) : 4).toString(16) : '-'); return b }
469
- public convert = {
470
- temperature: {
471
- f: {
472
- k: (val) => { return (val - 32) * (5 / 9) + 273.15; },
473
- c: (val) => { return (val - 32) * (5 / 9); },
474
- f: (val) => { return val; }
475
- },
476
- c: {
477
- k: (val) => { return val + 273.15; },
478
- c: (val) => { return val; },
479
- f: (val) => { return (val * (9 / 5)) + 32; }
480
- },
481
- k: {
482
- k: (val) => { return val; },
483
- c: (val) => { return val - 273.15; },
484
- f: (val) => { return ((val - 273.15) * (9 / 5)) + 32; }
485
- },
486
- convertUnits: (val: number, from: string, to: string) => {
487
- if (typeof val !== 'number') return null;
488
- let fn = this.convert.temperature[from.toLowerCase()];
489
- if (typeof fn !== 'undefined' && typeof fn[to.toLowerCase()] === 'function') return fn[to.toLowerCase()](val);
490
- }
491
- },
492
- pressure: {
493
- bar: {
494
- kpa: (val) => { return val * 100; },
495
- kilopascal: (val) => { return val * 100; },
496
- pa: (val) => { return val * 100000; },
497
- pascal: (val) => { return val * 100000; },
498
- atm: (val) => { return val * 0.986923; },
499
- atmosphere: (val) => { return val * 0.986923; },
500
- psi: (val) => { return val * 14.5038; },
501
- bar: (val) => { return val; }
502
- },
503
- kpa: {
504
- kpa: (val) => { return val; },
505
- kilopascal: (val) => { return val; },
506
- pa: (val) => { return val * 1000; },
507
- pascal: (val) => { return val * 1000; },
508
- atm: (val) => { return val / 101.325; },
509
- atmosphere: (val) => { return val / 101.325; },
510
- psi: (val) => { return val * 0.145038; },
511
- bar: (val) => { return val * .01; }
512
- },
513
- kilopascal: {
514
- kpa: (val) => { return val; },
515
- kilopascal: (val) => { return val; },
516
- pa: (val) => { return val * 1000; },
517
- pascal: (val) => { return val * 1000; },
518
- atm: (val) => { return val / 101.325; },
519
- atmosphere: (val) => { return val / 101.325; },
520
- psi: (val) => { return val * 0.145038; },
521
- bar: (val) => { return val * .01; }
522
- },
523
- pa: {
524
- kpa: (val) => { return val / 1000; },
525
- kilopascal: (val) => { return val / 1000; },
526
- pa: (val) => { return val; },
527
- pascal: (val) => { return val; },
528
- atm: (val) => { return val / 101325; },
529
- atmosphere: (val) => { return val / 101325; },
530
- psi: (val) => { return val * 0.000145038; },
531
- bar: (val) => { return val / 100000; }
532
- },
533
- pascal: {
534
- kpa: (val) => { return val / 1000; },
535
- kilopascal: (val) => { return val / 1000; },
536
- pa: (val) => { return val; },
537
- pascal: (val) => { return val; },
538
- atm: (val) => { return val / 101325; },
539
- atmosphere: (val) => { return val / 101325; },
540
- psi: (val) => { return val * 0.000145038; },
541
- bar: (val) => { return val / 100000; }
542
- },
543
- atm: {
544
- kpa: (val) => { return val * 101.325; },
545
- kilopascal: (val) => { return val * 101.325; },
546
- pa: (val) => { return val * 101325; },
547
- pascal: (val) => { return val * 101325; },
548
- atm: (val) => { return val; },
549
- atmosphere: (val) => { return val; },
550
- psi: (val) => { return val * 14.6959; },
551
- bar: (val) => { return val * 1.01325; }
552
- },
553
- atmosphere: {
554
- kpa: (val) => { return val * 101.325; },
555
- kilopascal: (val) => { return val * 101.325; },
556
- pa: (val) => { return val * 101325; },
557
- pascal: (val) => { return val * 101325; },
558
- atm: (val) => { return val; },
559
- atmosphere: (val) => { return val; },
560
- psi: (val) => { return val * 14.6959; },
561
- bar: (val) => { return val * 1.01325; }
562
- },
563
- psi: {
564
- kpa: (val) => { return val * 6.89476; },
565
- kilopascal: (val) => { return val * 6.89476; },
566
- pa: (val) => { return val * 6894.76; },
567
- pascal: (val) => { return val * 6894.76; },
568
- atm: (val) => { return val * 0.068046; },
569
- atmosphere: (val) => { return 0.068046; },
570
- psi: (val) => { return val; },
571
- bar: (val) => { return val * 0.0689476; }
572
- },
573
- convertUnits: (val: number, from: string, to: string) => {
574
- if (typeof val !== 'number') return null;
575
- let fn = this.convert.pressure[from.toLowerCase()];
576
- if (typeof fn !== 'undefined' && typeof fn[to.toLowerCase()] === 'function') return fn[to.toLowerCase()](val);
577
- }
578
-
579
- },
580
- volume: {
581
- gal: {
582
- l: (val) => { return val * 3.78541; },
583
- ml: (val) => { return val * 3.78541 * 1000; },
584
- cl: (val) => { return val * 3.78541 * 100; },
585
- gal: (val) => { return val; },
586
- oz: (val) => { return val * 128; },
587
- pint: (val) => { return val / 8; },
588
- qt: (val) => { return val / 4; },
589
- },
590
- l: {
591
- l: (val) => { return val; },
592
- ml: (val) => { return val * 1000; },
593
- cl: (val) => { return val * 100; },
594
- gal: (val) => { return val * 0.264172; },
595
- oz: (val) => { return val * 33.814; },
596
- pint: (val) => { return val * 2.11338; },
597
- qt: (val) => { return val * 1.05669; },
598
- },
599
- ml: {
600
- l: (val) => { return val * .001; },
601
- ml: (val) => { return val; },
602
- cl: (val) => { return val * .1; },
603
- gal: (val) => { return val * 0.000264172; },
604
- oz: (val) => { return val * 0.033814; },
605
- pint: (val) => { return val * 0.00211338; },
606
- qt: (val) => { return val * 0.00105669; },
607
- },
608
- cl: {
609
- l: (val) => { return val * .01; },
610
- ml: (val) => { return val * 10; },
611
- cl: (val) => { return val; },
612
- gal: (val) => { return val * 0.00264172; },
613
- oz: (val) => { return val * 0.33814; },
614
- pint: (val) => { return val * 0.0211338; },
615
- qt: (val) => { return val * 0.0105669; },
616
- },
617
- oz: {
618
- l: (val) => { return val * 0.0295735; },
619
- ml: (val) => { return val * 29.5735; },
620
- cl: (val) => { return val * 2.95735; },
621
- gal: (val) => { return val * 0.0078125; },
622
- oz: (val) => { return val; },
623
- pint: (val) => { return val * 0.0625; },
624
- qt: (val) => { return val * 0.03125; },
625
- },
626
- pint: {
627
- l: (val) => { return val * 0.473176; },
628
- ml: (val) => { return val * 473.176; },
629
- cl: (val) => { return val * 47.3176; },
630
- gal: (val) => { return val * 0.125; },
631
- oz: (val) => { return val * 16; },
632
- pint: (val) => { return val; },
633
- qt: (val) => { return val * 0.5; },
634
- },
635
- qt: {
636
- l: (val) => { return val * 0.946353; },
637
- ml: (val) => { return val * 946.353; },
638
- cl: (val) => { return val * 94.6353; },
639
- gal: (val) => { return val * 0.25; },
640
- oz: (val) => { return val * 32; },
641
- pint: (val) => { return val * 2; },
642
- qt: (val) => { return val; },
643
-
644
- },
645
- convertUnits: (val: number, from: string, to: string) => {
646
- if (typeof val !== 'number') return null;
647
- let fn = this.convert.volume[from.toLowerCase()];
648
- if (typeof fn !== 'undefined' && typeof fn[to.toLowerCase()] === 'function') return fn[to.toLowerCase()](val);
649
- }
650
- }
651
- }
652
- public formatDuration(seconds: number): string {
653
- if (seconds === 0) return '0sec';
654
- var fmt = '';
655
- let hrs = Math.floor(seconds / 3600);
656
- let min = Math.floor((seconds - (hrs * 3600)) / 60);
657
- let sec = Math.round(seconds) - ((hrs * 3600) + (min * 60));
658
- if (hrs > 1) fmt += (hrs.toString() + 'hrs');
659
- else if (hrs > 0) fmt += (hrs.toString() + 'hr');
660
-
661
- if (min > 0) fmt += ' ' + (min + 'min');
662
- if (sec > 0) fmt += ' ' + (sec + 'sec');
663
- return fmt.trim();
664
- }
665
- public parseNumber(val: string): number {
666
- if (typeof val === 'number') return val;
667
- else if (typeof val === 'undefined' || val === null) return;
668
- let tval = val.replace(/[^0-9\.\-]+/g, '');
669
- let v;
670
- if (tval.indexOf('.') !== -1) {
671
- v = parseFloat(tval);
672
- v = this.roundNumber(v, tval.length - tval.indexOf('.'));
673
- }
674
- else v = parseInt(tval, 10);
675
- return v;
676
- }
677
- public roundNumber(num, dec) { return +(Math.round(+(num + 'e+' + dec)) + 'e-' + dec); };
678
- public parseDuration(duration: string): number {
679
- if (typeof duration === 'number') return parseInt(duration, 10);
680
- else if (typeof duration !== 'string') return 0;
681
- let seconds = 0;
682
- let arr = duration.split(' ');
683
- for (let i = 0; i < arr.length; i++) {
684
- let s = arr[i];
685
- if (s.endsWith('sec')) seconds += this.parseNumber(s);
686
- if (s.endsWith('min')) seconds += (this.parseNumber(s) * 60);
687
- if (s.endsWith('hr')) seconds += (this.parseNumber(s) * 3600);
688
- if (s.endsWith('hrs')) seconds += (this.parseNumber(s) * 3600);
689
- }
690
- return seconds;
691
- }
692
- public isNullOrEmpty(val: any) { return (typeof val === 'string') ? val === null || val === '' : typeof val === 'undefined' || val === null; }
693
- // public sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
694
- // Use this method to get around the circular references for the toJSON function.
695
- public serialize(obj, fn?: (key, value) => any): string {
696
- let op = Object.getOwnPropertyNames(obj);
697
- let s = '{';
698
- for (let i in op) {
699
- let prop = op[i];
700
- if (typeof obj[prop] === 'undefined' || typeof obj[prop] === 'function') continue;
701
- let v = typeof fn === 'function' ? fn(prop, obj[prop]) : obj[prop];
702
- if (typeof v === 'undefined') continue;
703
- s += `"${prop}": ${JSON.stringify(v, fn)},`;
704
- }
705
- if (s.charAt(s.length - 1) === ',') s = s.substring(0, s.length - 1);
706
- return s + '}';
707
- }
708
- public replaceProps(obj, fn?: (key, value) => any): any {
709
- let op = Object.getOwnPropertyNames(obj);
710
- if (typeof obj === 'undefined') return undefined;
711
- let isArray = Array.isArray(obj);
712
- let o = isArray ? [] : {};
713
- for (let i in op) {
714
- let prop = op[i];
715
- if (typeof obj[prop] === 'undefined' || typeof obj[prop] === 'function') continue;
716
- let v = typeof fn === 'function' ? fn(prop, obj[prop]) : obj[prop];
717
- if (typeof v === 'undefined') continue;
718
- if (util.types.isBoxedPrimitive(v))
719
- o[prop] = v.valueOf();
720
- if (Array.isArray(v) || typeof v === 'object')
721
- o[prop] = utils.replaceProps(v, fn);
722
- else
723
- o[prop] = v;
724
- }
725
- return o;
726
- }
727
- public findLineByLeastSquares(values_x: number[], values_y: number[]): number[][] {
728
- var x_sum = 0;
729
- var y_sum = 0;
730
- var xy_sum = 0;
731
- var xx_sum = 0;
732
- var count = 0;
733
-
734
- /*
735
- * The above is just for quick access, makes the program faster
736
- */
737
- var x = 0;
738
- var y = 0;
739
- var values_length = values_x.length;
740
-
741
- if (values_length != values_y.length) {
742
- throw new Error('The parameters values_x and values_y need to have same size!');
743
- }
744
-
745
- /*
746
- * Above and below cover edge cases
747
- */
748
- if (values_length === 0) {
749
- return [[], []];
750
- }
751
-
752
- /*
753
- * Calculate the sum for each of the parts necessary.
754
- */
755
- for (let i = 0; i < values_length; i++) {
756
- x = values_x[i];
757
- y = values_y[i];
758
- x_sum += x;
759
- y_sum += y;
760
- xx_sum += x * x;
761
- xy_sum += x * y;
762
- count++;
763
- }
764
-
765
- /*
766
- * Calculate m and b for the line equation:
767
- * y = x * m + b
768
- */
769
- var m = (count * xy_sum - x_sum * y_sum) / (count * xx_sum - x_sum * x_sum);
770
- var b = (y_sum / count) - (m * x_sum) / count;
771
-
772
- /*
773
- * We then return the x and y data points according to our fit
774
- */
775
- var result_values_x: number[] = [];
776
- var result_values_y: number[] = [];
777
-
778
- for (let i = 0; i < values_length; i++) {
779
- x = values_x[i];
780
- y = x * m + b;
781
- result_values_x.push(x);
782
- result_values_y.push(y);
783
- }
784
-
785
- return [result_values_x, result_values_y];
786
- }
787
- public slopeOfLeastSquares(values_x: number[], values_y: number[]): number {
788
- let points = utils.findLineByLeastSquares(values_x, values_y);
789
- let points_x = points[0];
790
- let points_y = points[1];
791
- let slope = (points_y[0] - points_y[points_y.length - 1]) / (points_x[0] - points_x[points_x.length - 1]);
792
- return slope;
793
- }
794
- private random(bounds: number, onlyPositive: boolean = false) {
795
- let rand = Math.random() * bounds;
796
- if (!onlyPositive) {
797
- if (Math.random() <= .5) rand = rand * -1;
798
- }
799
- return rand;
800
- }
801
- public dec2bin(dec) {
802
- return (dec >>> 0).toString(2).padStart(8, '0');
803
- }
804
- }
805
-
1
+ /* nodejs-poolController. An application to control pool equipment.
2
+ Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
+ Russell Goldin, tagyoureit. russ.goldin@gmail.com
4
+
5
+ This program is free software: you can redistribute it and/or modify
6
+ it under the terms of the GNU Affero General Public License as
7
+ published by the Free Software Foundation, either version 3 of the
8
+ License, or (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License
16
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+ */
18
+ import { EventEmitter } from 'events';
19
+ import { logger } from "../logger/Logger";
20
+ import * as util from 'util';
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;
26
+ }
27
+ private dMath = {
28
+ sin: function (deg) { return Math.sin(deg * (Math.PI / 180)); },
29
+ cos: function (deg) { return Math.cos(deg * (Math.PI / 180)); },
30
+ tan: function (deg) { return Math.tan(deg * (Math.PI / 180)); },
31
+ asin: function (x) { return (180 / Math.PI) * Math.asin(x); },
32
+ acos: function (x) { return (180 / Math.PI) * Math.acos(x); },
33
+ atan: function (x) { return (180 / Math.PI) * Math.atan(x); }
34
+ }
35
+ private dt: Date;
36
+ private _longitude: number;
37
+ private _latitude: number;
38
+ private _zenith: number;
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'; }
43
+ private get longitudeHours(): number { return this.longitude / 15.0; }
44
+ private get doy(): number { return Math.ceil((this.dt.getTime() - new Date(this.dt.getFullYear(), 0, 1).getTime()) / 8.64e7); }
45
+ private get sunriseApproxTime(): number { return this.doy + ((6.0 - this.longitudeHours) / 24.0); }
46
+ private get sunsetApproxTime(): number { return this.doy + ((18.0 - this.longitudeHours) / 24.0); }
47
+ private get sunriseAnomaly(): number { return (this.sunriseApproxTime * 0.9856) - 3.289; }
48
+ private get sunsetAnomaly(): number { return (this.sunsetApproxTime * 0.9856) - 3.289; }
49
+ private calcTrueLongitude(anomaly: number) {
50
+ let tl = anomaly + (1.916 * this.dMath.sin(anomaly)) + (0.020 * this.dMath.sin(2 * anomaly)) + 282.634;
51
+ while (tl >= 360.0) tl -= 360.0;
52
+ while (tl < 0) tl += 360.0;
53
+ return tl;
54
+ }
55
+ private get sunriseLongitude(): number { return this.calcTrueLongitude(this.sunriseAnomaly); } // Check
56
+ private get sunsetLongitude(): number { return this.calcTrueLongitude(this.sunsetAnomaly); }
57
+ private calcRightAscension(trueLongitude) {
58
+ let asc = this.dMath.atan(0.91764 * this.dMath.tan(trueLongitude));
59
+ while (asc >= 360.0) asc -= 360.0;
60
+ while (asc < 0) asc += 360.0;
61
+ let lQuad = Math.floor(trueLongitude / 90.0) * 90.0;
62
+ let ascQuad = Math.floor(asc / 90.0) * 90.0;
63
+ return (asc + (lQuad - ascQuad)) / 15.0;
64
+ }
65
+ private get sunriseAscension(): number { return this.calcRightAscension(this.sunriseLongitude); }
66
+ private get sunsetAscension(): number { return this.calcRightAscension(this.sunsetLongitude); }
67
+ private calcSinDeclination(trueLongitude: number): number { return 0.39782 * this.dMath.sin(trueLongitude); }
68
+ private calcCosDeclination(sinDeclination: number): number { return this.dMath.cos(this.dMath.asin(sinDeclination)); }
69
+ private get sunriseSinDeclination(): number { return this.calcSinDeclination(this.sunriseLongitude); }
70
+ private get sunsetSinDeclination(): number { return this.calcSinDeclination(this.sunsetLongitude); }
71
+ private get sunriseCosDeclination(): number { return this.calcCosDeclination(this.sunriseSinDeclination); }
72
+ private get sunsetCosDeclination(): number { return this.calcCosDeclination(this.sunsetSinDeclination); }
73
+ private calcLocalHourAngle(sinDeclination: number, cosDeclination: number): number { return (this.dMath.cos(this.zenith) - (sinDeclination * this.dMath.sin(this.latitude))) / (cosDeclination * this.dMath.cos(this.latitude)); }
74
+ private get sunriseLocalTime(): number {
75
+ let ha = this.calcLocalHourAngle(this.sunriseSinDeclination, this.sunriseCosDeclination);
76
+ if (ha >= -1 && ha <= 1) {
77
+ let h = (360 - this.dMath.acos(ha)) / 15;
78
+ return (h + this.sunriseAscension - (0.06571 * this.sunriseApproxTime) - 6.622);
79
+ }
80
+ // The sun never rises here.
81
+ return;
82
+ }
83
+ private get sunsetLocalTime(): number {
84
+ let ha = this.calcLocalHourAngle(this.sunsetSinDeclination, this.sunsetCosDeclination);
85
+ if (ha >= -1 && ha <= 1) {
86
+ let h = this.dMath.acos(ha) / 15;
87
+ return (h + this.sunsetAscension - (0.06571 * this.sunsetApproxTime) - 6.622);
88
+ }
89
+ // The sun never sets here.
90
+ return;
91
+ }
92
+ private toLocalTime(time: number) {
93
+ let off = -(this.dt.getTimezoneOffset() * 60 * 1000);
94
+ let utcHours = Math.floor(time);
95
+ let utcMins = Math.floor(60 * (time - utcHours));
96
+ let utcSecs = Math.floor(3600 * (time - utcHours - (utcMins / 60)));
97
+ let dtLocal = new Date(new Date(this.dt.getFullYear(), this.dt.getMonth(), this.dt.getDate(), utcHours, utcMins, utcSecs).getTime() + off);
98
+ dtLocal.setFullYear(this.dt.getFullYear(), this.dt.getMonth(), this.dt.getDate());
99
+ return dtLocal;
100
+ }
101
+ public calculate(dt: Date): { dt: Date, sunrise: Date, sunset: Date } {
102
+ let times = { dt: this.dt = dt, sunrise: undefined, sunset: undefined };
103
+ if (this.isValid) {
104
+ let sunriseLocal = this.sunriseLocalTime;
105
+ let sunsetLocal = this.sunsetLocalTime;
106
+ if (typeof sunriseLocal !== 'undefined') {
107
+ sunriseLocal = (sunriseLocal - this.longitudeHours);
108
+ while (sunriseLocal >= 24) sunriseLocal -= 24;
109
+ while (sunriseLocal < 0) sunriseLocal += 24;
110
+ times.sunrise = this.toLocalTime(sunriseLocal);
111
+ }
112
+ else times.sunrise = undefined;
113
+ if (typeof sunsetLocal !== 'undefined') {
114
+ sunsetLocal = (sunsetLocal - this.longitudeHours);
115
+ while (sunsetLocal >= 24) sunsetLocal -= 24;
116
+ while (sunsetLocal < 0) sunsetLocal += 24;
117
+ times.sunset = this.toLocalTime(sunsetLocal);
118
+ }
119
+ else times.sunset = undefined;
120
+ }
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()) {
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;
143
+ }
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
+ // Set isCalculated = true to prevent warning spam (6 warnings per minute).
218
+ // When user sets valid lat/lon, the setters reset isCalculated = false, triggering recalculation.
219
+ this.isCalculated = true;
220
+ 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`);
221
+ }
222
+ }
223
+ public get sunrise(): Date {
224
+ if (!this.isCalculated) this.calcInternal();
225
+ return this._dtSunrise;
226
+ }
227
+ public get sunset(): Date {
228
+ if (!this.isCalculated) this.calcInternal();
229
+ return this._dtSunset;
230
+ }
231
+ public get nextSunrise(): Date {
232
+ if (!this.isCalculated) this.calcInternal();
233
+ return this._dtNextSunrise;
234
+ }
235
+ public get nextSunset(): Date {
236
+ if (!this.isCalculated) this.calcInternal();
237
+ return this._dtNextSunset;
238
+ }
239
+ public get prevSunrise(): Date {
240
+ if (!this.isCalculated) this.calcInternal();
241
+ return this._dtPrevSunrise;
242
+ }
243
+ public get prevSunset(): Date {
244
+ if (!this.isCalculated) this.calcInternal();
245
+ return this._dtPrevSunset;
246
+ }
247
+ 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 }; }
248
+ public calcAdjustedTimes(dt: Date, hours = 0, min = 0): { sunrise?: Date, sunset?: Date, nextSunrise?: Date, nextSunset?: Date, prevSunrise?: Date, prevSunset: Date, isValid: boolean } {
249
+ if (this.dt.getFullYear() === dt.getFullYear() && this.dt.getMonth() === dt.getMonth() && this.dt.getDate() === dt.getDate()) return this.getAdjustedTimes(hours, min);
250
+ let ms = (hours * 3600000) + (min * 60000);
251
+ let times = this.calculate(dt);
252
+ return {
253
+ sunrise: new Date(times.sunrise.getTime() + ms),
254
+ sunset: new Date(times.sunset.getTime() + ms),
255
+ nextSunrise: new Date(times.nextSunrise.getTime() + ms),
256
+ nextSunset: new Date(times.nextSunset.getTime() + ms),
257
+ prevSunrise: new Date(times.prevSunrise.getTime() + ms),
258
+ prevSunset: new Date(times.prevSunset.getTime() + ms),
259
+ isValid: this.isValid
260
+ }
261
+ }
262
+ public getAdjustedTimes(hours = 0, min = 0): { sunrise?: Date, sunset?: Date, nextSunrise?: Date, nextSunset?: Date, prevSunrise?: Date, prevSunset: Date, isValid: boolean } {
263
+ let ms = (hours * 3600000) + (min * 60000);
264
+ return {
265
+ sunrise: new Date(this.sunrise.getTime() + ms),
266
+ sunset: new Date(this.sunset.getTime() + ms),
267
+ nextSunrise: new Date(this.nextSunrise.getTime() + ms),
268
+ nextSunset: new Date(this.nextSunset.getTime() + ms),
269
+ prevSunrise: new Date(this.prevSunrise.getTime() + ms),
270
+ prevSunset: new Date(this.prevSunset.getTime() + ms),
271
+ isValid: this.isValid
272
+ }
273
+ }
274
+ }
275
+ export class Timestamp {
276
+ private static dateTextISO = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*))(?:Z|(\+|-)([\d|:]*))?$/;
277
+ private static dateTextAjax = /^\/Date\((d|-|.*)\)[\/|\\]$/;
278
+ private _dt: Date;
279
+ public emitter: EventEmitter;
280
+ constructor(dt?: Date | string) {
281
+ if (typeof dt === 'string') this._dt = new Date(dt);
282
+ else this._dt = dt || new Date();
283
+ if (!this.isValid) this._dt = new Date();
284
+ this.emitter = new EventEmitter();
285
+ }
286
+ private _isUpdating: boolean = false;
287
+ public static get now(): Timestamp { return new Timestamp(); }
288
+ public toDate(): Date { return this._dt; }
289
+ public get isValid() {
290
+ return this._dt instanceof Date && !isNaN(this._dt.getTime());
291
+ }
292
+ public set isUpdating(val: boolean) { this._isUpdating = val; }
293
+ public get isUpdating(): boolean { return this._isUpdating; }
294
+ public get hours(): number { return this._dt.getHours(); }
295
+ public set hours(val: number) {
296
+ if (this.hours !== val) {
297
+ this._dt.setHours(val);
298
+ this.emitter.emit('change');
299
+ }
300
+ }
301
+ public get minutes(): number { return this._dt.getMinutes(); }
302
+ public set minutes(val: number) {
303
+ if (this.minutes !== val) {
304
+ this._dt.setMinutes(val);
305
+ this.emitter.emit('change');
306
+ }
307
+ }
308
+ public get seconds(): number { return this._dt.getSeconds(); }
309
+ public set seconds(val: number) {
310
+ if (this.seconds !== val) {
311
+ this._dt.setSeconds(val);
312
+ // No need to emit this change as Intellicenter only
313
+ // reports to the minute.
314
+ //this.emitter.emit('change');
315
+ }
316
+ }
317
+ public get milliseconds(): number { return this._dt.getMilliseconds(); }
318
+ public set milliseconds(val: number) { this._dt.setMilliseconds(val); }
319
+ public get fullYear(): number { return this._dt.getFullYear(); }
320
+ public set fullYear(val: number) { this._dt.setFullYear(val); }
321
+ public get year(): number { return this._dt.getFullYear(); }
322
+ public set year(val: number) {
323
+ let dt = new Date();
324
+ let y = val < 100 ? (Math.floor(dt.getFullYear() / 100) * 100) + val : val;
325
+ if (y !== this.year) {
326
+ this._dt.setFullYear(y);
327
+ this.emitter.emit('change');
328
+ }
329
+ }
330
+ public get month(): number { return this._dt.getMonth() + 1; }
331
+ public set month(val: number) {
332
+ if (this.month !== val) {
333
+ this._dt.setMonth(val - 1);
334
+ this.emitter.emit('change');
335
+ }
336
+ }
337
+ public get date(): number { return this._dt.getDate(); }
338
+ public set date(val: number) {
339
+ if (this.date !== val) {
340
+ this._dt.setDate(val);
341
+ this.emitter.emit('change');
342
+ }
343
+ }
344
+ public getDay(): number { return this._dt.getDay(); }
345
+ public getTime() { return this._dt.getTime(); }
346
+ public format(): string { return Timestamp.toISOLocal(this._dt); }
347
+ public static toISOLocal(dt: Date): string {
348
+ if (typeof dt === 'undefined' || typeof dt.getTime !== 'function' || isNaN(dt.getTime())) return '';
349
+ let tzo = dt.getTimezoneOffset();
350
+ var pad = function (n) {
351
+ var t = Math.floor(Math.abs(n));
352
+ return (t < 10 ? '0' : '') + t;
353
+ };
354
+ return new Date(dt.getTime() - (tzo * 60000)).toISOString().slice(0, -1) + (tzo > 0 ? '-' : '+') + pad(tzo / 60) + pad(tzo % 60)
355
+ }
356
+ public setTimeFromSystemClock() {
357
+ let dt = this._dt;
358
+ this._dt = new Date();
359
+ // RKS: This was emitting down to the millisecond. We are only concerned with time to the minute.
360
+ if (typeof dt === 'undefined' ||
361
+ dt.getMinutes() !== this._dt.getMinutes() ||
362
+ dt.getHours() !== this._dt.getHours() ||
363
+ dt.getDate() !== this._dt.getDate() ||
364
+ dt.getMonth() !== this._dt.getMonth() ||
365
+ dt.getFullYear() !== this._dt.getFullYear())
366
+ this.emitter.emit('change');
367
+ }
368
+ public calcTZOffset(): { tzOffset: number, adjustDST: boolean } {
369
+ let obj = { tzOffset: 0, adjustDST: false };
370
+ let dateJan = new Date(this._dt.getFullYear(), 0, 1, 2);
371
+ let dateJul = new Date(this._dt.getFullYear(), 6, 1, 2);
372
+ obj.tzOffset = dateJan.getTimezoneOffset() / 60 * -1;
373
+ obj.adjustDST = dateJan.getTimezoneOffset() - dateJul.getTimezoneOffset() > 0;
374
+ return obj;
375
+ }
376
+ public addHours(hours: number, minutes: number = 0, seconds: number = 0, milliseconds: number = 0) {
377
+ let interval = hours * 3600000;
378
+ interval += minutes * 60000;
379
+ interval += seconds * 1000;
380
+ interval += milliseconds;
381
+ this._dt.setMilliseconds(this._dt.getMilliseconds() + interval);
382
+ return this;
383
+ }
384
+ public addMinutes(minutes: number, seconds?: number, milliseconds?: number): Timestamp { return this.addHours(0, minutes, seconds, this.milliseconds); }
385
+ public addSeconds(seconds: number, milliseconds: number = 0): Timestamp { return this.addHours(0, 0, seconds, milliseconds); }
386
+ public addMilliseconds(milliseconds: number): Timestamp { return this.addHours(0, 0, 0, milliseconds); }
387
+ public static today() {
388
+ let dt = new Date();
389
+ dt.setHours(0, 0, 0, 0);
390
+ return new Timestamp(dt);
391
+ }
392
+ public startOfDay() {
393
+ // This makes the returned timestamp immutable.
394
+ let dt = new Date(this._dt.getTime());
395
+ dt.setHours(0, 0, 0, 0);
396
+ return new Timestamp(dt);
397
+ }
398
+ public clone() { return new Timestamp(new Date(this._dt)); }
399
+ public static locale() { return Intl.DateTimeFormat().resolvedOptions().locale; }
400
+ public static parseISO(val: string): RegExpExecArray { return typeof val !== 'undefined' && val ? Timestamp.dateTextISO.exec(val) : null; }
401
+ public static parseAjax(val: string): RegExpExecArray { return typeof val !== 'undefined' && val ? Timestamp.dateTextAjax.exec(val) : null; }
402
+ public static dayOfWeek(time: Timestamp): number {
403
+ // for IntelliTouch set date/time
404
+ if (time.toDate().getUTCDay() === 0)
405
+ return 0;
406
+ else
407
+ return Math.pow(2, time.toDate().getUTCDay() - 1);
408
+ }
409
+ }
410
+ export enum ControllerType {
411
+ IntelliCenter = 'intellicenter',
412
+ IntelliTouch = 'intellitouch',
413
+ IntelliCom = 'intellicom',
414
+ EasyTouch = 'easytouch',
415
+ Unknown = 'unknown',
416
+ // Virtual = 'virtual',
417
+ Nixie = 'nixie',
418
+ AquaLink = 'aqualink',
419
+ SunTouch = 'suntouch',
420
+ None = 'none'
421
+ }
422
+
423
+ export class Utils {
424
+ public makeBool(val) {
425
+ if (typeof (val) === 'boolean') return val;
426
+ if (typeof (val) === 'undefined') return false;
427
+ if (typeof (val) === 'number') return val >= 1;
428
+ if (typeof (val) === 'string') {
429
+ if (val === '' || typeof val === 'undefined') return false;
430
+ switch (val.toLowerCase().trim()) {
431
+ case 'on':
432
+ case 'true':
433
+ case 'yes':
434
+ case 'y':
435
+ return true;
436
+ case 'off':
437
+ case 'false':
438
+ case 'no':
439
+ case 'n':
440
+ return false;
441
+ }
442
+ if (!isNaN(parseInt(val, 10))) return parseInt(val, 10) >= 1;
443
+ }
444
+ return false;
445
+ }
446
+ public static jsonReviver = (key, value) => {
447
+ if (typeof value === 'string') {
448
+ let d = Timestamp.parseISO(value);
449
+ // By parsing the date and then creating a new date from that we will get
450
+ // the date in the proper timezone.
451
+ if (d) return new Date(Date.parse(value));
452
+ d = Timestamp.parseAjax(value);
453
+ if (d) {
454
+ // Not sure we will be seeing ajax dates but this is
455
+ // something that we may see from external sources.
456
+ let a = d[1].split(/[-+,.]/);
457
+ return new Date(a[0] ? +a[0] : 0 - +a[1]);
458
+ }
459
+ }
460
+ return value;
461
+ }
462
+ public static jsonReplacer = (key, value) => {
463
+ // Add in code to change Timestamp into a string.
464
+ if (typeof value !== 'undefined' && value) {
465
+ if (typeof value.format === 'function') return value.format();
466
+ else if (typeof value.getTime === 'function') return Timestamp.toISOLocal(value);
467
+ }
468
+ return value;
469
+ }
470
+ public static parseJSON(json: string) { return JSON.parse(json, Utils.jsonReviver); }
471
+ public static stringifyJSON(obj: any) { return JSON.stringify(obj, Utils.jsonReplacer); }
472
+ public uuid(a?, b?) { for (b = a = ''; a++ < 36; b += a * 51 & 52 ? (a ^ 15 ? 8 ^ Math.random() * (a ^ 20 ? 16 : 4) : 4).toString(16) : '-'); return b }
473
+ public convert = {
474
+ temperature: {
475
+ f: {
476
+ k: (val) => { return (val - 32) * (5 / 9) + 273.15; },
477
+ c: (val) => { return (val - 32) * (5 / 9); },
478
+ f: (val) => { return val; }
479
+ },
480
+ c: {
481
+ k: (val) => { return val + 273.15; },
482
+ c: (val) => { return val; },
483
+ f: (val) => { return (val * (9 / 5)) + 32; }
484
+ },
485
+ k: {
486
+ k: (val) => { return val; },
487
+ c: (val) => { return val - 273.15; },
488
+ f: (val) => { return ((val - 273.15) * (9 / 5)) + 32; }
489
+ },
490
+ convertUnits: (val: number, from: string, to: string) => {
491
+ if (typeof val !== 'number') return null;
492
+ let fn = this.convert.temperature[from.toLowerCase()];
493
+ if (typeof fn !== 'undefined' && typeof fn[to.toLowerCase()] === 'function') return fn[to.toLowerCase()](val);
494
+ }
495
+ },
496
+ pressure: {
497
+ bar: {
498
+ kpa: (val) => { return val * 100; },
499
+ kilopascal: (val) => { return val * 100; },
500
+ pa: (val) => { return val * 100000; },
501
+ pascal: (val) => { return val * 100000; },
502
+ atm: (val) => { return val * 0.986923; },
503
+ atmosphere: (val) => { return val * 0.986923; },
504
+ psi: (val) => { return val * 14.5038; },
505
+ bar: (val) => { return val; }
506
+ },
507
+ kpa: {
508
+ kpa: (val) => { return val; },
509
+ kilopascal: (val) => { return val; },
510
+ pa: (val) => { return val * 1000; },
511
+ pascal: (val) => { return val * 1000; },
512
+ atm: (val) => { return val / 101.325; },
513
+ atmosphere: (val) => { return val / 101.325; },
514
+ psi: (val) => { return val * 0.145038; },
515
+ bar: (val) => { return val * .01; }
516
+ },
517
+ kilopascal: {
518
+ kpa: (val) => { return val; },
519
+ kilopascal: (val) => { return val; },
520
+ pa: (val) => { return val * 1000; },
521
+ pascal: (val) => { return val * 1000; },
522
+ atm: (val) => { return val / 101.325; },
523
+ atmosphere: (val) => { return val / 101.325; },
524
+ psi: (val) => { return val * 0.145038; },
525
+ bar: (val) => { return val * .01; }
526
+ },
527
+ pa: {
528
+ kpa: (val) => { return val / 1000; },
529
+ kilopascal: (val) => { return val / 1000; },
530
+ pa: (val) => { return val; },
531
+ pascal: (val) => { return val; },
532
+ atm: (val) => { return val / 101325; },
533
+ atmosphere: (val) => { return val / 101325; },
534
+ psi: (val) => { return val * 0.000145038; },
535
+ bar: (val) => { return val / 100000; }
536
+ },
537
+ pascal: {
538
+ kpa: (val) => { return val / 1000; },
539
+ kilopascal: (val) => { return val / 1000; },
540
+ pa: (val) => { return val; },
541
+ pascal: (val) => { return val; },
542
+ atm: (val) => { return val / 101325; },
543
+ atmosphere: (val) => { return val / 101325; },
544
+ psi: (val) => { return val * 0.000145038; },
545
+ bar: (val) => { return val / 100000; }
546
+ },
547
+ atm: {
548
+ kpa: (val) => { return val * 101.325; },
549
+ kilopascal: (val) => { return val * 101.325; },
550
+ pa: (val) => { return val * 101325; },
551
+ pascal: (val) => { return val * 101325; },
552
+ atm: (val) => { return val; },
553
+ atmosphere: (val) => { return val; },
554
+ psi: (val) => { return val * 14.6959; },
555
+ bar: (val) => { return val * 1.01325; }
556
+ },
557
+ atmosphere: {
558
+ kpa: (val) => { return val * 101.325; },
559
+ kilopascal: (val) => { return val * 101.325; },
560
+ pa: (val) => { return val * 101325; },
561
+ pascal: (val) => { return val * 101325; },
562
+ atm: (val) => { return val; },
563
+ atmosphere: (val) => { return val; },
564
+ psi: (val) => { return val * 14.6959; },
565
+ bar: (val) => { return val * 1.01325; }
566
+ },
567
+ psi: {
568
+ kpa: (val) => { return val * 6.89476; },
569
+ kilopascal: (val) => { return val * 6.89476; },
570
+ pa: (val) => { return val * 6894.76; },
571
+ pascal: (val) => { return val * 6894.76; },
572
+ atm: (val) => { return val * 0.068046; },
573
+ atmosphere: (val) => { return 0.068046; },
574
+ psi: (val) => { return val; },
575
+ bar: (val) => { return val * 0.0689476; }
576
+ },
577
+ convertUnits: (val: number, from: string, to: string) => {
578
+ if (typeof val !== 'number') return null;
579
+ let fn = this.convert.pressure[from.toLowerCase()];
580
+ if (typeof fn !== 'undefined' && typeof fn[to.toLowerCase()] === 'function') return fn[to.toLowerCase()](val);
581
+ }
582
+
583
+ },
584
+ volume: {
585
+ gal: {
586
+ l: (val) => { return val * 3.78541; },
587
+ ml: (val) => { return val * 3.78541 * 1000; },
588
+ cl: (val) => { return val * 3.78541 * 100; },
589
+ gal: (val) => { return val; },
590
+ oz: (val) => { return val * 128; },
591
+ pint: (val) => { return val / 8; },
592
+ qt: (val) => { return val / 4; },
593
+ },
594
+ l: {
595
+ l: (val) => { return val; },
596
+ ml: (val) => { return val * 1000; },
597
+ cl: (val) => { return val * 100; },
598
+ gal: (val) => { return val * 0.264172; },
599
+ oz: (val) => { return val * 33.814; },
600
+ pint: (val) => { return val * 2.11338; },
601
+ qt: (val) => { return val * 1.05669; },
602
+ },
603
+ ml: {
604
+ l: (val) => { return val * .001; },
605
+ ml: (val) => { return val; },
606
+ cl: (val) => { return val * .1; },
607
+ gal: (val) => { return val * 0.000264172; },
608
+ oz: (val) => { return val * 0.033814; },
609
+ pint: (val) => { return val * 0.00211338; },
610
+ qt: (val) => { return val * 0.00105669; },
611
+ },
612
+ cl: {
613
+ l: (val) => { return val * .01; },
614
+ ml: (val) => { return val * 10; },
615
+ cl: (val) => { return val; },
616
+ gal: (val) => { return val * 0.00264172; },
617
+ oz: (val) => { return val * 0.33814; },
618
+ pint: (val) => { return val * 0.0211338; },
619
+ qt: (val) => { return val * 0.0105669; },
620
+ },
621
+ oz: {
622
+ l: (val) => { return val * 0.0295735; },
623
+ ml: (val) => { return val * 29.5735; },
624
+ cl: (val) => { return val * 2.95735; },
625
+ gal: (val) => { return val * 0.0078125; },
626
+ oz: (val) => { return val; },
627
+ pint: (val) => { return val * 0.0625; },
628
+ qt: (val) => { return val * 0.03125; },
629
+ },
630
+ pint: {
631
+ l: (val) => { return val * 0.473176; },
632
+ ml: (val) => { return val * 473.176; },
633
+ cl: (val) => { return val * 47.3176; },
634
+ gal: (val) => { return val * 0.125; },
635
+ oz: (val) => { return val * 16; },
636
+ pint: (val) => { return val; },
637
+ qt: (val) => { return val * 0.5; },
638
+ },
639
+ qt: {
640
+ l: (val) => { return val * 0.946353; },
641
+ ml: (val) => { return val * 946.353; },
642
+ cl: (val) => { return val * 94.6353; },
643
+ gal: (val) => { return val * 0.25; },
644
+ oz: (val) => { return val * 32; },
645
+ pint: (val) => { return val * 2; },
646
+ qt: (val) => { return val; },
647
+
648
+ },
649
+ convertUnits: (val: number, from: string, to: string) => {
650
+ if (typeof val !== 'number') return null;
651
+ let fn = this.convert.volume[from.toLowerCase()];
652
+ if (typeof fn !== 'undefined' && typeof fn[to.toLowerCase()] === 'function') return fn[to.toLowerCase()](val);
653
+ }
654
+ }
655
+ }
656
+ public formatDuration(seconds: number): string {
657
+ if (seconds === 0) return '0sec';
658
+ var fmt = '';
659
+ let hrs = Math.floor(seconds / 3600);
660
+ let min = Math.floor((seconds - (hrs * 3600)) / 60);
661
+ let sec = Math.round(seconds) - ((hrs * 3600) + (min * 60));
662
+ if (hrs > 1) fmt += (hrs.toString() + 'hrs');
663
+ else if (hrs > 0) fmt += (hrs.toString() + 'hr');
664
+
665
+ if (min > 0) fmt += ' ' + (min + 'min');
666
+ if (sec > 0) fmt += ' ' + (sec + 'sec');
667
+ return fmt.trim();
668
+ }
669
+ public parseNumber(val: string): number {
670
+ if (typeof val === 'number') return val;
671
+ else if (typeof val === 'undefined' || val === null) return;
672
+ let tval = val.replace(/[^0-9\.\-]+/g, '');
673
+ let v;
674
+ if (tval.indexOf('.') !== -1) {
675
+ v = parseFloat(tval);
676
+ v = this.roundNumber(v, tval.length - tval.indexOf('.'));
677
+ }
678
+ else v = parseInt(tval, 10);
679
+ return v;
680
+ }
681
+ public roundNumber(num, dec) { return +(Math.round(+(num + 'e+' + dec)) + 'e-' + dec); };
682
+ public parseDuration(duration: string): number {
683
+ if (typeof duration === 'number') return parseInt(duration, 10);
684
+ else if (typeof duration !== 'string') return 0;
685
+ let seconds = 0;
686
+ let arr = duration.split(' ');
687
+ for (let i = 0; i < arr.length; i++) {
688
+ let s = arr[i];
689
+ if (s.endsWith('sec')) seconds += this.parseNumber(s);
690
+ if (s.endsWith('min')) seconds += (this.parseNumber(s) * 60);
691
+ if (s.endsWith('hr')) seconds += (this.parseNumber(s) * 3600);
692
+ if (s.endsWith('hrs')) seconds += (this.parseNumber(s) * 3600);
693
+ }
694
+ return seconds;
695
+ }
696
+ public isNullOrEmpty(val: any) { return (typeof val === 'string') ? val === null || val === '' : typeof val === 'undefined' || val === null; }
697
+ // public sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
698
+ // Use this method to get around the circular references for the toJSON function.
699
+ public serialize(obj, fn?: (key, value) => any): string {
700
+ let op = Object.getOwnPropertyNames(obj);
701
+ let s = '{';
702
+ for (let i in op) {
703
+ let prop = op[i];
704
+ if (typeof obj[prop] === 'undefined' || typeof obj[prop] === 'function') continue;
705
+ let v = typeof fn === 'function' ? fn(prop, obj[prop]) : obj[prop];
706
+ if (typeof v === 'undefined') continue;
707
+ s += `"${prop}": ${JSON.stringify(v, fn)},`;
708
+ }
709
+ if (s.charAt(s.length - 1) === ',') s = s.substring(0, s.length - 1);
710
+ return s + '}';
711
+ }
712
+ public replaceProps(obj, fn?: (key, value) => any): any {
713
+ let op = Object.getOwnPropertyNames(obj);
714
+ if (typeof obj === 'undefined') return undefined;
715
+ let isArray = Array.isArray(obj);
716
+ let o = isArray ? [] : {};
717
+ for (let i in op) {
718
+ let prop = op[i];
719
+ if (typeof obj[prop] === 'undefined' || typeof obj[prop] === 'function') continue;
720
+ let v = typeof fn === 'function' ? fn(prop, obj[prop]) : obj[prop];
721
+ if (typeof v === 'undefined') continue;
722
+ if (util.types.isBoxedPrimitive(v))
723
+ o[prop] = v.valueOf();
724
+ if (Array.isArray(v) || typeof v === 'object')
725
+ o[prop] = utils.replaceProps(v, fn);
726
+ else
727
+ o[prop] = v;
728
+ }
729
+ return o;
730
+ }
731
+ public findLineByLeastSquares(values_x: number[], values_y: number[]): number[][] {
732
+ var x_sum = 0;
733
+ var y_sum = 0;
734
+ var xy_sum = 0;
735
+ var xx_sum = 0;
736
+ var count = 0;
737
+
738
+ /*
739
+ * The above is just for quick access, makes the program faster
740
+ */
741
+ var x = 0;
742
+ var y = 0;
743
+ var values_length = values_x.length;
744
+
745
+ if (values_length != values_y.length) {
746
+ throw new Error('The parameters values_x and values_y need to have same size!');
747
+ }
748
+
749
+ /*
750
+ * Above and below cover edge cases
751
+ */
752
+ if (values_length === 0) {
753
+ return [[], []];
754
+ }
755
+
756
+ /*
757
+ * Calculate the sum for each of the parts necessary.
758
+ */
759
+ for (let i = 0; i < values_length; i++) {
760
+ x = values_x[i];
761
+ y = values_y[i];
762
+ x_sum += x;
763
+ y_sum += y;
764
+ xx_sum += x * x;
765
+ xy_sum += x * y;
766
+ count++;
767
+ }
768
+
769
+ /*
770
+ * Calculate m and b for the line equation:
771
+ * y = x * m + b
772
+ */
773
+ var m = (count * xy_sum - x_sum * y_sum) / (count * xx_sum - x_sum * x_sum);
774
+ var b = (y_sum / count) - (m * x_sum) / count;
775
+
776
+ /*
777
+ * We then return the x and y data points according to our fit
778
+ */
779
+ var result_values_x: number[] = [];
780
+ var result_values_y: number[] = [];
781
+
782
+ for (let i = 0; i < values_length; i++) {
783
+ x = values_x[i];
784
+ y = x * m + b;
785
+ result_values_x.push(x);
786
+ result_values_y.push(y);
787
+ }
788
+
789
+ return [result_values_x, result_values_y];
790
+ }
791
+ public slopeOfLeastSquares(values_x: number[], values_y: number[]): number {
792
+ let points = utils.findLineByLeastSquares(values_x, values_y);
793
+ let points_x = points[0];
794
+ let points_y = points[1];
795
+ let slope = (points_y[0] - points_y[points_y.length - 1]) / (points_x[0] - points_x[points_x.length - 1]);
796
+ return slope;
797
+ }
798
+ private random(bounds: number, onlyPositive: boolean = false) {
799
+ let rand = Math.random() * bounds;
800
+ if (!onlyPositive) {
801
+ if (Math.random() <= .5) rand = rand * -1;
802
+ }
803
+ return rand;
804
+ }
805
+ public dec2bin(dec) {
806
+ return (dec >>> 0).toString(2).padStart(8, '0');
807
+ }
808
+ }
809
+
806
810
  export const utils = new Utils();