nodejs-poolcontroller 8.0.5 → 8.1.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.
package/Changelog CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 8.1.0
4
+ 1. Support for dual chlorinators with REM chem controllers. It is now possible to have two separate chlorinators controlled in 'dynamic' mode by two separate REM chems. Note: In order for REM chem to control each chlorinator, each needs to be on a dedicated RS-485 port (not shared with an OCP or any other chlorinator).
5
+
6
+ ## 8.0.1-8.0.5
7
+ 1. Bug fixes including:
8
+ a. schedule end time errors
9
+ b. manual priority
10
+ c. screenlogic recurring schedules
11
+ d. VF pump message sequences
12
+ e. intellibrite themes
13
+ f. schedules are evaluated by bodies first and then everything else
14
+ g. solar stop/start delta logic
15
+ 2. Jandy WaterColors support
16
+ 3. Initial docker support (github docker actions)
17
+
3
18
  ## 8.0.0
4
19
  1. Refactor comms code to Async
5
20
  2. Update dependencies and Node >16
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ```diff
2
2
  - INTELLICENTER USERS: Do not upgrade Intellicenter to 2.006. Rollback to 1.064 to use this application.
3
3
  ```
4
- # nodejs-poolController - Version 8.0
4
+ # nodejs-poolController - Version 8.1
5
5
 
6
6
  ## What is nodejs-poolController
7
7
 
@@ -26,6 +26,10 @@ Equipment supported
26
26
  ## Latest Changes
