nodejs-poolcontroller 7.5.1 → 7.7.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 (64) hide show
  1. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -0
  2. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -0
  3. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  5. package/Changelog +19 -0
  6. package/Dockerfile +3 -3
  7. package/README.md +13 -8
  8. package/app.ts +1 -1
  9. package/config/Config.ts +38 -2
  10. package/config/VersionCheck.ts +27 -12
  11. package/controller/Constants.ts +2 -1
  12. package/controller/Equipment.ts +193 -9
  13. package/controller/Errors.ts +10 -0
  14. package/controller/Lockouts.ts +503 -0
  15. package/controller/State.ts +269 -64
  16. package/controller/boards/AquaLinkBoard.ts +1000 -0
  17. package/controller/boards/BoardFactory.ts +4 -0
  18. package/controller/boards/EasyTouchBoard.ts +468 -144
  19. package/controller/boards/IntelliCenterBoard.ts +466 -307
  20. package/controller/boards/IntelliTouchBoard.ts +37 -5
  21. package/controller/boards/NixieBoard.ts +671 -141
  22. package/controller/boards/SystemBoard.ts +1397 -641
  23. package/controller/comms/Comms.ts +462 -362
  24. package/controller/comms/messages/Messages.ts +174 -30
  25. package/controller/comms/messages/config/ChlorinatorMessage.ts +6 -3
  26. package/controller/comms/messages/config/CircuitMessage.ts +1 -0
  27. package/controller/comms/messages/config/ExternalMessage.ts +10 -8
  28. package/controller/comms/messages/config/HeaterMessage.ts +141 -29
  29. package/controller/comms/messages/config/OptionsMessage.ts +9 -2
  30. package/controller/comms/messages/config/PumpMessage.ts +53 -35
  31. package/controller/comms/messages/config/ScheduleMessage.ts +33 -25
  32. package/controller/comms/messages/config/ValveMessage.ts +2 -2
  33. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +38 -86
  34. package/controller/comms/messages/status/EquipmentStateMessage.ts +59 -23
  35. package/controller/comms/messages/status/HeaterStateMessage.ts +57 -3
  36. package/controller/comms/messages/status/IntelliChemStateMessage.ts +56 -8
  37. package/controller/comms/messages/status/PumpStateMessage.ts +23 -1
  38. package/controller/nixie/Nixie.ts +1 -1
  39. package/controller/nixie/bodies/Body.ts +3 -0
  40. package/controller/nixie/chemistry/ChemController.ts +164 -51
  41. package/controller/nixie/chemistry/Chlorinator.ts +137 -88
  42. package/controller/nixie/circuits/Circuit.ts +51 -19
  43. package/controller/nixie/heaters/Heater.ts +241 -31
  44. package/controller/nixie/pumps/Pump.ts +488 -206
  45. package/controller/nixie/schedules/Schedule.ts +91 -35
  46. package/controller/nixie/valves/Valve.ts +1 -1
  47. package/defaultConfig.json +20 -0
  48. package/package.json +21 -21
  49. package/web/Server.ts +94 -49
  50. package/web/bindings/aqualinkD.json +505 -0
  51. package/web/bindings/influxDB.json +71 -1
  52. package/web/bindings/mqtt.json +98 -39
  53. package/web/bindings/mqttAlt.json +59 -1
  54. package/web/interfaces/baseInterface.ts +1 -0
  55. package/web/interfaces/httpInterface.ts +23 -2
  56. package/web/interfaces/influxInterface.ts +45 -10
  57. package/web/interfaces/mqttInterface.ts +114 -54
  58. package/web/services/config/Config.ts +55 -132
  59. package/web/services/state/State.ts +81 -4
  60. package/web/services/state/StateSocket.ts +4 -4
  61. package/web/services/utilities/Utilities.ts +8 -6
  62. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -52
  63. package/config copy.json +0 -300
  64. package/issue_template.md +0 -52
@@ -17,14 +17,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
  import * as extend from 'extend';
18
18
  import { EventEmitter } from 'events';
19
19
  import { ncp } from "../nixie/Nixie";
20
+ import { NixieHeaterBase } from "../nixie/heaters/Heater";
20
21
  import { utils, Heliotrope, Timestamp } from '../Constants';
21
22
  import {SystemBoard, byteValueMap, ConfigQueue, ConfigRequest, BodyCommands, FilterCommands, PumpCommands, SystemCommands, CircuitCommands, FeatureCommands, ValveCommands, HeaterCommands, ChlorinatorCommands, ChemControllerCommands, EquipmentIdRange} from './SystemBoard';
22
23
  import { logger } from '../../logger/Logger';
23
- import { state, ChlorinatorState, ChemControllerState, TemperatureState, VirtualCircuitState, ICircuitState, ICircuitGroupState, LightGroupState, ValveState, FilterState } from '../State';
24
+ import { state, ChlorinatorState, ChemControllerState, TemperatureState, VirtualCircuitState, CircuitState, ICircuitState, ICircuitGroupState, LightGroupState, ValveState, FilterState, BodyTempState, FeatureState } from '../State';
24
25
  import { sys, Equipment, Options, Owner, Location, CircuitCollection, TempSensorCollection, General, PoolSystem, Body, Pump, CircuitGroupCircuit, CircuitGroup, ChemController, Circuit, Feature, Valve, ICircuit, Heater, LightGroup, LightGroupCircuit, ControllerType, Filter } from '../Equipment';
25
26
  import { Protocol, Outbound, Message, Response } from '../comms/messages/Messages';