27
27
  See [Changelog](https://github.com/tagyoureit/nodejs-poolController/blob/master/Changelog)
28
28
 
29
+ ## What's new in 8.1?
30
+
31
+ Support for dual chlorinators with REM chem controllers. It is now possible to have two separate chlorinators controlled in 'dynamic' mode by two separate REM chems. Note: In order for REM chem to control each chlorinator, each needs to be on a dedicated RS-485 port (not shared with an OCP or any other chlorinator).
32
+
29
33
  ## What's new in 8.0?
30
34
 
31
35
  Screenlogic can now be used as a direct connection point. If you feel that integrating an RS-485 adapter is a bit too much, then this is an option for you. The preferred method is still RS-485 as it is more fully featured.
@@ -1678,6 +1678,10 @@ export class Chlorinator extends EqItem {
1678
1678
  public set ignoreSaltReading(val: boolean) { this.setDataVal('ignoreSaltReading', val); }
1679
1679
  public get model() { return this.data.model; }
1680
1680
  public set model(val: number | any) { this.setDataVal('model', sys.board.valueMaps.chlorinatorModel.encode(val)); }
1681
+ public get ratedLbs(): number {
1682
+ let model = sys.board.valueMaps.chlorinatorModel.get(this.model);
1683
+ return typeof model.chlorinePerSec !== 'undefined' ? model.chlorinePerSec : 0;
1684
+ }
1681
1685
  }
1682
1686
  export class ValveCollection extends EqItemCollection<Valve> {
1683
1687
  constructor(data: any, name?: string) { super(data, name || "valves"); }
@@ -2207,80 +2211,6 @@ export interface IChemController {
2207
2211
  }
2208
2212
  export class ChemController extends EqItem implements IChemController {
2209
2213
  public initData() {
2210
- //var chemController = {
2211
- // id: 'number', // Id of the controller
2212
- // name: 'string', // Name assigned to the controller
2213
- // type: 'valueMap', // intellichem, rem -- There is an unknown but that should probably go away.
2214
- // body: 'valueMap', // Body assigned to the chem controller.
2215
- // address: 'number', // Address for IntelliChem controller only.
2216
- // isActive: 'booean',
2217
- // isVirtual: 'boolean', // False if controlled by OCP.
2218
- // calciumHardness: 'number',
2219
- // cyanuricAcid: 'number',
2220
- // alkalinity: 'number',
2221
- // HMIAdvancedDisplay: 'boolean', // This is related to IntelliChem and determines what is displayed on the controller.
2222
- // ph: { // pH chemical structure
2223
- // chemType: 'string', // Constant ph
2224
- // enabled: 'boolean', // Allows disabling the functions without deleting the settings.
2225
- // dosingMethod: 'valueMap', // manual, volume, volumeTime.
2226
- // // manual = The dosing pump is not triggered.
2227
- // // volume = Time is not considered as a limit to the dosing.
2228
- // // time = The only limit to the dose is the amount of time.
2229
- // // volumeTime = Limit the dose by volume or time whichever is sooner.
2230
- // maxDosingTime: 'number', // The maximum amount of time a dose can occur before mixing.
2231
- // maxDosingVolume: 'number', // The maximum volume for a dose in mL.
2232
- // mixingTime: 'number', // Amount of time between in seconds doses that the pump must run before adding another dose.
2233
- // startDelay: 'number', // The number of seconds that the pump must be running prior to considering a dose.
2234
- // setpoint: 'number', // Target setpoint for pH
2235
- // phSupply: 'valueMap', // base or acid.
2236
- // pump: {
2237
- // type: 'valueMap', // none, relay, ezo-pmp
2238
- // connectionId: 'uuid', // Unique identifier for njspc external connections.
2239
- // deviceBinding: 'string', // Binding value for REM to tell it what device is involved.
2240
- // ratedFlow: 'number', // The standard flow rate for the pump in mL/min.
2241
- // },
2242
- // tank: {
2243
- // capacity: 'number', // Capacity of the tank in the units provided.
2244
- // units: 'valueMap' // gal, mL, cL, L, oz, pt, qt.
2245
- // },
2246
- // probe: {
2247
- // connectionId: 'uuid', // A unique identifier that has been generated for connections in njspc.
2248
- // deviceBinding: 'string', // A mapping value that is used by REM to determine which device is used.
2249
- // type: 'valueMap' // none, ezo-ph, other.
2250
- // }
2251
-
2252
- // },
2253
- // orp: { // ORP chemical structure
2254
- // chemType: 'string', // Constant orp
2255
- // enabled: 'boolean', // Allows disabling the functions without deleting the settings.
2256
- // dosingMethod: 'valueMap', // manual, volume, volumeTime.
2257
- // // manual = The dosing pump is not triggered.
2258
- // // volume = Time is not considered as a limit to the dosing.
2259
- // // time = The only limit to the dose is the amount of time.
2260
- // // volumeTime = Limit the dose by volume or time whichever is sooner.
2261
- // maxDosingTime: 'number', // The maximum amount of time a dose can occur before mixing.
2262
- // maxDosingVolume: 'number', // The maximum volume for a dose in mL.
2263
- // mixingTime: 'number', // Amount of time between in seconds doses that the pump must run before adding another dose.
2264
- // startDelay: 'number', // The number of seconds that the pump must be running prior to considering a dose.
2265
- // setpoint: 'number', // Target setpoint for ORP
2266
- // useChlorinator: 'boolean', // Indicates whether the chlorinator will be used for dosing.
2267
- // pump: {
2268
- // type: 'valueMap', // none, relay, ezo-pmp
2269
- // connectionId: 'uuid', // Unique identifier for njspc external connections.
2270
- // deviceBinding: 'string', // Binding value for REM to tell it what device is involved.
2271
- // ratedFlow: 'number', // The standard flow rate for the pump in mL/min.
2272
- // },
2273
- // tank: {
2274
- // capacity: 'number', // Capacity of the tank in the units provided.
2275
- // units: 'valueMap' // gal, mL, cL, L, oz, pt, qt.
2276
- // },
2277
- // probe: {
2278
- // connectionId: 'uuid', // A unique identifier that has been generated for connections in njspc.
2279
- // deviceBinding: 'string', // A mapping value that is used by REM to determine which device is used.
2280
- // type: 'valueMap' // none, ezo-orp, other.
2281
- // }
2282
- // }
2283
- //}
2284
2214
  if (typeof this.data.lsiRange === 'undefined') this.data.lsiRange = { low: -.5, high: .5, enabled: true };
2285
2215
  if (typeof this.data.borates === 'undefined') this.data.borates = 0;
2286
2216
  if (typeof this.data.siCalcType === 'undefined') this.data.siCalcType = 0;
@@ -2299,8 +2229,6 @@ export class ChemController extends EqItem implements IChemController {
2299
2229
  public set address(val: number) { this.setDataVal('address', val); }
2300
2230
  public get isActive(): boolean { return this.data.isActive; }
2301
2231
  public set isActive(val: boolean) { this.setDataVal('isActive', val); }
2302
- // public get isVirtual(): boolean { return this.data.isVirtual; }
2303
- // public set isVirtual(val: boolean) { this.setDataVal('isVirtual', val); }
2304
2232
  public get calciumHardness(): number { return this.data.calciumHardness; }
2305
2233
  public set calciumHardness(val: number) { this.setDataVal('calciumHardness', val); }
2306
2234
  public get cyanuricAcid(): number { return this.data.cyanuricAcid; }
@@ -2460,7 +2388,7 @@ export class Chemical extends ChildEqItem implements IChemical {
2460
2388
  public set startDelay(val: number) { this.setDataVal('startDelay', val); }
2461
2389
  public get pump(): ChemicalPump { return new ChemicalPump(this.data, 'pump', this); }
2462
2390
  public get tank(): ChemicalTank { return new ChemicalTank(this.data, 'tank', this); }
2463
- public get chlor(): ChemicalChlor { return new ChemicalChlor(this.data, 'chlor', this); }
2391
+ // public get chlor(): Chlorinator { return new ChemicalChlor(this.data, 'chlor', this); }
2464
2392
  public get setpoint(): number { return this.data.setpoint; }
2465
2393
  public set setpoint(val: number) { this.setDataVal('setpoint', val); }
2466
2394
  public get tolerance(): AlarmSetting { return new AlarmSetting(this.data, 'tolerance', this); }
@@ -2518,6 +2446,14 @@ export class ChemicalORP extends Chemical {
2518
2446
  }
2519
2447
  public get useChlorinator(): boolean { return utils.makeBool(this.data.useChlorinator); }
2520
2448
  public set useChlorinator(val: boolean) { this.setDataVal('useChlorinator', val); }
2449
+ public get chlorId(): number {
2450
+ if (typeof this.data.chlorId === 'undefined'){
2451
+ // default to 1st chlorinator if not set; this is a backwards compatibility item when upgrading to 8.1
2452
+ return sys.chlorinators.getItemByIndex(0).id;
2453
+ }
2454
+ return this.data.chlorId;
2455
+ }
2456
+ public set chlorId(val: number) { this.setDataVal('chlorId', val); }
2521
2457
  public get phLockout(): number { return this.data.phLockout; }
2522
2458
  public set phLockout(val: number) { this.setDataVal('phLockout', val); }
2523
2459
  public get probe(): ChemicalORPProbe { return new ChemicalORPProbe(this.data, 'probe', this); }
@@ -2661,7 +2597,7 @@ export class ChemicalTank extends ChildEqItem {
2661
2597
  return tank;
2662
2598
  }
2663
2599
  }
2664
- export class ChemicalChlor extends ChildEqItem {
2600
+ /* export class ChemicalChlor extends ChildEqItem {
2665
2601
  // This whole class is a reference to the first chlorinator.
2666
2602
  // This may not follow a best practice
2667
2603
  // and certainly won't work for multiple chlors
@@ -2695,7 +2631,7 @@ export class ChemicalChlor extends ChildEqItem {
2695
2631
  chlor.model = sys.board.valueMaps.chlorinatorModel.transform(this.model);
2696
2632
  return chlor;
2697
2633
  }
2698
- }
2634
+ } */
2699
2635
  export class AlarmSetting extends ChildEqItem {
2700
2636
  public dataName = 'AlarmSettingConfig';
2701
2637
  public initData() {
@@ -2658,74 +2658,6 @@ export class ChemControllerState extends EqState implements IChemControllerState
2658
2658
  if (typeof this.data.siCalcType === 'undefined') {
2659
2659
  this.data.siCalcType = sys.board.valueMaps.siCalcTypes.transform(0);
2660
2660
  }
2661
- //var chemControllerState = {
2662
- // lastComm: 'number', // The unix time the chem controller sent its status.
2663
- // id: 'number', // Id of the chemController.
2664
- // type: 'valueMap', // intellichem, rem.
2665
- // address: 'number', // Assigned address if IntelliChem.
2666
- // name: 'string', // Name assigned to the controller.
2667
- // status: 'valueMap', // ok, nocomms, setupError
2668
- // body: 'valueMap', // Body that the chemController is assigned to.
2669
- // flowDetected: 'boolean', // True if there is currently sufficient flow to read and dose.
2670
- // flowDelay: 'boolean', // True of the controller is currently under a flow delay.
2671
- // firmware: 'string', // Firmware version from IntelliChem (this should be in config)
2672
- // saturationIndex: 'number', // Calculated LSI for the body.
2673
- // isActive: 'boolean',
2674
- // alarms: {}, // This has not changed although additional alarms will be added.
2675
- // warnings: {}, // This has not changed although additional warnings will be added.
2676
- // chemistryStatus: 'valueMap', // Current water quality status.
2677
- // ph: {
2678
- // chemType: 'string', // Constant ph.
2679
- // dosingTimeRemaining: 'number', // The number of seconds remaining for the current dose.
2680
- // dosingVolumeRemaining: 'number', // Remaining volume for the current dose in mL.
2681
- // mixTimeRemaining: 'number', // The number of seconds remaining in the current mix cycle.
2682
- // dosingStatus: 'valueMap', // dosing, monitoring, mixing.
2683
- // level: 'number', // The current pH level.
2684
- // lockout: 'boolean', // True if an attempt to dose was thwarted by error.
2685
- // manualDosing: 'boolean', // True if the pump is running outside of a dosing command.
2686
- // dailyLimitReached: 'boolean', // True if the calculated daily limit has been reached based upon body volume.
2687
- // pump: {
2688
- // type: 'valueMap', // The defined pump type.
2689
- // isDosing: 'boolean', // True if the pump is running.
2690
- // },
2691
- // tank: {
2692
- // level: 'number', // The current level for the tank.
2693
- // capacity: 'number', // Total capacity for the tank.
2694
- // units: 'valueMap', // nounits, gal, mL, cL, L, oz, pt, qt.
2695
- // },
2696
- // probe: {
2697
- // level: 'number', // Current ph level as measured by the probe.
2698
- // temperature: 'number', // The temperature used to calculate the adjusted probe level.
2699
- // tempUnits: 'valueMap' // Units for the temperature C or F.
2700
- // }
2701
- // },
2702
- // orp: {
2703
- // chemType: 'string', // Constant orp.
2704
- // dosingTimeRemaining: 'number', // The number of seconds remaining for the current dose.
2705
- // dosingVolumeRemaining: 'number', // Remaining volume for the current dose in mL.
2706
- // mixTimeRemaining: 'number', // The number of seconds remaining in the current mix cycle.
2707
- // dosingStatus: 'valueMap', // dosing, monitoring, mixing.
2708
- // level: 'number', // The current ORP level.
2709
- // lockout: 'boolean', // True if an attempt to dose was thwarted by error.
2710
- // manualDosing: 'boolean', // True if the pump is running outside of a dosing command.
2711
- // dailyLimitReached: 'boolean', // True if the calculated daily limit has been reached based upon body volume.
2712
- // pump: {
2713
- // type: 'valueMap', // The defined pump type.
2714
- // isDosing: 'boolean', // True if the pump is running.
2715
- // },
2716
- // tank: {
2717
- // level: 'number', // The current level for the tank.
2718
- // capacity: 'number', // Total capacity for the tank.
2719
- // units: 'valueMap', // nounits, gal, mL, cL, L, oz, pt, qt.
2720
- // },
2721
- // probe: {
2722
- // level: 'number', // Current ORP level as measured by the probe.
2723
- // temperature: 'number', // The temperature used to calculate the adjusted probe level.
2724
- // tempUnits: 'valueMap' // Units for the temperature C or F.
2725
- // }
2726
- // }
2727
- //}
2728
-
2729
2661
  }
2730
2662
  public dataName: string = 'chemController';
2731
2663
  public get lastComm(): number { return this.data.lastComm || 0; }
@@ -3175,8 +3107,8 @@ export class ChemicalORPState extends ChemicalState {
3175
3107
  public get chemType() { return 'orp'; }
3176
3108
  public set chemType(val) { this.setDataVal('chemType', val); }
3177
3109
  public get probe() { return new ChemicalProbeORPState(this.data, 'probe', this); }
3178
- public get useChlorinator(): boolean { return utils.makeBool(this.data.useChlorinator); }
3179
- public set useChlorinator(val: boolean) { this.setDataVal('useChlorinator', val); }
3110
+ // public get useChlorinator(): boolean { return utils.makeBool(this.data.useChlorinator); }
3111
+ // public set useChlorinator(val: boolean) { this.setDataVal('useChlorinator', val); }
3180
3112
  public get suspendDosing(): boolean {
3181
3113
  let cc = this.chemController;
3182
3114
  return cc.alarms.comms !== 0 || cc.alarms.orpProbeFault !== 0 || cc.alarms.orpPumpFault !== 0 || cc.alarms.bodyFault !== 0;
@@ -73,6 +73,7 @@ export class NixieBoard extends SystemBoard {
73
73
  [14, { name: 'colorlogic', desc: 'ColorLogic', isLight: true, theme: 'colorlogic' }],
74
74
  [15, { name: 'spadrain', desc: 'Spa Drain' }],
75
75
  [16, { name: 'pooltone', desc: 'Pool Tone', isLight: true, theme: 'pooltone' }],
76
+ [17, { name: 'watercolors', desc: 'WaterColors', isLight: true, theme: 'watercolors' }],
76
77
  ]);
77
78
  this.valueMaps.pumpTypes = new byteValueMap([
78
79
  [1, { name: 'ss', desc: 'Single Speed', maxCircuits: 8, hasAddress: false, hasBody: false, maxRelays: 1, relays: [{ id: 1, name: 'Pump On/Off' }]}],
@@ -1,7 +1,7 @@
1
1
  import { clearTimeout, setTimeout } from 'timers';
2
2
  import { conn } from '../../../controller/comms/Comms';
3
3
  import { Outbound, Protocol, Response } from '../../../controller/comms/messages/Messages';
4
- import { IChemical, IChemController, Chlorinator, ChemController, ChemControllerCollection, ChemFlowSensor, Chemical, ChemicalChlor, ChemicalORP, ChemicalORPProbe, ChemicalPh, ChemicalPhProbe, ChemicalProbe, ChemicalPump, ChemicalTank, sys } from "../../../controller/Equipment";
4
+ import { IChemical, IChemController, Chlorinator, ChemController, ChemControllerCollection, ChemFlowSensor, Chemical, ChemicalORP, ChemicalORPProbe, ChemicalPh, ChemicalPhProbe, ChemicalProbe, ChemicalPump, ChemicalTank, sys } from "../../../controller/Equipment";
5
5
  import { logger } from '../../../logger/Logger';
6
6
  import { InterfaceServerResponse, webApp } from "../../../web/Server";
7
7
  import { Timestamp, utils } from '../../Constants';
@@ -727,11 +727,16 @@ export class NixieChemController extends NixieChemControllerBase {
727
727
  else schem.alarms.orp = 0;
728
728
  let chlorErr = 0;
729
729
  if (useChlorinator && schem.isBodyOn) {
730
- let chlors = sys.chlorinators.getByBody(schem.activeBodyId);
731
- let chlor = chlors.getItemByIndex(0);
732
- let schlor = state.chlorinators.getItemById(chlor.id);
733
- this.orp.chlor.chlorId = chlor.id;
734
- if (schlor.status & 0xF0) chlorErr = 16;
730
+ const chlorCollection = sys.chlorinators.getByBody(schem.activeBodyId);
731
+ if (chlorCollection.length > 0) {
732
+ for (let chlor of chlorCollection.toArray()) {
733
+ let schlor = state.chlorinators.getItemById(chlor.id);
734
+ if (schlor.status & 0xF0) {
735
+ chlorErr = 16;
736
+ break;
737
+ }
738
+ }
739
+ }
735
740
  }
736
741
  schem.warnings.chlorinatorCommError = chlorErr;
737
742
  schem.warnings.pHLockout = useChlorinator === false && probeType !== 0 && pumpType !== 0 && schem.ph.level >= chem.orp.phLockout ? 1 : 0;
@@ -1067,7 +1072,7 @@ class NixieChemical extends NixieChildEquipment implements INixieChemical {
1067
1072
  schem.chlor.isDosing = schem.pump.isDosing = false;
1068
1073
  if (!this.chemical.flowOnlyMixing || (schem.chemController.isBodyOn && this.chemController.flowDetected && !schem.freezeProtect)) {
1069
1074
  if (this.chemType === 'orp' && typeof this.chemController.orp.orp.useChlorinator !== 'undefined' && this.chemController.orp.orp.useChlorinator && this.chemController.orp.orp.chlorDosingMethod > 0) {
1070
- if (state.chlorinators.getItemById(this.chemController.orp.chlor.chlorId).currentOutput !== 0) {
1075
+ if (state.chlorinators.getItemById(this.chlor.id).currentOutput !== 0) {
1071
1076
  logger.debug(`Chem mixing ORP (chlorinator) paused waiting for chlor current output to be 0%. Mix time remaining: ${utils.formatDuration(schem.mixTimeRemaining)} `);
1072
1077
  return;
1073
1078
  }
@@ -1422,24 +1427,25 @@ export class NixieChemPump extends NixieChildEquipment {
1422
1427
  }
1423
1428
  }
1424
1429
  export class NixieChemChlor extends NixieChildEquipment {
1425
- public chlor: ChemicalChlor;
1426
- public isOn: boolean;
1427
- public chlorId = 0;
1430
+ public get chlor(): Chlorinator { return sys.chlorinators.getItemById((this.getParent() as NixieChemicalORP).orp.chlorId); }
1431
+ public isOn: boolean; // can this just be chlor.isOn?
1428
1432
  public _lastOnStatus: number;
1429
1433
  protected _dosingTimer: NodeJS.Timeout;
1430
1434
  private _isStopping = false;
1431
1435
  public chlorInterval = 15;
1432
- constructor(chemical: NixieChemical, chlor: ChemicalChlor) { super(chemical); this.chlor = chlor; }
1436
+ // constructor(chemical: NixieChemical, chlor: ChemicalChlor) { super(chemical); this.chlor = chlor; }
1437
+ constructor(chemical: NixieChemical) { super(chemical); }
1433
1438
  public get chemical(): NixieChemical { return this.getParent() as NixieChemical; }
1434
1439
  public async setChlorAsync(schlor: ChemicalChlorState, data: any) {
1435
1440
  try {
1436
1441
  if (typeof data.chlorDosingMethod !== 'undefined' && data.chlorDosingMethod === 0) {
1437
1442
  if (schlor.chemical.dosingStatus === 0) { await this.chemical.cancelDosing(schlor.chemController.orp, 'dosing method changed'); }
1438
1443
  if (schlor.chemical.dosingStatus === 1) { await this.chemical.cancelMixing(schlor.chemController.orp); }
1439
- let chlor = sys.chlorinators.getItemById(this.chlorId);
1444
+ let chlor = sys.chlorinators.getItemById((this.getParent() as NixieChemicalORP).orp.chlorId);
1440
1445
  chlor.disabled = false;
1441
1446
  chlor.isDosing = false;
1442
1447
  }
1448
+ let c = sys.chlorinators.toArray
1443
1449
  } catch (err) { logger.error(`setChlorAsync: ${err.message}`); return Promise.reject(err); }
1444
1450
  }
1445
1451
  public async stopDosing(schem: IChemicalState, reason: string): Promise<void> {
@@ -1479,7 +1485,8 @@ export class NixieChemChlor extends NixieChildEquipment {
1479
1485
  await this.chemical.cancelDosing(schem, 'undefined dose');
1480
1486
  return;
1481
1487
  }
1482
- if (this.chlor.ratedLbs === 0) {
1488
+ let chlor = sys.chlorinators.getItemById((this.getParent() as NixieChemicalORP).orp.chlorId);
1489
+ if (chlor.ratedLbs === 0) {
1483
1490
  // We aren't going to do anything.
1484
1491
  logger.verbose(`Chem dose ignore chlor because it doesn't have a dosing rating.`);
1485
1492
  }
@@ -1488,7 +1495,7 @@ export class NixieChemChlor extends NixieChildEquipment {
1488
1495
  let isBodyOn = schem.chemController.flowDetected;
1489
1496
  await this.chemical.initDose(schem);
1490
1497
  let chemController = schem.getParent()
1491
- let schlor = state.chlorinators.getItemById(this.chlorId);
1498
+ let schlor = state.chlorinators.getItemById(chlor.id);
1492
1499
  if (!isBodyOn) {
1493
1500
  // Make sure the chlor is off.
1494
1501
  logger.info(`Chem chlor flow not detected. Body is not running.`);
@@ -1501,7 +1508,7 @@ export class NixieChemChlor extends NixieChildEquipment {
1501
1508
  if (chem.ph.dosePriority)
1502
1509
  await this.chemical.cancelDosing(schem, 'ph dose priority');
1503
1510
  }
1504
- else if (this.chlor.superChlor) {
1511
+ else if (chlor.superChlor) {
1505
1512
  // if superchlor is active, it may be to boost the ORP and we should respect that
1506
1513
  await this.chemical.cancelDosing(schem, 'superchlor');
1507
1514
  }
@@ -1518,7 +1525,7 @@ export class NixieChemChlor extends NixieChildEquipment {
1518
1525
  else {
1519
1526
  if (typeof dose._lastLatch !== 'undefined') {
1520
1527
  let time = new Date().getTime() - (dose._lastLatch || new Date().getTime());
1521
- let vol = this.chlor.ratedLbs * time / 1000;
1528
+ let vol = chlor.ratedLbs * time / 1000;
1522
1529
  schem.appendDose(vol, time);
1523
1530
  }
1524
1531
  logger.info(`Chem Controller ${dose.chem} chlorinated ${Math.round(dose.volumeDosed * 1000000) / 1000000}lbs of ${Math.round(dose.volume * 1000000) / 1000000}lbs - ${utils.formatDuration(dose.timeRemaining)} remaining`);
@@ -1565,14 +1572,15 @@ export class NixieChemChlor extends NixieChildEquipment {
1565
1572
  public async turnOff(schem: IChemicalState): Promise<ChlorinatorState> {
1566
1573
  try {
1567
1574
  //logger.info(`Turning off the chlorinator`);
1568
- let chlor = sys.chlorinators.getItemById(this.chlorId);
1575
+ let chemORP = this.getParent() as NixieChemicalORP;
1576
+ let chlor = sys.chlorinators.getItemById(chemORP.orp.chlorId);
1569
1577
  let schlor = state.chlorinators.getItemById(chlor.id);
1570
1578
  if (schlor.currentOutput === 0 && schlor.targetOutput === 0 && !schlor.superChlor && chlor.disabled && !chlor.isDosing) {
1571
1579
  this.isOn = schem.chlor.isDosing = false;
1572
1580
  return schlor;
1573
1581
  }
1574
1582
  let cstate = await sys.board.chlorinator.setChlorAsync({
1575
- id: 1,
1583
+ id: chlor.id,
1576
1584
  disabled: true,
1577
1585
  isDosing: false
1578
1586
  })
@@ -1583,14 +1591,15 @@ export class NixieChemChlor extends NixieChildEquipment {
1583
1591
  }
1584
1592
  public async turnOn(schem: ChemicalState, latchTimeout?: number): Promise<ChlorinatorState> {
1585
1593
  try {
1586
- let chlor = sys.chlorinators.getItemById(this.chlorId);
1594
+ let chemORP = this.getParent() as NixieChemicalORP;
1595
+ let chlor = sys.chlorinators.getItemById(chemORP.orp.chlorId);
1587
1596
  let schlor = state.chlorinators.getItemById(chlor.id);
1588
1597
  if (schlor.currentOutput === 100 && schlor.targetOutput === 100 && !schlor.superChlor && !chlor.disabled && chlor.isDosing) {
1589
1598
  this.isOn = schem.chlor.isDosing = true;
1590
1599
  return schlor;
1591
1600
  }
1592
1601
  let cstate = await sys.board.chlorinator.setChlorAsync({
1593
- id: 1,
1602
+ id: chlor.id,
1594
1603
  disabled: false,
1595
1604
  isDosing: true
1596
1605
  })
@@ -1939,13 +1948,14 @@ export class NixieChemicalORP extends NixieChemical {
1939
1948
  this.chemType = 'orp';
1940
1949
  this.orp = chemical;
1941
1950
  this.probe = new NixieChemProbeORP(this, chemical.probe);
1942
- this.chlor = new NixieChemChlor(this, chemical.chlor);
1951
+ // this.chlor = new NixieChemChlor(this, chemical.chlor);
1952
+ this.chlor = new NixieChemChlor(this);
1943
1953
  let sorp = state.chemControllers.getItemById(controller.id).orp;
1944
1954
  if (!this.orp.enabled) {
1945
1955
  this.orp.doserType = 0;
1946
1956
  sorp.chemType = 'none';
1947
1957
  }
1948
- else if (sorp.useChlorinator) {
1958
+ else if (this.orp.useChlorinator) {
1949
1959
  this.orp.doserType = 2;
1950
1960
  sorp.chemType = 'chlorine';
1951
1961
  }
@@ -1962,14 +1972,38 @@ export class NixieChemicalORP extends NixieChemical {
1962
1972
  public async setORPAsync(sorp: ChemicalORPState, data: any) {
1963
1973
  try {
1964
1974
  if (typeof data !== 'undefined') {
1965
- sorp.useChlorinator = this.orp.useChlorinator = typeof data.useChlorinator !== 'undefined' ? utils.makeBool(data.useChlorinator) : this.orp.useChlorinator;
1975
+ this.orp.useChlorinator = typeof data.useChlorinator !== 'undefined' ? utils.makeBool(data.useChlorinator) : this.orp.useChlorinator;
1976
+ if (this.orp.useChlorinator) {
1977
+ if (typeof data.chlorId === 'undefined') {
1978
+ return Promise.reject(new InvalidEquipmentDataError(`Chlorinator ID must be provided when useChlorinator is true`, 'chemController', data.chlorId));
1979
+ }
1980
+ let chlor = sys.chlorinators.getItemById(data.chlorId);
1981
+ if (typeof chlor === 'undefined') {
1982
+ return Promise.reject(new InvalidEquipmentDataError(`Chlorinator with ID ${data.chlorId} not found`, 'chemController', data.chlorId));
1983
+ }
1984
+ if (chlor.body !== this.chemController.chem.body && chlor.body !== 32) {
1985
+ return Promise.reject(new InvalidEquipmentDataError(`Chlorinator body does not match the chem controller body`, 'chemController', data.chlorId));
1986
+ }
1987
+ let assignedChemController = sys.chemControllers.get().find((cc: ChemController) =>
1988
+ {
1989
+ return cc.orp.chlorId === data.chlorId && cc.id !== this.chemController.id;
1990
+ });
1991
+ if (assignedChemController) {
1992
+ return Promise.reject(new InvalidEquipmentDataError(`Chlorinator is already assigned to another chem controller`, 'chemController', data.chlorId));
1993
+ }
1994
+ this.orp.chlorId = data.chlorId;
1995
+ if (typeof data.chlorDosingMethod !== 'undefined') { this.orp.chlorDosingMethod = data.chlorDosingMethod; }
1996
+ } else {
1997
+ this.orp.chlorId = undefined;
1998
+ this.orp.chlorDosingMethod = undefined;
1999
+ }
1966
2000
  sorp.enabled = this.orp.enabled = typeof data.enabled !== 'undefined' ? utils.makeBool(data.enabled) : this.orp.enabled;
1967
2001
  sorp.level = typeof data.level !== 'undefined' && !isNaN(parseFloat(data.level)) ? parseFloat(data.level) : sorp.level;
1968
2002
  this.orp.phLockout = typeof data.phLockout !== 'undefined' && !isNaN(parseFloat(data.phLockout)) ? parseFloat(data.phLockout) : this.orp.phLockout;
1969
2003
  this.orp.flowReadingsOnly = typeof data.flowReadingsOnly !== 'undefined' ? utils.makeBool(data.flowReadingsOnly) : this.orp.flowReadingsOnly;
1970
2004
  this.orp.disableOnFreeze = typeof data.disableOnFreeze !== 'undefined' ? utils.makeBool(data.disableOnFreeze) : this.orp.disableOnFreeze;
1971
2005
  if (!this.orp.disableOnFreeze) sorp.freezeProtect = false;
1972
- if (typeof data.chlorDosingMethod !== 'undefined') { this.orp.chlorDosingMethod = data.chlorDosingMethod; }
2006
+
1973
2007
  await this.setDosing(this.orp, data);
1974
2008
  await this.setMixing(this.orp, data);
1975
2009
  await this.probe.setProbeORPAsync(sorp.probe, data.probe);
@@ -1980,7 +2014,7 @@ export class NixieChemicalORP extends NixieChemical {
1980
2014
  this.orp.doserType = 0;
1981
2015
  sorp.chemType = 'none';
1982
2016
  }
1983
- else if (sorp.useChlorinator) {
2017
+ else if (this.orp.useChlorinator) {
1984
2018
  this.orp.doserType = 2;
1985
2019
  sorp.chemType = 'chlorine';
1986
2020
  }
@@ -2069,7 +2103,7 @@ export class NixieChemicalORP extends NixieChemical {
2069
2103
 
2070
2104
  public async cancelDosing(sorp: ChemicalORPState, reason: string): Promise<void> {
2071
2105
  try {
2072
- if (typeof sorp.useChlorinator !== 'undefined' && sorp.useChlorinator && this.chemController.orp.orp.chlorDosingMethod > 0) {
2106
+ if (typeof this.orp.useChlorinator !== 'undefined' && this.orp.useChlorinator && this.chemController.orp.orp.chlorDosingMethod > 0) {
2073
2107
  await this.chlor.stopDosing(sorp, reason);
2074
2108
  // for chlor, we want 15 minute intervals
2075
2109
  if (sorp.doseHistory.length) {
@@ -2104,7 +2138,7 @@ export class NixieChemicalORP extends NixieChemical {
2104
2138
  if (typeof mixingTime !== 'undefined') {
2105
2139
  // This is a manual mix so we need to make sure the pump is not dosing.
2106
2140
  logger.info(`Clearing any possible ${schem.chemType} dosing or existing mix for mixingTime: ${mixingTime}`);
2107
- if (schem.chemController.orp.useChlorinator) await this.chlor.stopDosing(schem, 'mix override');
2141
+ if (this.orp.useChlorinator) await this.chlor.stopDosing(schem, 'mix override');
2108
2142
  else await this.pump.stopDosing(schem, 'mix override');
2109
2143
  await this.stopMixing(schem);
2110
2144
  }
@@ -2274,9 +2308,8 @@ export class NixieChemicalORP extends NixieChemical {
2274
2308
  return;
2275
2309
  }
2276
2310
  }
2277
-
2278
-
2279
- let chlor = sys.chlorinators.getItemById(this.chlor.chlorId); // Still haven't seen any systems with 2+ chlors
2311
+ let chlor = this.chlor.chlor; // Still haven't seen any systems with 2+ chlors.
2312
+ // 2024.12.25 RSG - Oh really? See https://github.com/tagyoureit/nodejs-poolController/discussions/896
2280
2313
  let schlor = state.chlorinators.getItemById(chlor.id);
2281
2314
  // If someone or something is superchloring the pool, let it be
2282
2315
  if (schlor.superChlor) return;
@@ -2429,7 +2462,7 @@ export class NixieChemicalORP extends NixieChemical {
2429
2462
  logger.info(`Removing chlor ${chlor.id} from Chem Controller ${this.getParent().id}`);
2430
2463
  let schem = state.chemControllers.getItemById(this.getParent().id);
2431
2464
  this.orp.useChlorinator = false;
2432
- schem.orp.useChlorinator = false;
2465
+ // schem.orp.useChlorinator = false;
2433
2466
  if (schem.orp.dosingStatus === 0) { await this.cancelDosing(schem.orp, 'deleting chlorinator'); }
2434
2467
  if (schem.orp.dosingStatus === 1) { await this.cancelMixing(schem.orp); }
2435
2468
  }
@@ -45,7 +45,7 @@ export class NixieCircuitCollection extends NixieEquipmentCollection<NixieCircui
45
45
  } catch (err) { return logger.error(`NCP: setServiceModeAsync: ${err.message}`); }
46
46
  }
47
47
  public async setLightThemeAsync(id: number, theme: any) {
48
- let c: NixieCircuit = this.find(elem => elem.id === id) as NixieCircuit;
48
+ let c: NixieCircuit = this.find(elem => elem.id === id) as NixieCircuit;
49
49
  if (typeof c === 'undefined') return Promise.reject(new Error(`NCP: Circuit ${id} could not be found to set light theme ${theme.name}.`));
50
50
  await c.setLightThemeAsync(theme);
51
51
  } catch(err) { return logger.error(`NCP: sendOnOffSequence: ${err.message}`); }
@@ -75,10 +75,10 @@ export class NixieCircuitCollection extends NixieEquipmentCollection<NixieCircui
75
75
  catch (err) { logger.error(`setCircuitAsync: ${err.message}`); return Promise.reject(err); }
76
76
  }
77
77
  public async checkCircuitEggTimerExpirationAsync(cstate: ICircuitState) {
78
- try {
79
- let c: NixieCircuit = this.find(elem => elem.id === cstate.id) as NixieCircuit;
80
- await c.checkCircuitEggTimerExpirationAsync(cstate);
81
- } catch (err) { logger.error(`NCP: Error synching circuit states: ${err}`); }
78
+ try {
79
+ let c: NixieCircuit = this.find(elem => elem.id === cstate.id) as NixieCircuit;
80
+ await c.checkCircuitEggTimerExpirationAsync(cstate);
81
+ } catch (err) { logger.error(`NCP: Error synching circuit states: ${err}`); }
82
82
  }
83
83
  public async initAsync(circuits: CircuitCollection) {
84
84
  try {
@@ -159,21 +159,21 @@ export class NixieCircuit extends NixieEquipment {
159
159
  protected async setIntelliBriteThemeAsync(cstate: CircuitState, theme: any): Promise<InterfaceServerResponse> {
160
160
  let arr = [];
161
161
  let count = typeof theme !== 'undefined' && theme.sequence ? theme.sequence : 0;
162
-
163
- // Removing this. No need to turn the light off first. We actually need it on to start the sequence for theme setting to work correctly when the light is starting from the off state.
164
- // if (cstate.isOn) arr.push({ isOn: false, timeout: 1000 });
165
-
166
- // Start the sequence of off/on after the light is on.
162
+
163
+ // Removing this. No need to turn the light off first. We actually need it on to start the sequence for theme setting to work correctly when the light is starting from the off state.
164
+ // if (cstate.isOn) arr.push({ isOn: false, timeout: 1000 });
165
+
166
+ // Start the sequence of off/on after the light is on.
167
167
  arr.push({ isOn: true, timeout: 100 });
168
168
  for (let i = 0; i < count; i++) {
169
- arr.push({ isOn: false, timeout: 100 });
170
- arr.push({ isOn: true, timeout: 100 });
169
+ arr.push({ isOn: false, timeout: 100 });
170
+ arr.push({ isOn: true, timeout: 100 });
171
171
  }
172
- // Ensure light stays on long enough for the theme to stick (required for light group theme setting to function correctly).
173
- // 2s was too short.
174
- arr.push({ isOn: true, timeout: 3000 });
175
-
176
- logger.debug(arr);
172
+ // Ensure light stays on long enough for the theme to stick (required for light group theme setting to function correctly).
173
+ // 2s was too short.
174
+ arr.push({ isOn: true, timeout: 3000 });
175
+
176
+ logger.debug(arr);
177
177
  let res = await NixieEquipment.putDeviceService(this.circuit.connectionId, `/state/device/${this.circuit.deviceBinding}`, arr, 60000);
178
178
  // Even though we ended with on we need to make sure that the relay stays on now that we are done.
179
179
  if (!res.error) {
@@ -182,7 +182,7 @@ export class NixieCircuit extends NixieEquipment {
182
182
  }
183
183
  return res;
184
184
  }
185
- protected async setWaterColorsThemeAsync(cstate: CircuitState, theme: any): Promise<InterfaceServerResponse> {
185
+ protected async setPoolToneThemeAsync(cstate: CircuitState, theme: any): Promise<InterfaceServerResponse> {
186
186
  let ptheme = sys.board.valueMaps.lightThemes.findItem(cstate.lightingTheme) || { val: 0, sequence: 0 };
187
187
  // First check to see if we are on. If we are not then we need to emit our status as if we are initializing and busy.
188
188
  let arr = [];
@@ -196,7 +196,40 @@ export class NixieCircuit extends NixieEquipment {
196
196
  let count = theme.sequence - ptheme.sequence;
197
197
  if (count < 0) count = count + 16;
198
198
  for (let i = 0; i < count; i++) {
199
- arr.push({ isOn: true, timeout: 200 });
199
+ arr.push({ isOn: true, timeout: 200 });
200
+ arr.push({ isOn: false, timeout: 200 });
201
+ }
202
+ console.log(arr);
203
+ if (arr.length === 0) return new InterfaceServerResponse(200, 'Success');
204
+ let res = await NixieEquipment.putDeviceService(this.circuit.connectionId, `/state/device/${this.circuit.deviceBinding}`, arr, 60000);
205
+ // Even though we ended with on we need to make sure that the relay stays on now that we are done.
206
+ if (!res.error) {
207
+ cstate.lightingTheme = ptheme.val;
208
+ cstate.isOn = true; // At this point the relay will be off but we want the process
209
+ // to assume that the relay state is not actually changing.
210
+ this._sequencing = false;
211
+ await this.setCircuitStateAsync(cstate, true, false);
212
+ }
213
+ return res;
214
+ }
215
+ protected async setWaterColorsThemeAsync(cstate: CircuitState, theme: any): Promise<InterfaceServerResponse> {
216
+ // RSG 2024.12.24 - This logic was aligned with the Pool Tone themes. I haven't checked if that
217
+ // logic is correct, but made a copy and adjusted for the watercolors themes.
218
+
219
+ let ptheme = sys.board.valueMaps.lightThemes.findItem(cstate.lightingTheme) || { val: 0, sequence: 0 };
220
+ // First check to see if we are on. If we are not then we need to emit our status as if we are initializing and busy.
221
+ let arr = [];
222
+ if (ptheme.val === 0) {
223
+ // We don't know our previous theme so we are going to sync the lights to get a starting point.
224
+ arr.push({ isOn: true, timeout: 1000 }); // Turn on for 1 second
225
+ arr.push({ isOn: false, timeout: 5000 }); // Turn off for 5 seconds
226
+ arr.push({ isOn: true, timeout: 1000 });
227
+ ptheme = sys.board.valueMaps.lightThemes.findItem('alpinewhite');
228
+ }
229
+ let count = theme.sequence - ptheme.sequence;
230
+ if (count < 0) count = count + 14;
231
+ for (let i = 0; i < count; i++) {
232
+ arr.push({ isOn: true, timeout: 200 });
200
233
  arr.push({ isOn: false, timeout: 200 });
201
234
  }
202
235
  console.log(arr);
@@ -206,7 +239,7 @@ export class NixieCircuit extends NixieEquipment {
206
239
  if (!res.error) {
207
240
  cstate.lightingTheme = ptheme.val;
208
241
  cstate.isOn = true; // At this point the relay will be off but we want the process
209
- // to assume that the relay state is not actually changing.
242
+ // to assume that the relay state is not actually changing.
210
243
  this._sequencing = false;
211
244
  await this.setCircuitStateAsync(cstate, true, false);
212
245
  }
@@ -243,7 +276,7 @@ export class NixieCircuit extends NixieEquipment {
243
276
  if (!res.error) {
244
277
  cstate.lightingTheme = ptheme.val;
245
278
  cstate.isOn = true; // At this point the relay will be off but we want the process
246
- // to assume that the relay state is not actually changing.
279
+ // to assume that the relay state is not actually changing.
247
280
  this._sequencing = false;
248
281
  await this.setCircuitStateAsync(cstate, true, false);
249
282
  }
@@ -264,7 +297,6 @@ export class NixieCircuit extends NixieEquipment {
264
297
  switch (type.name) {
265
298
  case 'colorcascade':
266
299
  case 'globrite':
267
- case 'pooltone':
268
300
  case 'magicstream':
269
301
  case 'intellibrite':
270
302
  res = await this.setIntelliBriteThemeAsync(cstate, theme);
@@ -275,6 +307,9 @@ export class NixieCircuit extends NixieEquipment {
275
307
  case 'watercolors':
276
308
  res = await this.setWaterColorsThemeAsync(cstate, theme);
277
309
  break;
310
+ case 'pooltone':
311
+ res = await this.setPoolToneThemeAsync(cstate, theme);
312
+ break;
278
313
  }
279
314
  cstate.action = 0;
280
315
  // Make sure clients know that we are done.
@@ -285,11 +320,11 @@ export class NixieCircuit extends NixieEquipment {
285
320
  }
286
321
  public async sendOnOffSequenceAsync(count: number | { isOn: boolean, timeout: number }[], timeout?: number): Promise<InterfaceServerResponse> {
287
322
  try {
288
-
323
+
289
324
  this._sequencing = true;
290
325
  let arr = [];
291
326
  let cstate = state.circuits.getItemById(this.circuit.id);
292
-
327
+
293
328
  if (typeof count === 'number') {
294
329
  if (cstate.isOn) arr.push({ isOn: false, timeout: 1000 });
295
330
  let t = typeof timeout === 'undefined' ? 100 : timeout;
@@ -339,21 +374,21 @@ export class NixieCircuit extends NixieEquipment {
339
374
  // Check to see if we should be on by poking the schedules.
340
375
  }
341
376
  if (utils.isNullOrEmpty(this.circuit.connectionId) || utils.isNullOrEmpty(this.circuit.deviceBinding)) {
342
- if (val && val !== cstate.isOn){
377
+ if (val && val !== cstate.isOn) {
343
378
  sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(cstate.id), cstate, val);
344
379
  }
345
- else if (!val){
380
+ else if (!val) {
346
381
  if (cstate.manualPriorityActive) delayMgr.cancelManualPriorityDelay(cstate.id);
347
382
  cstate.manualPriorityActive = false; // if the delay was previously cancelled, still need to turn this off
348
- }
349
- cstate.isOn = val;
383
+ }
384
+ cstate.isOn = val;
350
385
  return new InterfaceServerResponse(200, 'Success');
351
386
  }
352
387
  if (this._sequencing) return new InterfaceServerResponse(200, 'Success');
353
388
  let res = await NixieEquipment.putDeviceService(this.circuit.connectionId, `/state/device/${this.circuit.deviceBinding}`, { isOn: val, latch: val ? 10000 : undefined });
354
389
  if (res.status.code === 200) {
355
390
  // Set this up so we can process our egg timer.
356
- if (val && val !== cstate.isOn){
391
+ if (val && val !== cstate.isOn) {
357
392
  sys.board.circuits.setEndTime(sys.circuits.getInterfaceById(cstate.id), cstate, val);
358
393
  switch (sys.board.valueMaps.circuitFunctions.getName(this.circuit.type)) {
359
394
  case 'colorlogic':
@@ -385,7 +420,7 @@ export class NixieCircuit extends NixieEquipment {
385
420
  break;
386
421
  }
387
422
  }
388
- else if (!val){
423
+ else if (!val) {
389
424
  delayMgr.cancelManualPriorityDelays();
390
425
  cstate.manualPriorityActive = false; // if the delay was previously cancelled, still need to turn this off
391
426
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodejs-poolcontroller",
3
- "version": "8.0.5",
3
+ "version": "8.1.0",
4
4
  "description": "nodejs-poolController",
5
5
  "main": "app.js",
6
6
  "author": {
@@ -301,7 +301,8 @@ export class ConfigRoute {
301
301
  // waterFlow: sys.board.valueMaps.chemControllerWaterFlow.toArray(), // remove
302
302
  controllers: sys.chemControllers.get(),
303
303
  maxChemControllers: sys.equipment.maxChemControllers,
304
- doserTypes: sys.board.valueMaps.chemDoserTypes.toArray()
304
+ doserTypes: sys.board.valueMaps.chemDoserTypes.toArray(),
305
+ chlorinators: sys.chlorinators.get(),
305
306
  };
306
307
  return res.status(200).send(opts);
307
308
  }