26
- import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, ParameterOutOfRangeError } from '../Errors';
27
- import {conn} from '../comms/Comms';
27
+ import { BoardProcessError, EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, InvalidOperationError, ParameterOutOfRangeError } from '../Errors';
28
+ import { conn } from '../comms/Comms';
29
+ import { delayMgr } from '../Lockouts';
28
30
  export class NixieBoard extends SystemBoard {
29
31
  constructor (system: PoolSystem){
30
32
  super(system);
@@ -36,30 +38,44 @@ export class NixieBoard extends SystemBoard {
36
38
  this.equipmentIds.features.start = 129;
37
39
  this.equipmentIds.circuitGroups.start = 193;
38
40
  this.equipmentIds.virtualCircuits.start = 237;
41
+ this.valueMaps.equipmentMaster = new byteValueMap([
42
+ [1, { val: 1, name: 'ncp', desc: 'Nixie Control Panel' }],
43
+ [2, { val: 2, name: 'ext', desc: 'External Control Panel'}]
44
+ ]);
45
+
46
+ this.valueMaps.featureFunctions = new byteValueMap([
47
+ [0, { name: 'generic', desc: 'Generic' }],
48
+ [1, { name: 'spillway', desc: 'Spillway' }],
49
+ [2, { name: 'spadrain', desc: 'Spa Drain' }]
50
+ ]);
39
51
  this.valueMaps.circuitFunctions = new byteValueMap([
40
52
  [0, { name: 'generic', desc: 'Generic' }],
41
53
  [1, { name: 'spillway', desc: 'Spillway' }],
42
- [2, { name: 'mastercleaner', desc: 'Master Cleaner' }],
54
+ [2, { name: 'mastercleaner', desc: 'Master Cleaner', body: 1 }],
43
55
  [3, { name: 'chemrelay', desc: 'Chem Relay' }],
44
56
  [4, { name: 'light', desc: 'Light', isLight: true }],
45
- [5, { name: 'intellibrite', desc: 'Intellibrite', isLight: true }],
46
- [6, { name: 'globrite', desc: 'GloBrite', isLight: true }],
57
+ [5, { name: 'intellibrite', desc: 'Intellibrite', isLight: true, theme: 'intellibrite' }],
58
+ [6, { name: 'globrite', desc: 'GloBrite', isLight: true, theme: 'intellibrite' }],
47
59
  [7, { name: 'globritewhite', desc: 'GloBrite White', isLight: true }],
48
- [8, { name: 'magicstream', desc: 'Magicstream', isLight: true }],
60
+ [8, { name: 'magicstream', desc: 'Magicstream', isLight: true, theme: 'magicstream' }],
49
61
  [9, { name: 'dimmer', desc: 'Dimmer', isLight: true }],
50
- [10, { name: 'colorcascade', desc: 'ColorCascade', isLight: true }],
51
- [11, { name: 'mastercleaner2', desc: 'Master Cleaner 2' }],
52
- [12, { name: 'pool', desc: 'Pool', hasHeatSource: true }],
53
- [13, { name: 'spa', desc: 'Spa', hasHeatSource: true }],
54
- [14, { name: 'colorlogic', desc: 'ColorLogic', isLight:true }]
62
+ [10, { name: 'colorcascade', desc: 'ColorCascade', isLight: true, theme: 'intellibrite' }],
63
+ [11, { name: 'mastercleaner2', desc: 'Master Cleaner 2', body: 2 }],
64
+ [12, { name: 'pool', desc: 'Pool', hasHeatSource: true, body: 1 }],
65
+ [13, { name: 'spa', desc: 'Spa', hasHeatSource: true, body: 2 }],
66
+ [14, { name: 'colorlogic', desc: 'ColorLogic', isLight: true, theme: 'colorlogic' }],
67
+ [15, { name: 'spadrain', desc: 'Spa Drain' }],
68
+ [16, { name: 'pooltone', desc: 'Pool Tone', isLight: true, theme: 'pooltone' }],
55
69
  ]);
56
70
  this.valueMaps.pumpTypes = new byteValueMap([
57
- [1, { name: 'ss', desc: 'Single Speed', maxCircuits: 0, hasAddress: false, hasBody: true, maxRelays: 1 }],
58
- [2, { name: 'ds', desc: 'Two Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 2 }],
71
+ [1, { name: 'ss', desc: 'Single Speed', maxCircuits: 0, hasAddress: false, hasBody: true, maxRelays: 1, relays: [{ id: 1, name: 'Pump On/Off' }]}],
72
+ [2, { name: 'ds', desc: 'Two Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 2, relays: [{ id: 1, name: 'Low Speed' }, { id: 2, name: 'High Speed' }]}],
59
73
  [3, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true }],
60
74
  [4, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
61
75
  [5, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
62
- [100, { name: 'sf', desc: 'SuperFlo VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1 }]
76
+ [6, { name: 'hwvs', desc: 'Hayward Eco/TriStar VS', minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true }],
77
+ [7, { name: 'hwrly', desc: 'Hayward Relay VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, maxSpeeds: 8, relays: [{ id: 1, name: 'Step #1' }, { id: 2, name: 'Step #2'}, { id: 3, name: 'Step #3' }, { id: 4, name: 'Pump On' }] }],
78
+ [100, { name: 'sf', desc: 'SuperFlo VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1, maxSpeeds: 4, relays: [{ id: 1, name: 'Program #1' }, { id: 2, name: 'Program #2' }, { id: 3, name: 'Program #3' }, { id: 4, name: 'Program #4' }]}]
63
79
  ]);
64
80
  // RSG - same as systemBoard definition; can delete.
65
81
  this.valueMaps.heatModes = new byteValueMap([
@@ -132,10 +148,14 @@ export class NixieBoard extends SystemBoard {
132
148
  [245, { name: 'spaHeater', desc: 'Spa Heater' }],
133
149
  [246, { name: 'freeze', desc: 'Freeze' }],
134
150
  [247, { name: 'poolSpa', desc: 'Pool/Spa' }],
135
- [248, { name: 'solarHeat', desc: 'Solar Heat' }],
136
151
  [251, { name: 'heater', desc: 'Heater' }],
137
152
  [252, { name: 'solar', desc: 'Solar' }],
138
- [255, { name: 'poolHeatEnable', desc: 'Pool Heat Enable' }]
153
+ [253, { name: 'solar1', desc: 'Solar Body 1' }],
154
+ [254, { name: 'solar2', desc: 'Solar Body 2' }],
155
+ [255, { name: 'solar3', desc: 'Solar Body 3' }],
156
+ [256, { name: 'solar4', desc: 'Solar Body 4' }],
157
+ [257, { name: 'poolHeatEnable', desc: 'Pool Heat Enable' }],
158
+ [258, { name: 'anyHeater', desc: 'Any Heater' }]
139
159
  ]);
140
160
  this.valueMaps.scheduleTimeTypes.merge([
141
161
  [1, { name: 'sunrise', desc: 'Sunrise' }],
@@ -144,36 +164,52 @@ export class NixieBoard extends SystemBoard {
144
164
 
145
165
  this.valueMaps.lightThemes = new byteValueMap([
146
166
  // IntelliBrite Themes
147
- [0, { name: 'white', desc: 'White', type: 'intellibrite', sequence: 11 }],
148
- [1, { name: 'green', desc: 'Green', type: 'intellibrite', sequence: 9 }],
149
- [2, { name: 'blue', desc: 'Blue', type: 'intellibrite', sequence: 8 }],
150
- [3, { name: 'magenta', desc: 'Magenta', type: 'intellibrite', sequence: 12 }],
151
- [4, { name: 'red', desc: 'Red', type: 'intellibrite', sequence: 10 }],
152
- [5, { name: 'sam', desc: 'SAm Mode', type: 'intellibrite', sequence: 1 }],
153
- [6, { name: 'party', desc: 'Party', type: 'intellibrite', sequence: 2 }],
154
- [7, { name: 'romance', desc: 'Romance', type: 'intellibrite', sequence: 3 }],
155
- [8, { name: 'caribbean', desc: 'Caribbean', type: 'intellibrite', sequence: 4 }],
156
- [9, { name: 'american', desc: 'American', type: 'intellibrite', sequence: 5 }],
157
- [10, { name: 'sunset', desc: 'Sunset', type: 'intellibrite', sequence: 6 }],
158
- [11, { name: 'royal', desc: 'Royal', type: 'intellibrite', sequence: 7 }],
167
+ [0, { name: 'white', desc: 'White', types: ['intellibrite', 'magicstream'], sequence: 11 }],
168
+ [1, { name: 'green', desc: 'Green', types: ['intellibrite', 'magicstream'], sequence: 9 }],
169
+ [2, { name: 'blue', desc: 'Blue', types: ['intellibrite', 'magicstream'], sequence: 8 }],
170
+ [3, { name: 'magenta', desc: 'Magenta', types: ['intellibrite', 'magicstream'], sequence: 12 }],
171
+ [4, { name: 'red', desc: 'Red', types: ['intellibrite', 'magicstream'], sequence: 10 }],
172
+ [5, { name: 'sam', desc: 'SAm Mode', types: ['intellibrite', 'magicstream'], sequence: 1 }],
173
+ [6, { name: 'party', desc: 'Party', types: ['intellibrite', 'magicstream'], sequence: 2 }],
174
+ [7, { name: 'romance', desc: 'Romance', types: ['intellibrite', 'magicstream'], sequence: 3 }],
175
+ [8, { name: 'caribbean', desc: 'Caribbean', types: ['intellibrite', 'magicstream'], sequence: 4 }],
176
+ [9, { name: 'american', desc: 'American', types: ['intellibrite', 'magicstream'], sequence: 5 }],
177
+ [10, { name: 'sunset', desc: 'Sunset', types: ['intellibrite', 'magicstream'], sequence: 6 }],
178
+ [11, { name: 'royal', desc: 'Royal', types: ['intellibrite', 'magicstream'], sequence: 7 }],
159
179
  // ColorLogic Themes
160
- [20, { name: 'cloudwhite', desc: 'Cloud White', type: 'colorlogic', sequence: 7 }],
161
- [21, { name: 'deepsea', desc: 'Deep Sea', type: 'colorlogic', sequence: 2 }],
162
- [22, { name: 'royalblue', desc: 'Royal Blue', type: 'colorlogic', sequence: 3 }],
163
- [23, { name: 'afternoonskies', desc: 'Afternoon Skies', type: 'colorlogic', sequence: 4 }],
164
- [24, { name: 'aquagreen', desc: 'Aqua Green', type: 'colorlogic', sequence: 5 }],
165
- [25, { name: 'emerald', desc: 'Emerald', type: 'colorlogic', sequence: 6 }],
166
- [26, { name: 'warmred', desc: 'Warm Red', type: 'colorlogic', sequence: 8 }],
167
- [27, { name: 'flamingo', desc: 'Flamingo', type: 'colorlogic', sequence: 9 }],
168
- [28, { name: 'vividviolet', desc: 'Vivid Violet', type: 'colorlogic', sequence: 10 }],
169
- [29, { name: 'sangria', desc: 'Sangria', type: 'colorlogic', sequence: 11 }],
170
- [30, { name: 'twilight', desc: 'Twilight', type: 'colorlogic', sequence: 12 }],
171
- [31, { name: 'tranquility', desc: 'Tranquility', type: 'colorlogic', sequence: 13 }],
172
- [32, { name: 'gemstone', desc: 'Gemstone', type: 'colorlogic', sequence: 14 }],
173
- [33, { name: 'usa', desc: 'USA', type: 'colorlogic', sequence: 15 }],
174
- [34, { name: 'mardigras', desc: 'Mardi Gras', type: 'colorlogic', sequence: 16 }],
175
- [35, { name: 'coolcabaret', desc: 'Cabaret', type: 'colorlogic', sequence: 17 }],
176
-
180
+ [20, { name: 'cloudwhite', desc: 'Cloud White', types: ['colorlogic'], sequence: 7 }],
181
+ [21, { name: 'deepsea', desc: 'Deep Sea', types: ['colorlogic'], sequence: 2 }],
182
+ [22, { name: 'royalblue', desc: 'Royal Blue', types: ['colorlogic'], sequence: 3 }],
183
+ [23, { name: 'afternoonskies', desc: 'Afternoon Skies', types: ['colorlogic'], sequence: 4 }],
184
+ [24, { name: 'aquagreen', desc: 'Aqua Green', types: ['colorlogic'], sequence: 5 }],
185
+ [25, { name: 'emerald', desc: 'Emerald', types: ['colorlogic'], sequence: 6 }],
186
+ [26, { name: 'warmred', desc: 'Warm Red', types: ['colorlogic'], sequence: 8 }],
187
+ [27, { name: 'flamingo', desc: 'Flamingo', types: ['colorlogic'], sequence: 9 }],
188
+ [28, { name: 'vividviolet', desc: 'Vivid Violet', types: ['colorlogic'], sequence: 10 }],
189
+ [29, { name: 'sangria', desc: 'Sangria', types: ['colorlogic'], sequence: 11 }],
190
+ [30, { name: 'twilight', desc: 'Twilight', types: ['colorlogic'], sequence: 12 }],
191
+ [31, { name: 'tranquility', desc: 'Tranquility', types: ['colorlogic'], sequence: 13 }],
192
+ [32, { name: 'gemstone', desc: 'Gemstone', types: ['colorlogic'], sequence: 14 }],
193
+ [33, { name: 'usa', desc: 'USA', types: ['colorlogic'], sequence: 15 }],
194
+ [34, { name: 'mardigras', desc: 'Mardi Gras', types: ['colorlogic'], sequence: 16 }],
195
+ [35, { name: 'coolcabaret', desc: 'Cabaret', types: ['colorlogic'], sequence: 17 }],
196
+ // Sunseeker PoolTone Themes
197
+ [36, { name: 'eveningsea', desc: 'Evening Sea', types: ['pooltone'], sequence: 1 }],
198
+ [37, { name: 'eveningrivers', desc: 'Evening Rivers', types: ['pooltone'], sequence: 2 }],
199
+ [38, { name: 'riviera', desc: 'Riviera', types: ['pooltone'], sequence: 3 }],
200
+ [39, { name: 'neutralwhite', desc: 'Neutral White', types: ['pooltone'], sequence: 4 }],
201
+ [40, { name: 'rainbow', desc: 'Rainbow', types: ['pooltone'], sequence: 5 }],
202
+ [41, { name: 'colorriver', desc: 'Color River', types: ['pooltone'], sequence: 6 }],
203
+ [42, { name: 'disco', desc: 'Disco', types: ['pooltone'], sequence: 7 }],
204
+ [43, { name: 'fourseasons', desc: 'Four Seasons', types: ['pooltone'], sequence: 8 }],
205
+ [44, { name: 'Party', desc: 'Party', types: ['pooltone'], sequence: 9 }],
206
+ [45, { name: 'sunwhite', desc: 'Sun White', types: ['pooltone'], sequence: 10 }],
207
+ [46, { name: 'red', desc: 'Red', types: ['pooltone'], sequence: 11 }],
208
+ [47, { name: 'green', desc: 'Green', types: ['pooltone'], sequence: 12 }],
209
+ [48, { name: 'blue', desc: 'Blue', types: ['pooltone'], sequence: 13 }],
210
+ [49, { name: 'greenblue', desc: 'Green-Blue', types: ['pooltone'], sequence: 14 }],
211
+ [50, { name: 'redgreen', desc: 'Red-Green', types: ['pooltone'], sequence: 15 }],
212
+ [51, { name: 'bluered', desc: 'Blue-red', types: ['pooltone'], sequence: 16 }],
177
213
  [255, { name: 'none', desc: 'None' }]
178
214
  ]);
179
215
  this.valueMaps.lightColors = new byteValueMap([
@@ -202,8 +238,10 @@ export class NixieBoard extends SystemBoard {
202
238
  [1, { name: 'heater', desc: 'Heater' }],
203
239
  [2, { name: 'solar', desc: 'Solar' }],
204
240
  [3, { name: 'cooling', desc: 'Cooling' }],
241
+ [6, { name: 'mtheat', desc: 'Heater' }],
205
242
  [4, { name: 'hpheat', desc: 'Heating' }],
206
- [8, { name: 'hpcool', desc: 'Cooling' }]
243
+ [8, { name: 'hpcool', desc: 'Cooling' }],
244
+ [128, {name: 'cooldown', desc: 'Cooldown'}]
207
245
  ]);
208
246
  this.valueMaps.scheduleTypes = new byteValueMap([
209
247
  [0, { name: 'runonce', desc: 'Run Once', startDate: true, startTime: true, endTime: true, days: false, heatSource: true, heatSetpoint: true }],
@@ -429,7 +467,7 @@ export class NixieBoard extends SystemBoard {
429
467
  //public chlorinator: NixieChlorinatorCommands = new NixieChlorinatorCommands(this);
430
468
  public bodies: NixieBodyCommands = new NixieBodyCommands(this);
431
469
  public filters: NixieFilterCommands = new NixieFilterCommands(this);
432
- //public pumps: NixiePumpCommands = new NixiePumpCommands(this);
470
+ public pumps: NixiePumpCommands = new NixiePumpCommands(this);
433
471
  //public schedules: NixieScheduleCommands = new NixieScheduleCommands(this);
434
472
  public heaters: NixieHeaterCommands = new NixieHeaterCommands(this);
435
473
  public valves: NixieValveCommands = new NixieValveCommands(this);
@@ -456,12 +494,24 @@ export class NixieFilterCommands extends FilterCommands {
456
494
  try {
457
495
  await ncp.filters.setFilterStateAsync(fstate, isOn);
458
496
  }
459
- catch (err) { return Promise.reject(`Nixie: Error setFiterStateAsync ${err.message}`); }
497
+ catch (err) { return Promise.reject(new BoardProcessError(`Nixie: Error setFiterStateAsync ${err.message}`, 'setFilterStateAsync')); }
460
498
  }
461
499
  }
462
-
463
500
  export class NixieSystemCommands extends SystemCommands {
464
- public cancelDelay(): Promise<any> { state.delay = sys.board.valueMaps.delay.getValue('nodelay'); return Promise.resolve(state.data.delay); }
501
+ public cancelDelay(): Promise<any> {
502
+ delayMgr.cancelPumpValveDelays();
503
+ delayMgr.cancelHeaterCooldownDelays();
504
+ delayMgr.cancelHeaterStartupDelays();
505
+ delayMgr.cancelCleanerStartDelays();
506
+ delayMgr.cancelManualPriorityDelays();
507
+ state.delay = sys.board.valueMaps.delay.getValue('nodelay');
508
+ return Promise.resolve(state.data.delay);
509
+ }
510
+ public setManualOperationPriority(id: number): Promise<any> {
511
+ let cstate = state.circuits.getInterfaceById(id);
512
+ delayMgr.setManualPriorityDelay(cstate);
513
+ return Promise.resolve(cstate);
514
+ }
465
515
  public setDateTimeAsync(obj: any): Promise<any> { return Promise.resolve(); }
466
516
  public getDOW() { return this.board.valueMaps.scheduleDays.toArray(); }
467
517
  public async setGeneralAsync(obj: any): Promise<General> {
@@ -480,7 +530,20 @@ export class NixieSystemCommands extends SystemCommands {
480
530
  }
481
531
  }
482
532
  export class NixieCircuitCommands extends CircuitCommands {
483
- public async setCircuitStateAsync(id: number, val: boolean): Promise<ICircuitState> {
533
+ // This is our poll loop for circuit relay states.
534
+ public async syncCircuitRelayStates() {
535
+ try {
536
+ for (let i = 0; i < sys.circuits.length; i++) {
537
+ // Run through all the controlled circuits to see whether they should be triggered or not.
538
+ let circ = sys.circuits.getItemByIndex(i);
539
+ if (circ.master === 1 && circ.isActive) {
540
+ let cstate = state.circuits.getItemById(circ.id);
541
+ if (cstate.isOn) await ncp.circuits.setCircuitStateAsync(cstate, cstate.isOn);
542
+ }
543
+ }
544
+ } catch (err) { logger.error(`syncCircuitRelayStates: Error synchronizing circuit relays ${err.message}`); }
545
+ }
546
+ public async setCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
484
547
  sys.board.suspendStatus(true);
485
548
  try {
486
549
  // We need to do some routing here as it is now critical that circuits, groups, and features
@@ -490,48 +553,340 @@ export class NixieCircuitCommands extends CircuitCommands {
490
553
  else if (sys.board.equipmentIds.features.isInRange(id))
491
554
  return await sys.board.features.setFeatureStateAsync(id, val);
492
555
 
556
+
493
557
  let circuit: ICircuit = sys.circuits.getInterfaceById(id, false, { isActive: false });
494
558
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Circuit or Feature id ${id} not valid`, id, 'Circuit'));
495
559
  let circ = state.circuits.getInterfaceById(id, circuit.isActive !== false);
560
+ if (circ.stopDelay) {
561
+ // Send this off so that the relays are properly set. In the end we cannot change right now. If this
562
+ // happens to be a body circuit then the relay state will be skipped anyway.
563
+ await ncp.circuits.setCircuitStateAsync(circ, circ.isOn);
564
+ return circ;
565
+ }
496
566
  let newState = utils.makeBool(val);
497
- // First, if we are turning the circuit on, lets determine whether the circuit is a pool or spa circuit and if this is a shared system then we need
498
- // to turn off the other body first.
499
- //[12, { name: 'pool', desc: 'Pool', hasHeatSource: true }],
500
- //[13, { name: 'spa', desc: 'Spa', hasHeatSource: true }]
501
- if (newState && (circuit.type === 12 || circuit.type === 13)) {
567
+ let ctype = sys.board.valueMaps.circuitFunctions.getName(circ.type);
568
+ // Filter out any special circuit types.
569
+ switch (ctype) {
570
+ case 'pool':
571
+ case 'spa':
572
+ await this.setBodyCircuitStateAsync(id, newState, ignoreDelays);
573
+ break;
574
+ case 'mastercleaner':
575
+ case 'mastercleaner2':
576
+ await this.setCleanerCircuitStateAsync(id, newState, ignoreDelays);
577
+ break;
578
+ case 'spillway':
579
+ await this.setSpillwayCircuitStateAsync(id, newState, ignoreDelays);
580
+ break;
581
+ case 'spadrain':
582
+ await this.setDrainCircuitStateAsync(id, newState, ignoreDelays);
583
+ break;
584
+ default:
585
+ await ncp.circuits.setCircuitStateAsync(circ, newState);
586
+ await sys.board.processStatusAsync();
587
+ break;
588
+ }
589
+ // Let the main nixie controller set the circuit state and affect the relays if it needs to.
590
+ return state.circuits.getInterfaceById(circ.id);
591
+ }
592
+ catch (err) { logger.error(`Nixie: setCircuitState ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setCircuitStateAsync ${err.message}`, 'setCircuitState')); }
593
+ finally {
594
+ state.emitEquipmentChanges();
595
+ ncp.pumps.syncPumpStates();
596
+ sys.board.suspendStatus(false);
597
+ }
598
+ }
599
+ protected async setCleanerCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
600
+ try {
601
+ let cstate = state.circuits.getItemById(id);
602
+ let circuit = sys.circuits.getItemById(id);
603
+ // We know which body the cleaner belongs to by an attribute on the circuit function.
604
+ let ctype = sys.board.valueMaps.circuitFunctions.get(circuit.type);
605
+ let bstate = state.temps.bodies.getItemById(ctype.body || 1);
606
+ // Cleaner lockout should occur when
607
+ // 1. The body circuit is off.
608
+ // 2. The spillway mode is running.
609
+
610
+ // Optional modes include
611
+ // 1. The current body is heating with solar.
612
+
613
+ // Lockouts are cleared when
614
+ // 1. The above conditions are no longer true.
615
+ // 2. The user requests the circuit to be off.
616
+ if (!val) {
617
+ // We can always turn a cleaner circuit off. Even if a delay is underway.
618
+ delayMgr.clearCleanerStartDelays(bstate.id);
619
+ await ncp.circuits.setCircuitStateAsync(cstate, false);
620
+ }
621
+ else if (val) {
622
+ logger.info(`Setting cleaner circuit ${cstate.name} to ${val}`);
623
+ // Alright we are turning the cleaner on.
624
+ // To turn on the cleaner circuit we must first ensure the body is on. If it is not then we abort.
625
+ if (!bstate.isOn) {
626
+ logger.info(`Cannot turn on cleaner circuit ${cstate.name}. ${bstate.name} is not running`);
627
+ await ncp.circuits.setCircuitStateAsync(cstate, false);
628
+ return cstate;
629
+ }
630
+ // If there is a drain circuit going shut that thing off.
631
+ await this.turnOffDrainCircuits(ignoreDelays);
632
+ // If solar is currently on and the cleaner solar delay is set then we need to calculate a delay
633
+ // to turn on the cleaner.
634
+ let delayTime = 0;
635
+ let dtNow = new Date().getTime();
636
+ if (typeof ignoreDelays === 'undefined' || !ignoreDelays) {
637
+ if (sys.general.options.cleanerSolarDelay && sys.general.options.cleanerSolarDelayTime > 0) {
638
+ let circBody = state.circuits.getItemById(bstate.circuit);
639
+ // If the body has not been on or the solar heater has not been on long enough then we need to delay the startup.
640
+ if (sys.board.valueMaps.heatStatus.getName(bstate.heatStatus) === 'solar') {
641
+ // Check for the solar delay. We need to know when the heater first kicked in. A cleaner and solar
642
+ // heater can run at the same time but the heater must be on long enough for the timer to expire.
643
+
644
+ // The reasoning behind this is so that the booster pump can be assured that there is sufficient pressure
645
+ // for it to start and any air from the solar has had time to purge through the system.
646
+ let heaters = sys.heaters.getSolarHeaters(bstate.id);
647
+ let startTime = 0;
648
+ for (let i = 0; i < heaters.length; i++) {
649
+ let heater = heaters.getItemByIndex(i);
650
+ let hstate = state.heaters.getItemById(heater.id);
651
+ startTime = Math.max(startTime, hstate.startTime.getTime());
652
+ }
653
+ // Lets see if we have a solar start delay.
654
+ delayTime = Math.max(Math.round(((sys.general.options.cleanerSolarDelayTime * 1000) - (dtNow - startTime))) / 1000, delayTime);
655
+ }
656
+ }
657
+ if (sys.general.options.cleanerStartDelay && sys.general.options.cleanerStartDelayTime) {
658
+ let bcstate = state.circuits.getItemById(bstate.circuit);
659
+ // So we should be started. Lets determine whethere there should be any delay.
660
+ delayTime = Math.max(Math.round(((sys.general.options.cleanerStartDelayTime * 1000) - (dtNow - bcstate.startTime.getTime())) / 1000), delayTime);
661
+ logger.info(`Cleaner delay time calculated to ${delayTime}`);
662
+ }
663
+ }
664
+ if (delayTime > 5) delayMgr.setCleanerStartDelay(cstate, bstate.id, delayTime);
665
+ else await ncp.circuits.setCircuitStateAsync(cstate, true);
666
+ }
667
+ return cstate;
668
+ } catch (err) { return Promise.reject(new BoardProcessError(`Nixie: Error setting cleaner circuit state: ${err.message}`, 'setCleanerCircuitStateAsync')); }
669
+ }
670
+ protected async setBodyCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<CircuitState> {
671
+ try {
672
+ let cstate = state.circuits.getItemById(id);
673
+ let circuit = sys.circuits.getItemById(id);
674
+ let bstate = state.temps.bodies.getBodyByCircuitId(id);
675
+ if (val) {
676
+ // We are turning on a body circuit.
677
+ logger.verbose(`Turning on a body circuit ${bstate.name}`);
502
678
  if (sys.equipment.shared === true) {
679
+ // If we are turning on and this is a shared system it means that we need to turn off
680
+ // the other circuit.
681
+ let delayPumps = false;
682
+ await this.turnOffDrainCircuits(ignoreDelays);
683
+ if (bstate.id === 2) await this.turnOffSpillwayCircuits();
684
+ if (sys.general.options.pumpDelay === true && ignoreDelays !== true) {
685
+ // Now that this is off check the valve positions. If they are not currently in the correct position we need to delay any attached pump
686
+ // so that it does not come on while the valve is rotating. Default 30 seconds.
687
+ let iValves = sys.valves.getIntake();
688
+ for (let i = 0; i < iValves.length && !delayPumps; i++) {
689
+ let vstate = state.valves.getItemById(iValves[i].id);
690
+ if (vstate.isDiverted === true && circuit.type === 12) delayPumps = true;
691
+ else if (vstate.isDiverted === false && circuit.type === 13) delayPumps = true;
692
+ }
693
+ if (!delayPumps) {
694
+ let rValves = sys.valves.getReturn();
695
+ for (let i = 0; i < rValves.length && !delayPumps; i++) {
696
+ let vstate = state.valves.getItemById(rValves[i].id);
697
+ if (vstate.isDiverted === true && circuit.type === 12) delayPumps = true;
698
+ else if (vstate.isDiverted === false && circuit.type === 13) delayPumps = true;
699
+ }
700
+ }
701
+ }
503
702
  // If we are shared we need to turn off the other circuit.
504
- let offType = circ.type === 12 ? 13 : 12;
703
+ let offType = circuit.type === 12 ? 13 : 12;
505
704
  let off = sys.circuits.get().filter(elem => elem.type === offType);
705
+ let delayCooldown = false;
506
706
  // Turn the circuits off that are part of the shared system. We are going back to the board
507
707
  // just in case we got here for a circuit that isn't on the current defined panel.
508
708
  for (let i = 0; i < off.length; i++) {
509
709
  let coff = off[i];
510
- logger.info(`Turning off shared body ${coff.name} circuit`);
511
- await sys.board.circuits.setCircuitStateAsync(coff.id, false);
710
+ let bsoff = state.temps.bodies.getBodyByCircuitId(coff.id);
711
+ let csoff = state.circuits.getItemById(coff.id);
712
+ // Ensure the cleaner circuits for this body are off.
713
+ await this.turnOffCleanerCircuits(bsoff);
714
+ if (csoff.isOn) {
715
+ logger.verbose(`Turning off shared body ${coff.name} circuit`);
716
+ delayMgr.clearBodyStartupDelay(bsoff);
717
+ if (bsoff.heaterCooldownDelay && ignoreDelays !== true) {
718
+ // In this condition we are requesting that the shared body start when the cooldown delay
719
+ // has finished. This will add this request to the cooldown delay code. The setHeaterCooldownDelay
720
+ // code is expected to be re-entrant and checks the id so that it does not clear
721
+ // the original request if it is asked for again.
722
+
723
+ // NOTE: There is room for improvement here. For instance, if the result
724
+ // of turning on the circuit is that the heater(s) requiring cooldown will result in being on
725
+ // then why not cancel the current cooldown cycle and let the user get on with it.
726
+ // Consider:
727
+ // 1. Check each heater attached to the off body to see if it is also attached to the on body.
728
+ // 2. If the heater is attached check to see if there is any cooldown time left on it.
729
+ // 3. If the above conditions are true cancel the cooldown cycle.
730
+ logger.verbose(`${bsoff.name} is already in Cooldown mode`);
731
+ delayMgr.setHeaterCooldownDelay(bsoff, bstate);
732
+ delayCooldown = true;
733
+ }
734
+ else {
735
+ // We need to deal with heater cooldown delays here since you cannot turn off the body while the heater is
736
+ // cooling down. This means we need to check to see if the heater requires cooldown then set a delay for it
737
+ // if it does. The delay manager will shut the body off and start the new body when it is done.
738
+ let heaters = sys.board.heaters.getHeatersByCircuitId(circuit.id);
739
+ let cooldownTime = 0;
740
+ if (ignoreDelays !== true) {
741
+ for (let j = 0; j < heaters.length; j++) {
742
+ let nheater = ncp.heaters.find(x => x.id === heaters[j].id) as NixieHeaterBase;
743
+ cooldownTime = Math.max(nheater.getCooldownTime(), cooldownTime);
744
+ }
745
+ }
746
+ if (cooldownTime > 0) {
747
+ // We need do start a cooldown cycle for the body. If there is already
748
+ // a cooldown underway this will append the on to it.
749
+ delayMgr.setHeaterCooldownDelay(bsoff, bstate, cooldownTime * 1000);
750
+ delayCooldown = true;
751
+ }
752
+ else {
753
+ await ncp.circuits.setCircuitStateAsync(csoff, false);
754
+ bsoff.isOn = false;
755
+ }
756
+ }
757
+ }
758
+ }
759
+ if (delayCooldown) return cstate;
760
+ if (delayPumps === true) sys.board.pumps.setPumpValveDelays([id, bstate.circuit]);
761
+ }
762
+ // Now we need to set the startup delay for all the heaters. This is true whether
763
+ // the system is shared or not so lets get a list of all the associated heaters for the body in question.
764
+ if (sys.general.options.heaterStartDelay && sys.general.options.heaterStartDelayTime > 0) {
765
+ let heaters = sys.board.heaters.getHeatersByCircuitId(circuit.id);
766
+ for (let j = 0; j < heaters.length; j++) {
767
+ let hstate = state.heaters.getItemById(heaters[j].id);
768
+ delayMgr.setHeaterStartupDelay(hstate);
512
769
  }
513
770
  }
514
- //sys.board.virtualChlorinatorController.start();
771
+ await ncp.circuits.setCircuitStateAsync(cstate, val);
772
+ bstate.isOn = val;
515
773
  }
516
- if (id === 6) state.temps.bodies.getItemById(1, true).isOn = val;
517
- else if (id === 1) state.temps.bodies.getItemById(2, true).isOn = val;
518
- // Let the main nixie controller set the circuit state and affect the relays if it needs to.
519
- await ncp.circuits.setCircuitStateAsync(circ, newState);
520
- await sys.board.processStatusAsync();
521
- return state.circuits.getInterfaceById(circ.id);
522
- }
523
- catch (err) { return Promise.reject(`Nixie: Error setCircuitStateAsync ${err.message}`); }
524
- finally {
525
- state.emitEquipmentChanges();
526
- ncp.pumps.syncPumpStates();
527
- sys.board.suspendStatus(false);
528
- }
774
+ else if (!val) {
775
+ // Alright we are turning off a circuit that will result in a body shutting off. If this
776
+ // circuit is already under delay it should have been processed out earlier.
777
+ delayMgr.cancelPumpValveDelays();
778
+ delayMgr.cancelHeaterStartupDelays();
779
+ if (cstate.startDelay) delayMgr.clearBodyStartupDelay(bstate);
780
+ await this.turnOffCleanerCircuits(bstate);
781
+ if (sys.equipment.shared && bstate.id === 2) await this.turnOffDrainCircuits(ignoreDelays);
782
+ logger.verbose(`Turning off a body circuit ${circuit.name}`);
783
+ if (cstate.isOn) {
784
+
785
+ // Check to see if we have any heater cooldown delays that need to take place.
786
+ let heaters = sys.board.heaters.getHeatersByCircuitId(circuit.id);
787
+ let cooldownTime = 0;
788
+ for (let j = 0; j < heaters.length; j++) {
789
+ let nheater = ncp.heaters.find(x => x.id === heaters[j].id) as NixieHeaterBase;
790
+ cooldownTime = Math.max(nheater.getCooldownTime(), cooldownTime);
791
+ }
792
+ if (cooldownTime > 0) {
793
+ logger.info(`Starting a Cooldown Delay ${cooldownTime}sec`);
794
+ // We need do start a cooldown cycle for the body.
795
+ delayMgr.setHeaterCooldownDelay(bstate, undefined, cooldownTime * 1000);
796
+ }
797
+ else {
798
+ await ncp.circuits.setCircuitStateAsync(cstate, val);
799
+ bstate.isOn = val;
800
+ }
801
+ }
802
+ }
803
+ return cstate;
804
+ } catch (err) { logger.error(`Nixie: Error setBodyCircuitStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setBodyCircuitStateAsync ${err.message}`, 'setBodyCircuitStateAsync')); }
529
805
  }
806
+ protected async setSpillwayCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<CircuitState> {
807
+ try {
808
+ let cstate = state.circuits.getItemById(id);
809
+ let delayPumps = false;
810
+ if (cstate.isOn !== val) {
811
+ if (sys.equipment.shared === true) {
812
+ // First we need to check to see if the pool is on.
813
+ if (val) {
814
+ let spastate = state.circuits.getItemById(1);
815
+ if (spastate.isOn) {
816
+ logger.warn(`Cannot turn ${cstate.name} on because ${spastate.name} is on`);
817
+ return cstate;
818
+ }
819
+ // If there are any drain circuits or features that are currently engaged we need to turn them off.
820
+ await this.turnOffDrainCircuits(ignoreDelays);
821
+ if (sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([6, id]);
822
+ }
823
+ else if (!val && !ignoreDelays) {
824
+ // If we are turning off and there is another circuit that ties to the same pumps then we need set a valve delay. This means
825
+ // that if the pool circuit is on then we need to delay the pumps. However, if there is no other circuit that needs
826
+ // the pump to be on, then no harm no foul a delay in the pump won't mean anything.
827
+
828
+ // Conditions where this should not delay.
829
+ // 1. Another spillway circuit or feature is on.
830
+ // 2. There is no other running circuit that will affect the intake or return.
831
+ let arrIds = sys.board.valves.getBodyValveCircuitIds(true);
832
+ if (arrIds.length > 1) {
833
+ if (sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) {
834
+ sys.board.pumps.setPumpValveDelays([6, id]);
835
+ }
836
+ }
837
+ }
838
+ }
839
+ }
840
+ logger.verbose(`Turning ${val ? 'on' : 'off'} a spillway circuit ${cstate.name}`);
841
+ await ncp.circuits.setCircuitStateAsync(cstate, val);
842
+ return cstate;
843
+ } catch (err) { logger.error(`Nixie: Error setSpillwayCircuitStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setSpillwayCircuitStateAsync ${err.message}`, 'setBodyCircuitStateAsync')); }
844
+ }
845
+ protected async setDrainCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<CircuitState> {
846
+ try {
847
+ // Drain circuits can be very bad. This is because they can be turned on then never turned off
848
+ // we may want to create some limits are to how long they can be on or even force them off
849
+ // if for instance the spa is not on.
850
+ // RULES FOR DRAIN CIRCUITS:
851
+ // 1. All spillway circuits must be off.
852
+ let cstate = state.circuits.getItemById(id);
853
+ let delayPumps = false;
854
+ if (cstate.isOn !== val) {
855
+ if (sys.equipment.shared === true) {
856
+ let spastate = state.temps.bodies.getItemById(2);
857
+ let poolstate = state.temps.bodies.getItemById(1);
858
+ // First we need to check to see if the pool is on.
859
+ if (val) {
860
+ if (spastate.isOn || spastate.startDelay || poolstate.isOn || poolstate.startDelay) {
861
+ logger.warn(`Cannot turn ${cstate.name} on because a body is on`);
862
+ return cstate;
863
+ }
864
+ // If there are any spillway circuits or features that are currently engaged we need to turn them off.
865
+ await this.turnOffSpillwayCircuits(true);
866
+ // If there are any cleaner circuits on for the main body turn them off.
867
+ await this.turnOffCleanerCircuits(state.temps.bodies.getItemById(1), true);
868
+ if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 1, 6]);
869
+ }
870
+ else if (!val && !ignoreDelays) {
871
+ if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 1, 6]);
872
+ }
873
+ }
874
+ }
875
+ logger.verbose(`Turning ${val ? 'on' : 'off'} a drain circuit ${cstate.name}`);
876
+ await ncp.circuits.setCircuitStateAsync(cstate, val);
877
+ return cstate;
878
+ } catch (err) { logger.error(`Nixie: Error setSpillwayCircuitStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setBodyCircuitStateAsync ${err.message}`, 'setBodyCircuitStateAsync')); }
879
+ }
880
+
530
881
  public toggleCircuitStateAsync(id: number): Promise<ICircuitState> {
531
882
  let circ = state.circuits.getInterfaceById(id);
532
883
  return this.setCircuitStateAsync(id, !(circ.isOn || false));
533
884
  }
534
885
  public async setLightThemeAsync(id: number, theme: number) {
886
+ if (sys.board.equipmentIds.circuitGroups.isInRange(id)) {
887
+ await this.setLightGroupThemeAsync(id, theme);
888
+ return Promise.resolve(state.lightGroups.getItemById(id));
889
+ }
535
890
  let cstate = state.circuits.getItemById(id);
536
891
  let circ = sys.circuits.getItemById(id);
537
892
  let thm = sys.board.valueMaps.lightThemes.findItem(theme);
@@ -605,7 +960,11 @@ export class NixieCircuitCommands extends CircuitCommands {
605
960
  }
606
961
  return arr;
607
962
  }
608
- public getCircuitFunctions() { return sys.board.valueMaps.circuitFunctions.toArray(); }
963
+ public getCircuitFunctions() {
964
+ let cf = sys.board.valueMaps.circuitFunctions.toArray();
965
+ if (!sys.equipment.shared) cf = cf.filter(x => { return x.name !== 'spillway' && x.name !== 'spadrain' });
966
+ return cf;
967
+ }
609
968
  public getCircuitNames() {
610
969
  return [...sys.board.valueMaps.circuitNames.toArray(), ...sys.board.valueMaps.customNames.toArray()];
611
970
  }
@@ -631,6 +990,8 @@ export class NixieCircuitCommands extends CircuitCommands {
631
990
  if (typeof data.connectionId !== 'undefined') circuit.connectionId = data.connectionId;
632
991
  if (typeof data.deviceBinding !== 'undefined') circuit.deviceBinding = data.deviceBinding;
633
992
  circuit.dontStop = circuit.eggTimer === 1440;
993
+ // update end time in case egg timer is changed while circuit is on
994
+ sys.board.circuits.setEndTime(circuit, scircuit, scircuit.isOn, true);
634
995
  sys.emitEquipmentChange();
635
996
  state.emitEquipmentChanges();
636
997
  ncp.circuits.setCircuitAsync(circuit, data);
@@ -682,6 +1043,8 @@ export class NixieCircuitCommands extends CircuitCommands {
682
1043
  }
683
1044
  group.circuits.length = obj.circuits.length; // RSG - removed as this will delete circuits that were not changed
684
1045
  }
1046
+ // update end time in case group is changed while circuit is on
1047
+ sys.board.circuits.setEndTime(group, sgroup, sgroup.isOn, true);
685
1048
  resolve(group);
686
1049
  });
687
1050
 
@@ -810,42 +1173,33 @@ export class NixieCircuitCommands extends CircuitCommands {
810
1173
  public async setLightGroupThemeAsync(id: number, theme: number): Promise<ICircuitState> {
811
1174
  const grp = sys.lightGroups.getItemById(id);
812
1175
  const sgrp = state.lightGroups.getItemById(id);
813
- grp.lightingTheme = sgrp.lightingTheme = theme;
814
- for (let i = 0; i < grp.circuits.length; i++) {
815
- let c = grp.circuits.getItemByIndex(i);
816
- let cstate = state.circuits.getItemById(c.circuit);
817
- // if theme is 'off' light groups should not turn on
818
- if (cstate.isOn && sys.board.valueMaps.lightThemes.getName(theme) === 'off')
1176
+ //grp.lightingTheme = sgrp.lightingTheme = theme;
1177
+ let thm = sys.board.valueMaps.lightThemes.transform(theme);
1178
+ sgrp.action = sys.board.valueMaps.circuitActions.getValue('lighttheme');
1179
+
1180
+ try {
1181
+ // Go through and set the theme for all lights in the group.
1182
+ for (let i = 0; i < grp.circuits.length; i++) {
1183
+ let c = grp.circuits.getItemByIndex(i);
1184
+ //let cstate = state.circuits.getItemById(c.circuit);
1185
+ await sys.board.circuits.setLightThemeAsync(c.circuit, theme);
819
1186
  await sys.board.circuits.setCircuitStateAsync(c.circuit, false);
820
- else if (!cstate.isOn && sys.board.valueMaps.lightThemes.getName(theme) !== 'off') await sys.board.circuits.setCircuitStateAsync(c.circuit, true);
1187
+ }
1188
+ await utils.sleep(5000);
1189
+ // Turn the circuits all back on again.
1190
+ for (let i = 0; i < grp.circuits.length; i++) {
1191
+ let c = grp.circuits.getItemByIndex(i);
1192
+ //let cstate = state.circuits.getItemById(c.circuit);
1193
+ await sys.board.circuits.setCircuitStateAsync(c.circuit, true);
1194
+ }
1195
+ sgrp.lightingTheme = theme;
1196
+ return sgrp;
821
1197
  }
822
- sgrp.isOn = sys.board.valueMaps.lightThemes.getName(theme) === 'off' ? false : true;
823
- // If we truly want to support themes in lightGroups we probably need to program
824
- // the specific on/off toggles to enable that. For now this will go through the motions but it's just a pretender.
825
- switch (theme) {
826
- case 0: // off
827
- case 1: // on
828
- break;
829
- case 128: // sync
830
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'sync'); });
831
- break;
832
- case 144: // swim
833
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'swim'); });
834
- break;
835
- case 160: // swim
836
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'set'); });
837
- break;
838
- case 190: // save
839
- case 191: // recall
840
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'other'); });
841
- break;
842
- default:
843
- setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'color'); });
844
- // other themes for magicstream?
1198
+ catch (err) { return Promise.reject(err); }
1199
+ finally {
1200
+ sgrp.action = 0;
1201
+ sgrp.emitEquipmentChange();
845
1202
  }
846
- sgrp.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
847
- state.emitEquipmentChanges();
848
- return Promise.resolve(sgrp);
849
1203
  }
850
1204
  public async setLightGroupAttribsAsync(group: LightGroup): Promise<LightGroup> {
851
1205
  let grp = sys.lightGroups.getItemById(group.id);
@@ -861,20 +1215,54 @@ export class NixieCircuitCommands extends CircuitCommands {
861
1215
  }
862
1216
  catch (err) { return Promise.reject(err); }
863
1217
  }
864
- public sequenceLightGroupAsync(id: number, operation: string): Promise<LightGroupState> {
1218
+ //public async runLightCommandAsync(id: number, command: string): Promise<ICircuitState> {
1219
+ // let circ = sys.circuits.getItemById(id);
1220
+ // try {
1221
+ // let type = sys.board.valueMaps.circuitFunctions.transform(circ.type);
1222
+ // let cmd = sys.board.valueMaps.lightCommands.findItem(command);
1223
+ // if (typeof cmd === 'undefined') return Promise.reject(new InvalidOperationError(`Light command ${command} does not exist`, 'runLightCommandAsync'));
1224
+ // if (typeof cmd.sequence !== 'undefined' && circ.master === 1) {
1225
+ // await sys.board.circuits.setCircuitStateAsync(id, true);
1226
+ // await ncp.circuits.sendOnOffSequenceAsync(id, cmd.sequence);
1227
+ // }
1228
+ // return state.circuits.getItemById(id);
1229
+ // }
1230
+ // catch (err) { return Promise.reject(`Error runLightCommandAsync ${err.message}`); }
1231
+ //}
1232
+ public async sequenceLightGroupAsync(id: number, operation: string): Promise<LightGroupState> {
865
1233
  let sgroup = state.lightGroups.getItemById(id);
866
- let nop = sys.board.valueMaps.intellibriteActions.getValue(operation);
867
- if (nop > 0) {
868
- sgroup.action = nop;
869
- sgroup.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
870
- state.emitEquipmentChanges();
871
- setTimeout(function () {
872
- sgroup.action = 0;
873
- sgroup.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
874
- state.emitEquipmentChanges();
875
- }, 20000); // It takes 20 seconds to sequence.
876
- }
877
- return Promise.resolve(sgroup);
1234
+ let grp = sys.lightGroups.getItemById(id);
1235
+ let nop = sys.board.valueMaps.circuitActions.getValue(operation);
1236
+ try {
1237
+ switch (operation) {
1238
+ case 'colorsync':
1239
+ sgroup.action = nop;
1240
+ sgroup.emitEquipmentChange();
1241
+ for (let i = 0; i < grp.circuits.length; i++) {
1242
+ let c = grp.circuits.getItemByIndex(i);
1243
+ await sys.board.circuits.setCircuitStateAsync(c.circuit, false);
1244
+ }
1245
+ await utils.sleep(10000);
1246
+ // Turn the circuits all back on again.
1247
+ for (let i = 0; i < grp.circuits.length; i++) {
1248
+ let c = grp.circuits.getItemByIndex(i);
1249
+ await sys.board.circuits.setCircuitStateAsync(c.circuit, true);
1250
+ }
1251
+ break;
1252
+ case 'colorset':
1253
+ sgroup.action = nop;
1254
+ sgroup.emitEquipmentChange();
1255
+ await utils.sleep(5000);
1256
+ break;
1257
+ case 'colorswim':
1258
+ sgroup.action = nop;
1259
+ sgroup.emitEquipmentChange();
1260
+ await utils.sleep(5000);
1261
+ break;
1262
+ }
1263
+ return sgroup;
1264
+ } catch (err) { return Promise.reject(err); }
1265
+ finally { sgroup.action = 0; sgroup.emitEquipmentChange(); }
878
1266
  }
879
1267
  public async setCircuitGroupStateAsync(id: number, val: boolean): Promise<ICircuitGroupState> {
880
1268
  let grp = sys.circuitGroups.getItemById(id, false, { isActive: false });
@@ -892,19 +1280,21 @@ export class NixieCircuitCommands extends CircuitCommands {
892
1280
  if (circuit.desiredState === 1) cval = val ? true : false;
893
1281
  else if (circuit.desiredState === 2) cval = val ? false : true;
894
1282
  else continue;
895
- arr.push(sys.board.circuits.setCircuitStateAsync(circuit.circuit, cval));
1283
+ await sys.board.circuits.setCircuitStateAsync(circuit.circuit, cval);
1284
+ //arr.push(sys.board.circuits.setCircuitStateAsync(circuit.circuit, cval));
896
1285
  }
897
- return new Promise<ICircuitGroupState>(async (resolve, reject) => {
898
- await Promise.all(arr).catch((err) => { reject(err) });
899
- resolve(gstate);
900
- });
1286
+ return state.circuitGroups.getItemById(grp.id, grp.isActive !== false);
1287
+ //return new Promise<ICircuitGroupState>(async (resolve, reject) => {
1288
+ // await Promise.all(arr).catch((err) => { reject(err) });
1289
+ // resolve(gstate);
1290
+ //});
901
1291
  }
902
1292
  public async setLightGroupStateAsync(id: number, val: boolean): Promise<ICircuitGroupState> {
903
1293
  let grp = sys.circuitGroups.getItemById(id, false, { isActive: false });
904
1294
  if (grp.dataName === 'circuitGroupConfig') return await sys.board.circuits.setCircuitGroupStateAsync(id, val);
905
1295
  let gstate = state.lightGroups.getItemById(grp.id, grp.isActive !== false);
906
1296
  let circuits = grp.circuits.toArray();
907
- sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(gstate.id), gstate, val);
1297
+ sys.board.circuits.setEndTime(grp, gstate, val);
908
1298
  gstate.isOn = val;
909
1299
  let arr = [];
910
1300
  for (let i = 0; i < circuits.length; i++) {
@@ -942,6 +1332,8 @@ export class NixieFeatureCommands extends FeatureCommands {
942
1332
  if (typeof obj.dontStop !== 'undefined' && utils.makeBool(obj.dontStop) === true) obj.eggTimer = 1440;
943
1333
  if (typeof obj.eggTimer !== 'undefined') feature.eggTimer = parseInt(obj.eggTimer, 10);
944
1334
  feature.dontStop = feature.eggTimer === 1440;
1335
+ // update end time in case feature is changed while circuit is on
1336
+ sys.board.circuits.setEndTime(feature, sfeature, sfeature.isOn, true);
945
1337
  return new Promise<Feature>((resolve, reject) => { resolve(feature); });
946
1338
  }
947
1339
  public async deleteFeatureAsync(obj: any): Promise<Feature> {
@@ -963,20 +1355,99 @@ export class NixieFeatureCommands extends FeatureCommands {
963
1355
  else
964
1356
  Promise.reject(new InvalidEquipmentIdError('Feature id has not been defined', undefined, 'Feature'));
965
1357
  }
966
- public async setFeatureStateAsync(id: number, val: boolean): Promise<ICircuitState> {
1358
+ public async setFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
967
1359
  try {
968
1360
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
969
1361
  if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid feature id: ${id}`, id, 'Feature'));
970
1362
  let feature = sys.features.getItemById(id);
971
1363
  let fstate = state.features.getItemById(feature.id, feature.isActive !== false);
972
- sys.board.circuits.setEndTime(feature, fstate, val);
973
- fstate.isOn = val;
1364
+ feature.master = 1;
1365
+ let ftype = sys.board.valueMaps.featureFunctions.getName(feature.type);
1366
+ if(val && !fstate.isOn) sys.board.circuits.setEndTime(feature, fstate, val);
1367
+ switch (ftype) {
1368
+ case 'spadrain':
1369
+ this.setDrainFeatureStateAsync(id, val, ignoreDelays);
1370
+ break;
1371
+ case 'spillway':
1372
+ this.setSpillwayFeatureStateAsync(id, val, ignoreDelays);
1373
+ break;
1374
+ default:
1375
+ fstate.isOn = val;
1376
+ break;
1377
+ }
974
1378
  sys.board.valves.syncValveStates();
975
1379
  ncp.pumps.syncPumpStates();
1380
+ if (!val){
1381
+ if (fstate.manualPriorityActive) delayMgr.cancelManualPriorityDelay(fstate.id);
1382
+ fstate.manualPriorityActive = false; // if the delay was previously cancelled, still need to turn this off
1383
+ }
976
1384
  state.emitEquipmentChanges();
977
1385
  return fstate;
978
1386
  } catch (err) { return Promise.reject(new Error(`Error setting feature state ${err.message}`)); }
979
1387
  }
1388
+ protected async setSpillwayFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<FeatureState> {
1389
+ try {
1390
+ let cstate = state.features.getItemById(id);
1391
+ if (cstate.isOn !== val) {
1392
+ if (sys.equipment.shared === true) {
1393
+ let spastate = state.temps.bodies.getItemById(2);
1394
+ if (val) {
1395
+ if (spastate.isOn || spastate.startDelay) {
1396
+ logger.warn(`Cannot turn ${cstate.name} on because ${spastate.name} is on`);
1397
+ return cstate;
1398
+ }
1399
+ // If there are any drain circuits or features that are currently engaged we need to turn them off.
1400
+ await sys.board.circuits.turnOffDrainCircuits(ignoreDelays);
1401
+ if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 6]);
1402
+ }
1403
+ else if (!val) {
1404
+ let arrIds = sys.board.valves.getBodyValveCircuitIds(true);
1405
+ if (arrIds.length > 1) {
1406
+ if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 6]);
1407
+ }
1408
+ }
1409
+ }
1410
+ logger.verbose(`Turning ${val ? 'on' : 'off'} a spillway feature ${cstate.name}`);
1411
+ cstate.isOn = val;
1412
+ }
1413
+ return cstate;
1414
+ } catch (err) { logger.error(`Nixie: Error setSpillwayFeatureStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setSpillwayFeatureStateAsync ${err.message}`, 'setSpillwayFeatureStateAsync')); }
1415
+ }
1416
+ protected async setDrainFeatureStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<FeatureState> {
1417
+ try {
1418
+ // Drain circuits can be very bad. This is because they can be turned on then never turned off
1419
+ // we may want to create some limits are to how long they can be on or even force them off
1420
+ // if for instance the spa is not on.
1421
+ // RULES FOR DRAIN CIRCUITS:
1422
+ // 1. All spillway circuits must be off.
1423
+ let cstate = state.features.getItemById(id);
1424
+ if (cstate.isOn !== val) {
1425
+ if (sys.equipment.shared === true) {
1426
+ if (val) {
1427
+ // First we need to check to see if the pool is on.
1428
+ let poolstate = state.temps.bodies.getItemById(1);
1429
+ let spastate = state.temps.bodies.getItemById(2);
1430
+ if ((spastate.isOn || spastate.startDelay || poolstate.isOn || poolstate.startDelay) && val) {
1431
+ logger.warn(`Cannot turn ${cstate.name} on because a body circuit is on`);
1432
+ return cstate;
1433
+ }
1434
+ // If there are any spillway circuits or features that are currently engaged we need to turn them off.
1435
+ await sys.board.circuits.turnOffSpillwayCircuits(true);
1436
+ // If there are any cleaner circuits on for the main body turn them off.
1437
+ await sys.board.circuits.turnOffCleanerCircuits(state.temps.bodies.getItemById(1), true);
1438
+ if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 1, 6]);
1439
+ }
1440
+ else if (!val) {
1441
+ if (!ignoreDelays && sys.general.options.pumpDelay && sys.general.options.valveDelayTime > 0) sys.board.pumps.setPumpValveDelays([id, 1, 6]);
1442
+ }
1443
+ }
1444
+ logger.verbose(`Turning ${val ? 'on' : 'off'} a spa drain circuit ${cstate.name}`);
1445
+ cstate.isOn = val;
1446
+ }
1447
+ return cstate;
1448
+ } catch (err) { logger.error(`Nixie: Error setSpillwayCircuitStateAsync ${err.message}`); return Promise.reject(new BoardProcessError(`Nixie: Error setBodyCircuitStateAsync ${err.message}`, 'setBodyCircuitStateAsync')); }
1449
+ }
1450
+
980
1451
  public async toggleFeatureStateAsync(id: number): Promise<ICircuitState> {
981
1452
  let feat = state.features.getItemById(id);
982
1453
  return this.setFeatureStateAsync(id, !(feat.isOn || false));
@@ -1005,6 +1476,11 @@ export class NixieFeatureCommands extends FeatureCommands {
1005
1476
  }
1006
1477
  let sgrp = state.circuitGroups.getItemById(grp.id);
1007
1478
  sgrp.isOn = bIsOn;
1479
+ if (sgrp.isOn && typeof sgrp.endTime === 'undefined') sys.board.circuits.setEndTime(grp, sgrp, sgrp.isOn, true);
1480
+ if (!sgrp.isOn && sgrp.manualPriorityActive){
1481
+ delayMgr.cancelManualPriorityDelays();
1482
+ sgrp.manualPriorityActive = false; // if the delay was previously cancelled, still need to turn this off
1483
+ }
1008
1484
  }
1009
1485
  sys.board.valves.syncValveStates();
1010
1486
  }
@@ -1022,12 +1498,61 @@ export class NixieFeatureCommands extends FeatureCommands {
1022
1498
  }
1023
1499
  let sgrp = state.lightGroups.getItemById(grp.id);
1024
1500
  sgrp.isOn = bIsOn;
1501
+ if (sgrp.isOn && typeof sgrp.endTime === 'undefined') sys.board.circuits.setEndTime(grp, sgrp, sgrp.isOn, true);
1502
+ if (!sgrp.isOn && sgrp.manualPriorityActive){
1503
+ delayMgr.cancelManualPriorityDelay(grp.id);
1504
+ sgrp.manualPriorityActive = false; // if the delay was previously cancelled, still need to turn this off
1505
+ }
1025
1506
  }
1507
+
1026
1508
  sys.board.valves.syncValveStates();
1027
1509
  }
1028
1510
  state.emitEquipmentChanges();
1029
1511
  }
1512
+ }
1513
+ export class NixiePumpCommands extends PumpCommands {
1514
+ public async setPumpValveDelays(circuitIds: number[], delay?: number) {
1515
+ try {
1516
+ logger.info(`Setting pump valve delays: ${JSON.stringify(circuitIds)}`);
1517
+ // Alright now we have to delay the pumps associated with the circuit. So lets iterate all our
1518
+ // pump states and see where we land.
1519
+ for (let i = 0; i < sys.pumps.length; i++) {
1520
+ let pump = sys.pumps.getItemByIndex(i);
1521
+ let pstate = state.pumps.getItemById(pump.id);
1522
+ let pt = sys.board.valueMaps.pumpTypes.get(pump.type);
1030
1523
 
1524
+ // [1, { name: 'ss', desc: 'Single Speed', maxCircuits: 0, hasAddress: false, hasBody: true, maxRelays: 1 }],
1525
+ // [2, { name: 'ds', desc: 'Two Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 2 }],
1526
+ // [3, { name: 'vs', desc: 'Intelliflo VS', maxPrimingTime: 6, minSpeed: 450, maxSpeed: 3450, maxCircuits: 8, hasAddress: true }],
1527
+ // [4, { name: 'vsf', desc: 'Intelliflo VSF', minSpeed: 450, maxSpeed: 3450, minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
1528
+ // [5, { name: 'vf', desc: 'Intelliflo VF', minFlow: 15, maxFlow: 130, maxCircuits: 8, hasAddress: true }],
1529
+ // [100, { name: 'sf', desc: 'SuperFlo VS', hasAddress: false, maxCircuits: 8, maxRelays: 4, equipmentMaster: 1 }]
1530
+ switch (pt.name) {
1531
+ case 'ss':
1532
+ // If a single speed pump is designated it will be the filter pump but we need to map any settings
1533
+ // to bodies.
1534
+ console.log(`Body: ${pump.body} Pump: ${pump.name} Pool: ${circuitIds.includes(6)} `);
1535
+ if ((pump.body === 255 && (circuitIds.includes(6) || circuitIds.includes(1))) ||
1536
+ (pump.body === 0 && circuitIds.includes(6)) ||
1537
+ (pump.body === 101 && circuitIds.includes(1))) {
1538
+ delayMgr.setPumpValveDelay(pstate);
1539
+ }
1540
+ break;
1541
+ default:
1542
+ if (pt.maxCircuits > 0) {
1543
+ for (let j = 0; j < pump.circuits.length; j++) {
1544
+ let circ = pump.circuits.getItemByIndex(j);
1545
+ if (circuitIds.includes(circ.circuit)) {
1546
+ delayMgr.setPumpValveDelay(pstate);
1547
+ break;
1548
+ }
1549
+ }
1550
+ }
1551
+ break;
1552
+ }
1553
+ }
1554
+ } catch (err) { }
1555
+ }
1031
1556
  }
1032
1557
  export class NixieValveCommands extends ValveCommands {
1033
1558
  public async setValveAsync(obj: any): Promise<Valve> {
@@ -1078,14 +1603,15 @@ export class NixieValveCommands extends ValveCommands {
1078
1603
  export class NixieHeaterCommands extends HeaterCommands {
1079
1604
  public async setHeaterAsync(obj: any): Promise<Heater> {
1080
1605
  try {
1081
- let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
1606
+ let id = typeof obj.id === 'undefined' || !obj.id ? -1 : parseInt(obj.id, 10);
1082
1607
  if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
1083
- else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Virtual Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater'));
1608
+ else if (id < 256 && id > 0) return Promise.reject(new InvalidEquipmentIdError('Nixie Heaters controlled by njspc must have an Id > 256.', obj.id, 'Heater'));
1084
1609
  let heater: Heater;
1085
1610
  if (id <= 0) {
1086
1611
  // We are adding a heater. In this case all heaters are virtual.
1087
1612
  let vheaters = sys.heaters.filter(h => h.master === 1);
1088
- id = vheaters.length + 256;
1613
+ id = Math.max(vheaters.getMaxId() + 1 || 0, vheaters.length + 256);
1614
+ logger.info(`Adding a new heater with id ${id}`);
1089
1615
  }
1090
1616
  heater = sys.heaters.getItemById(id, true);
1091
1617
  if (typeof obj !== undefined) {
@@ -1105,16 +1631,17 @@ export class NixieHeaterCommands extends HeaterCommands {
1105
1631
  } catch (err) { return Promise.reject(new Error(`Error setting heater configuration: ${err}`)); }
1106
1632
  }
1107
1633
  public async deleteHeaterAsync(obj: any): Promise<Heater> {
1108
- return new Promise<Heater>((resolve, reject) => {
1634
+ try {
1109
1635
  let id = parseInt(obj.id, 10);
1110
- if (isNaN(id)) return reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
1636
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
1111
1637
  let heater = sys.heaters.getItemById(id);
1112
1638
  heater.isActive = false;
1639
+ await ncp.heaters.deleteHeaterAsync(id);
1113
1640
  sys.heaters.removeItemById(id);
1114
1641
  state.heaters.removeItemById(id);
1115
1642
  sys.board.heaters.updateHeaterServices();
1116
- resolve(heater);
1117
- });
1643
+ return heater;
1644
+ } catch (err) { return Promise.reject(new BoardProcessError(err.message, 'deleteHeaterAsync')); }
1118
1645
  }
1119
1646
  public updateHeaterServices() {
1120
1647
  let htypes = sys.board.heaters.getInstalledHeaterTypes();
@@ -1122,7 +1649,7 @@ export class NixieHeaterCommands extends HeaterCommands {
1122
1649
  let heatPumpInstalled = htypes.heatpump > 0;
1123
1650
  let gasHeaterInstalled = htypes.gas > 0;
1124
1651
  let ultratempInstalled = htypes.ultratemp > 0;
1125
-
1652
+ let mastertempInstalled = htypes.mastertemp > 0;
1126
1653
  // The heat mode options are
1127
1654
  // 1 = Off
1128
1655
  // 2 = Gas Heater
@@ -1142,8 +1669,10 @@ export class NixieHeaterCommands extends HeaterCommands {
1142
1669
  // 3 = Solar Heater
1143
1670
  // 4 = Solar Preferred
1144
1671
  // 5 = Heat Pump
1672
+
1145
1673
  if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[1, { name: 'off', desc: 'Off' }]]);
1146
1674
  if (gasHeaterInstalled) sys.board.valueMaps.heatSources.merge([[2, { name: 'heater', desc: 'Heater' }]]);
1675
+ if (mastertempInstalled) sys.board.valueMaps.heatSources.merge([[11, { name: 'mtheater', desc: 'MasterTemp' }]]);
1147
1676
  if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar Only', hasCoolSetpoint: htypes.hasCoolSetpoint }], [4, { name: 'solarpref', desc: 'Solar Preferred', hasCoolSetpoint: htypes.hasCoolSetpoint }]]);
1148
1677
  else if (solarInstalled) sys.board.valueMaps.heatSources.merge([[3, { name: 'solar', desc: 'Solar', hasCoolsetpoint: htypes.hasCoolSetpoint }]]);
1149
1678
  if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatSources.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Pref' }]]);
@@ -1154,11 +1683,12 @@ export class NixieHeaterCommands extends HeaterCommands {
1154
1683
 
1155
1684
  sys.board.valueMaps.heatModes = new byteValueMap([[1, { name: 'off', desc: 'Off' }]]);
1156
1685
  if (gasHeaterInstalled) sys.board.valueMaps.heatModes.merge([[2, { name: 'heater', desc: 'Heater' }]]);
1157
- if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar Only' }], [4, { name: 'solarpref', desc: 'Solar Preferred' }]]);
1686
+ if (mastertempInstalled) sys.board.valueMaps.heatModes.merge([[11, { name: 'mtheater', desc: 'MasterTemp' }]]);
1687
+ if (solarInstalled && (gasHeaterInstalled || heatPumpInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar Only' }], [4, { name: 'solarpref', desc: 'Solar Preferred' }]]);
1158
1688
  else if (solarInstalled) sys.board.valueMaps.heatModes.merge([[3, { name: 'solar', desc: 'Solar' }]]);
1159
- if (ultratempInstalled && (gasHeaterInstalled || heatPumpInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp Only' }], [6, { name: 'ultratemppref', desc: 'UltraTemp Pref' }]]);
1689
+ if (ultratempInstalled && (gasHeaterInstalled || heatPumpInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp Only' }], [6, { name: 'ultratemppref', desc: 'UltraTemp Pref' }]]);
1160
1690
  else if (ultratempInstalled) sys.board.valueMaps.heatModes.merge([[5, { name: 'ultratemp', desc: 'UltraTemp' }]]);
1161
- if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
1691
+ if (heatPumpInstalled && (gasHeaterInstalled || solarInstalled || mastertempInstalled)) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heatpump Only' }], [25, { name: 'heatpumppref', desc: 'Heat Pump Preferred' }]]);
1162
1692
  else if (heatPumpInstalled) sys.board.valueMaps.heatModes.merge([[9, { name: 'heatpump', desc: 'Heat Pump' }]]);
1163
1693
  // Now set the body data.
1164
1694
  for (let i = 0; i < sys.bodies.length; i++) {