nodejs-poolcontroller 8.4.0 → 8.4.1
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/.github/workflows/ghcr-publish.yml +1 -1
- package/157_issues.md +101 -0
- package/AGENTS.md +17 -1
- package/README.md +13 -2
- package/controller/Equipment.ts +49 -0
- package/controller/State.ts +8 -0
- package/controller/boards/AquaLinkBoard.ts +174 -2
- package/controller/boards/EasyTouchBoard.ts +44 -0
- package/controller/boards/IntelliCenterBoard.ts +360 -172
- package/controller/boards/NixieBoard.ts +7 -4
- package/controller/boards/SunTouchBoard.ts +1 -0
- package/controller/boards/SystemBoard.ts +39 -4
- package/controller/comms/Comms.ts +9 -3
- package/controller/comms/messages/Messages.ts +218 -24
- package/controller/comms/messages/config/EquipmentMessage.ts +34 -0
- package/controller/comms/messages/config/ExternalMessage.ts +1051 -989
- package/controller/comms/messages/config/GeneralMessage.ts +65 -0
- package/controller/comms/messages/config/OptionsMessage.ts +15 -2
- package/controller/comms/messages/config/PumpMessage.ts +427 -421
- package/controller/comms/messages/config/SecurityMessage.ts +37 -13
- package/controller/comms/messages/status/EquipmentStateMessage.ts +0 -218
- package/controller/comms/messages/status/HeaterStateMessage.ts +27 -15
- package/controller/comms/messages/status/NeptuneModbusStateMessage.ts +217 -0
- package/controller/comms/messages/status/VersionMessage.ts +67 -18
- package/controller/nixie/chemistry/ChemController.ts +65 -33
- package/controller/nixie/heaters/Heater.ts +10 -1
- package/controller/nixie/pumps/Pump.ts +145 -2
- package/docker-compose.yml +1 -0
- package/logger/Logger.ts +75 -64
- package/package.json +1 -1
- package/tsconfig.json +2 -1
- package/web/Server.ts +3 -1
- package/web/services/config/Config.ts +150 -1
- package/web/services/state/State.ts +21 -0
- package/web/services/state/StateSocket.ts +28 -0
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
type=sha
|
|
34
34
|
type=raw,value=latest,enable={{is_default_branch}}
|
|
35
35
|
labels: |
|
|
36
|
-
org.opencontainers.image.source
|
|
36
|
+
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
|
37
37
|
org.opencontainers.image.revision=${{ github.sha }}
|
|
38
38
|
org.opencontainers.image.title=njspc
|
|
39
39
|
org.opencontainers.image.description=Pentair pool controller bridge (GHCR image)
|
package/157_issues.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# IntelliCenter v3 Config Triage Summary
|
|
2
|
+
|
|
3
|
+
## Initial issues reported
|
|
4
|
+
|
|
5
|
+
- General -> Personal Information:
|
|
6
|
+
- City/State/Zip not syncing between dashPanel and WCP/OCP.
|
|
7
|
+
- Pool Alias/Owner/Zip values overwriting each other.
|
|
8
|
+
- Latitude blank in dashPanel; longitude not editable/syncing.
|
|
9
|
+
- General -> Timezone & Locality:
|
|
10
|
+
- Switching Internet/12h <-> Manual/24h caused bogus Pool/Spa setpoints and heat mode in dashPanel.
|
|
11
|
+
- General -> Delays:
|
|
12
|
+
- Delays not linking correctly.
|
|
13
|
+
- Missing v3 fields: Frz Cycle Time (min) and Frz Override (min).
|
|
14
|
+
- Manual Operation Priority in dashPanel does not clearly map to WCP.
|
|
15
|
+
- Sensor Calibration:
|
|
16
|
+
- Generally working; occasional +/-1 variance (likely timing/read cadence).
|
|
17
|
+
- Alerts/Security:
|
|
18
|
+
- Tabs empty.
|
|
19
|
+
- Loading stuck at 0%:
|
|
20
|
+
- Intermittent post-change loading hang (notably replay 157).
|
|
21
|
+
|
|
22
|
+
## What is fixed
|
|
23
|
+
|
|
24
|
+
- v3 Action 168 body temp/settings parsing hardened:
|
|
25
|
+
- Added handling for payload variants so Pool/Spa setpoint/heat-mode values are parsed correctly.
|
|
26
|
+
- Latitude/Longitude outbound mapping corrected:
|
|
27
|
+
- Fixed wrong latitude assignment target.
|
|
28
|
+
- Corrected byte math/order for lat/lon packet payload.
|
|
29
|
+
- Normalized coordinate assignment precision.
|
|
30
|
+
- General config refresh reliability improved:
|
|
31
|
+
- Forced v3 refresh path to include general category refresh to reduce stale General-tab data.
|
|
32
|
+
- Country field inbound parse width increased:
|
|
33
|
+
- Expanded from 16 to 32 bytes to avoid truncation/misalignment.
|
|
34
|
+
|
|
35
|
+
## What is not fully fixed yet
|
|
36
|
+
|
|
37
|
+
- Personal Information end-to-end mapping:
|
|
38
|
+
- Alias/Owner/City/State/Zip still need definitive byte-level inbound/outbound confirmation.
|
|
39
|
+
- Delays v3 fields:
|
|
40
|
+
- Frz Cycle Time (min) and Frz Override (min) offsets not yet identified.
|
|
41
|
+
- Manual Operation Priority mapping still unconfirmed.
|
|
42
|
+
- Alerts/Security mapping:
|
|
43
|
+
- Need packet-level evidence to confirm source actions/offsets and parser wiring.
|
|
44
|
+
- Loading stuck:
|
|
45
|
+
- Root cause identified as RS-485 comm instability + frequent v3 refresh retriggers under noisy conditions; mitigation implementation still pending.
|
|
46
|
+
|
|
47
|
+
## Root cause of loading stuck
|
|
48
|
+
|
|
49
|
+
- In replay 157, poolState ends with:
|
|
50
|
+
- status.name = "loading"
|
|
51
|
+
- percent = 0
|
|
52
|
+
- Same run shows elevated comm issues:
|
|
53
|
+
- unrecoverable collisions
|
|
54
|
+
- invalid packets
|
|
55
|
+
- outbound retries
|
|
56
|
+
- repeated config refresh triggers (ACK(168), ACK(184), piggyback)
|
|
57
|
+
- Net effect:
|
|
58
|
+
- refresh cycles are retriggered/churned during unstable traffic and can fail to settle cleanly.
|
|
59
|
+
|
|
60
|
+
## Information needed to finish remaining work
|
|
61
|
+
|
|
62
|
+
- Capture one setting change at a time (with timestamp notes), then wait a few seconds before next change.
|
|
63
|
+
|
|
64
|
+
### Personal Information
|
|
65
|
+
|
|
66
|
+
- Pool Alias
|
|
67
|
+
- Owner
|
|
68
|
+
- City
|
|
69
|
+
- State
|
|
70
|
+
- Zip
|
|
71
|
+
- Latitude
|
|
72
|
+
- Longitude
|
|
73
|
+
|
|
74
|
+
### Time/Locality
|
|
75
|
+
|
|
76
|
+
- 12/24-hour switch
|
|
77
|
+
- Internet/manual clock source
|
|
78
|
+
- DST toggle
|
|
79
|
+
- Time zone change
|
|
80
|
+
|
|
81
|
+
### Delays (especially v3-specific)
|
|
82
|
+
|
|
83
|
+
- Frz Cycle Time (min)
|
|
84
|
+
- Frz Override (min)
|
|
85
|
+
- Any setting suspected to map to Manual Operation Priority
|
|
86
|
+
|
|
87
|
+
### Alerts/Security
|
|
88
|
+
|
|
89
|
+
- Toggle each available option individually (if present on WCP/OCP)
|
|
90
|
+
|
|
91
|
+
### Packet evidence needed for each run
|
|
92
|
+
|
|
93
|
+
- Action 30 (config responses)
|
|
94
|
+
- Action 168 (external/config broadcasts)
|
|
95
|
+
- Related request/ack flow (Action 222, Action 1, piggyback trigger context)
|
|
96
|
+
|
|
97
|
+
## Current status
|
|
98
|
+
|
|
99
|
+
- DONE: High-confidence parsing and lat/lon write-path fixes applied.
|
|
100
|
+
- PENDING: Remaining fields require targeted packet captures for accurate mapping.
|
|
101
|
+
- PENDING: Loading-hang mitigation implementation.
|
package/AGENTS.md
CHANGED
|
@@ -123,6 +123,17 @@
|
|
|
123
123
|
**Generalization:** This applies to *anything* that is overridden by controller boards (IntelliCenter, EasyTouch, IntelliTouch, AquaLink, SunTouch, Nixie, etc.).
|
|
124
124
|
If you find yourself writing `if (sys.controllerType === ...)` inside `SystemBoard.ts`, that is almost always a design smell—create/extend the controller-specific command class in the appropriate board file and keep `SystemBoard.ts` purely generic.
|
|
125
125
|
|
|
126
|
+
### 7.1 Nixie Chemistry Relay UI Sync: Circuits, Not Features (CRITICAL)
|
|
127
|
+
**Rule:** For Nixie/REM chemistry relay status sync, treat **circuits** as the hardware-bound source of truth. Do **not** implement hardware binding logic on `Feature`.
|
|
128
|
+
|
|
129
|
+
- `Feature` is logical/virtual state in this context; relay bindings (`connectionId`, `deviceBinding`) belong on `Circuit`.
|
|
130
|
+
- When chemistry pump state changes (`NixieChemPump.turnOn/turnOff`), locate matching entries in `sys.circuits` by `connectionId` + `deviceBinding`.
|
|
131
|
+
- Reflect state in `state.circuits` (including `setEndTime` semantics) and emit changes; do not bind/sync through `state.features`.
|
|
132
|
+
- Avoid duplicate relay writes: if the chemistry code already sent the hardware command, sync in-memory circuit state instead of issuing an additional toggle command for that same relay.
|
|
133
|
+
- If the UI "Features" panel must show the relay, use a **circuit** with `showInFeatures=true` (presentation concern), not a bound `Feature`.
|
|
134
|
+
|
|
135
|
+
**Context:** Added after investigation/fix for discussion #1159 (acid feeder relay dot not reflecting REM dosing state in dashPanel).
|
|
136
|
+
|
|
126
137
|
## Analysis Patterns
|
|
127
138
|
|
|
128
139
|
### 6. Examine Working State, Not Just Failures
|
|
@@ -458,6 +469,11 @@ Always prefix with `#` and packet ID.
|
|
|
458
469
|
4. Look for request/response pairs (outbound `dir":"out"` → inbound `dir":"in"`)
|
|
459
470
|
5. **Always extract and display packet IDs** in any analysis output
|
|
460
471
|
|
|
472
|
+
**Replay caveat (API lines):**
|
|
473
|
+
- Some replay packet logs include captured API requests (for example `/app/config/stopPacketCapture` in `v3.008_replay.157_config1` and `v3.008_replay.160_config2`).
|
|
474
|
+
- Treat these as tooling/control-plane events, not protocol source-of-truth packets.
|
|
475
|
+
- If replaying API lines out-of-order causes app errors, classify separately from RS-485 protocol regressions.
|
|
476
|
+
|
|
461
477
|
**Decoding times (v3.004 big-endian):**
|
|
462
478
|
- Two bytes `[hi, lo]` → `hi * 256 + lo` = minutes since midnight
|
|
463
479
|
- Example: `[2, 33]` → `2*256 + 33 = 545` → 9:05 AM
|
|
@@ -592,6 +608,6 @@ Each protocol file should include:
|
|
|
592
608
|
|
|
593
609
|
---
|
|
594
610
|
|
|
595
|
-
**Last Updated:**
|
|
611
|
+
**Last Updated:** March 13, 2026
|
|
596
612
|
**Source:** nodejs-poolController IntelliCenter v3.004 compatibility work
|
|
597
613
|
|
package/README.md
CHANGED
|
@@ -100,6 +100,14 @@ Assuming you cloned the repo, the following are easy steps to get the latest ver
|
|
|
100
100
|
|
|
101
101
|
See the [wiki](https://github.com/tagyoureit/nodejs-poolController/wiki/Docker). Thanks @wurmr @andylippitt @emes.
|
|
102
102
|
|
|
103
|
+
#### Image channels (important)
|
|
104
|
+
|
|
105
|
+
The project has multiple image channels. `latest` only means latest within that specific channel.
|
|
106
|
+
|
|
107
|
+
* `ghcr.io/tagyoureit/njspc` - official controller image published from this repository's GitHub Actions (tracks upstream `master`).
|
|
108
|
+
* `msmi/nodejs-poolcontroller` - legacy Docker Hub controller image (can lag upstream).
|
|
109
|
+
* `ghcr.io/rstrouse/njspc-dash` - dashPanel image currently published separately.
|
|
110
|
+
|
|
103
111
|
### Docker Compose (Controller + Optional dashPanel UI)
|
|
104
112
|
|
|
105
113
|
Below is an example `docker-compose.yml` snippet showing this controller (`njspc`) and an OPTIONAL dashPanel UI service (`njspc-dash`). The dashPanel image is published separately; uncomment if you want a built-in web dashboard on port 5150.
|
|
@@ -107,7 +115,7 @@ Below is an example `docker-compose.yml` snippet showing this controller (`njspc
|
|
|
107
115
|
```yaml
|
|
108
116
|
services:
|
|
109
117
|
njspc:
|
|
110
|
-
image: ghcr.io/
|
|
118
|
+
image: ${NJSPC_IMAGE:-ghcr.io/tagyoureit/njspc}
|
|
111
119
|
container_name: njspc
|
|
112
120
|
restart: unless-stopped
|
|
113
121
|
environment:
|
|
@@ -137,7 +145,7 @@ services:
|
|
|
137
145
|
# user: "0:0"
|
|
138
146
|
|
|
139
147
|
njspc-dash:
|
|
140
|
-
image: ghcr.io/
|
|
148
|
+
image: ${NJSPC_DASH_IMAGE:-ghcr.io/rstrouse/njspc-dash}
|
|
141
149
|
container_name: njspc-dash
|
|
142
150
|
restart: unless-stopped
|
|
143
151
|
depends_on:
|
|
@@ -172,6 +180,9 @@ Quick start:
|
|
|
172
180
|
|
|
173
181
|
Notes:
|
|
174
182
|
* Provide either RS-485 device OR enable network (ScreenLogic) connection.
|
|
183
|
+
* `latest` is channel-specific; use image labels to verify exact code revision:
|
|
184
|
+
* `docker image inspect ghcr.io/tagyoureit/njspc:latest --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}'`
|
|
185
|
+
* `docker image inspect msmi/nodejs-poolcontroller:latest --format '{{ index .Config.Labels "git-commit" }}'`
|
|
175
186
|
* Coordinates env vars prevent heliotrope warnings before the panel reports location.
|
|
176
187
|
* Persistence (controller):
|
|
177
188
|
* `./server-config.json:/app/config.json` main runtime config. You can either:
|
package/controller/Equipment.ts
CHANGED
|
@@ -59,6 +59,7 @@ interface IPoolSystem {
|
|
|
59
59
|
remotes: RemoteCollection;
|
|
60
60
|
eggTimers: EggTimerCollection;
|
|
61
61
|
security: Security;
|
|
62
|
+
alerts: Alerts;
|
|
62
63
|
chemControllers: ChemControllerCollection;
|
|
63
64
|
board: SystemBoard;
|
|
64
65
|
updateControllerDateTimeAsync(
|
|
@@ -202,6 +203,7 @@ export class PoolSystem implements IPoolSystem {
|
|
|
202
203
|
this.lightGroups = new LightGroupCollection(this.data, 'lightGroups');
|
|
203
204
|
this.remotes = new RemoteCollection(this.data, 'remotes');
|
|
204
205
|
this.security = new Security(this.data, 'security');
|
|
206
|
+
this.alerts = new Alerts(this.data, 'alerts');
|
|
205
207
|
this.customNames = new CustomNameCollection(this.data, 'customNames');
|
|
206
208
|
this.eggTimers = new EggTimerCollection(this.data, 'eggTimers');
|
|
207
209
|
this.chemControllers = new ChemControllerCollection(this.data, 'chemControllers');
|
|
@@ -322,6 +324,7 @@ export class PoolSystem implements IPoolSystem {
|
|
|
322
324
|
this.remotes.clear(0);
|
|
323
325
|
this.schedules.clear(0);
|
|
324
326
|
this.security.clear();
|
|
327
|
+
this.alerts.clear();
|
|
325
328
|
this.valves.clear(0);
|
|
326
329
|
this.chemControllers.clear(0);
|
|
327
330
|
this.filters.clear(0);
|
|
@@ -376,6 +379,7 @@ export class PoolSystem implements IPoolSystem {
|
|
|
376
379
|
public lightGroups: LightGroupCollection;
|
|
377
380
|
public remotes: RemoteCollection;
|
|
378
381
|
public security: Security;
|
|
382
|
+
public alerts: Alerts;
|
|
379
383
|
public customNames: CustomNameCollection;
|
|
380
384
|
public chemControllers: ChemControllerCollection;
|
|
381
385
|
public chemDosers: ChemDoserCollection;
|
|
@@ -479,6 +483,8 @@ export class PoolSystem implements IPoolSystem {
|
|
|
479
483
|
remotes: self.data.remotes || [],
|
|
480
484
|
heaters: self.data.heaters || [],
|
|
481
485
|
covers: self.data.covers || [],
|
|
486
|
+
security: self.data.security || {},
|
|
487
|
+
alerts: self.data.alerts || {},
|
|
482
488
|
appVersion: self.data.appVersion || '0.0.0'
|
|
483
489
|
};
|
|
484
490
|
}
|
|
@@ -835,6 +841,8 @@ export class Options extends EqItem {
|
|
|
835
841
|
if (typeof this.data.heaterStartDelayTime === 'undefined') this.data.heaterStartDelayTime = 10;
|
|
836
842
|
if (typeof this.data.cleanerStartDelayTime === 'undefined') this.data.cleanerStartDelayTime = 300; // 5min
|
|
837
843
|
if (typeof this.data.cleanerSolarDelayTime === 'undefined') this.data.cleanerSolarDelayTime = 300; // 5min
|
|
844
|
+
if (typeof this.data.freezeCycleTime === 'undefined') this.data.freezeCycleTime = 15;
|
|
845
|
+
if (typeof this.data.freezeOverride === 'undefined') this.data.freezeOverride = 30;
|
|
838
846
|
}
|
|
839
847
|
public get clockMode(): number | any { return this.data.clockMode; }
|
|
840
848
|
public set clockMode(val: number | any) { this.setDataVal('clockMode', sys.board.valueMaps.clockModes.encode(val)); }
|
|
@@ -857,6 +865,10 @@ export class Options extends EqItem {
|
|
|
857
865
|
public set cooldownDelay(val: boolean) { this.setDataVal('cooldownDelay', val); }
|
|
858
866
|
public get freezeThreshold(): number { return this.data.freezeThreshold; }
|
|
859
867
|
public set freezeThreshold(val: number) { this.setDataVal('freezeThreshold', val); }
|
|
868
|
+
public get freezeCycleTime(): number { return this.data.freezeCycleTime; }
|
|
869
|
+
public set freezeCycleTime(val: number) { this.setDataVal('freezeCycleTime', val); }
|
|
870
|
+
public get freezeOverride(): number { return this.data.freezeOverride; }
|
|
871
|
+
public set freezeOverride(val: number) { this.setDataVal('freezeOverride', val); }
|
|
860
872
|
public get heaterStartDelay(): boolean { return this.data.heaterStartDelay; }
|
|
861
873
|
public set heaterStartDelay(val: boolean) { this.setDataVal('heaterStartDelay', val); }
|
|
862
874
|
public get heaterStartDelayTime(): number { return this.data.heaterStartDelayTime; }
|
|
@@ -1453,6 +1465,7 @@ export interface ICircuit {
|
|
|
1453
1465
|
freeze?: boolean;
|
|
1454
1466
|
isActive: boolean;
|
|
1455
1467
|
lightingTheme?: number;
|
|
1468
|
+
level?: number;
|
|
1456
1469
|
//showInCircuits?: boolean;
|
|
1457
1470
|
showInFeatures?: boolean;
|
|
1458
1471
|
macro?: boolean;
|
|
@@ -2173,13 +2186,46 @@ export class SecurityRole extends EqItem {
|
|
|
2173
2186
|
public set flag2(val: number) { this.setDataVal('flag2', val); }
|
|
2174
2187
|
public get pin(): string { return this.data.pin; }
|
|
2175
2188
|
public set pin(val: string) { this.setDataVal('pin', val); }
|
|
2189
|
+
public get permissionsMask(): number { return this.data.permissionsMask; }
|
|
2190
|
+
public set permissionsMask(val: number) { this.setDataVal('permissionsMask', val); }
|
|
2191
|
+
public get permissionsBytes(): number[] { return Array.isArray(this.data.permissionsBytes) ? this.data.permissionsBytes : []; }
|
|
2192
|
+
public set permissionsBytes(val: number[]) { this.setDataVal('permissionsBytes', Array.isArray(val) ? [...val] : []); }
|
|
2176
2193
|
}
|
|
2177
2194
|
export class Security extends EqItem {
|
|
2178
2195
|
public dataName = 'securityConfig';
|
|
2196
|
+
public initData() {
|
|
2197
|
+
if (typeof this.data.enabled === 'undefined') this.data.enabled = false;
|
|
2198
|
+
if (typeof this.data.enabledByte === 'undefined') this.data.enabledByte = 0;
|
|
2199
|
+
}
|
|
2179
2200
|
public get enabled(): boolean { return this.data.enabled; }
|
|
2180
2201
|
public set enabled(val: boolean) { this.setDataVal('enabled', val); }
|
|
2202
|
+
public get enabledByte(): number { return this.data.enabledByte; }
|
|
2203
|
+
public set enabledByte(val: number) { this.setDataVal('enabledByte', val); }
|
|
2181
2204
|
public get roles(): SecurityRoleCollection { return new SecurityRoleCollection(this.data, "roles"); }
|
|
2182
2205
|
}
|
|
2206
|
+
export class Alerts extends EqItem {
|
|
2207
|
+
public dataName = 'alertsConfig';
|
|
2208
|
+
public initData() {
|
|
2209
|
+
if (typeof this.data.circuitNotifications === 'undefined') this.data.circuitNotifications = 0;
|
|
2210
|
+
if (typeof this.data.pumpNotifications === 'undefined') this.data.pumpNotifications = 0;
|
|
2211
|
+
if (typeof this.data.heaterNotifications === 'undefined') this.data.heaterNotifications = 0;
|
|
2212
|
+
if (typeof this.data.chlorinatorNotifications === 'undefined') this.data.chlorinatorNotifications = 0;
|
|
2213
|
+
if (typeof this.data.raw !== 'object' || this.data.raw === null) this.data.raw = {};
|
|
2214
|
+
}
|
|
2215
|
+
public get circuitNotifications(): number { return this.data.circuitNotifications; }
|
|
2216
|
+
public set circuitNotifications(val: number) { this.setDataVal('circuitNotifications', val); }
|
|
2217
|
+
public get pumpNotifications(): number { return this.data.pumpNotifications; }
|
|
2218
|
+
public set pumpNotifications(val: number) { this.setDataVal('pumpNotifications', val); }
|
|
2219
|
+
public get heaterNotifications(): number { return this.data.heaterNotifications; }
|
|
2220
|
+
public set heaterNotifications(val: number) { this.setDataVal('heaterNotifications', val); }
|
|
2221
|
+
public get chlorinatorNotifications(): number { return this.data.chlorinatorNotifications; }
|
|
2222
|
+
public set chlorinatorNotifications(val: number) { this.setDataVal('chlorinatorNotifications', val); }
|
|
2223
|
+
public setRaw(selector: number, raw: number[]) {
|
|
2224
|
+
if (typeof this.data.raw !== 'object' || this.data.raw === null) this.data.raw = {};
|
|
2225
|
+
this.data.raw[`selector${selector}`] = Array.isArray(raw) ? [...raw] : [];
|
|
2226
|
+
this.hasChanged = true;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2183
2229
|
export class ChemControllerCollection extends EqItemCollection<ChemController> {
|
|
2184
2230
|
constructor(data: any, name?: string) { super(data, name || "chemControllers"); }
|
|
2185
2231
|
public createItem(data: any): ChemController { return new ChemController(data); }
|
|
@@ -2238,6 +2284,7 @@ export class ChemController extends EqItem implements IChemController {
|
|
|
2238
2284
|
if (typeof this.data.lsiRange === 'undefined') this.data.lsiRange = { low: -.5, high: .5, enabled: true };
|
|
2239
2285
|
if (typeof this.data.borates === 'undefined') this.data.borates = 0;
|
|
2240
2286
|
if (typeof this.data.siCalcType === 'undefined') this.data.siCalcType = 0;
|
|
2287
|
+
if (typeof this.data.intellichemStandalone === 'undefined') this.data.intellichemStandalone = false;
|
|
2241
2288
|
super.initData();
|
|
2242
2289
|
}
|
|
2243
2290
|
public dataName = 'chemControllerConfig';
|
|
@@ -2253,6 +2300,8 @@ export class ChemController extends EqItem implements IChemController {
|
|
|
2253
2300
|
public set address(val: number) { this.setDataVal('address', val); }
|
|
2254
2301
|
public get isActive(): boolean { return this.data.isActive; }
|
|
2255
2302
|
public set isActive(val: boolean) { this.setDataVal('isActive', val); }
|
|
2303
|
+
public get intellichemStandalone(): boolean { return utils.makeBool(this.data.intellichemStandalone); }
|
|
2304
|
+
public set intellichemStandalone(val: boolean) { this.setDataVal('intellichemStandalone', val); }
|
|
2256
2305
|
public get calciumHardness(): number { return this.data.calciumHardness; }
|
|
2257
2306
|
public set calciumHardness(val: number) { this.setDataVal('calciumHardness', val); }
|
|
2258
2307
|
public get cyanuricAcid(): number { return this.data.cyanuricAcid; }
|
package/controller/State.ts
CHANGED
|
@@ -542,6 +542,8 @@ export interface ICircuitState {
|
|
|
542
542
|
get(bCopy?: boolean);
|
|
543
543
|
showInFeatures?: boolean;
|
|
544
544
|
isActive?: boolean;
|
|
545
|
+
level?: number;
|
|
546
|
+
color?: { red: number; green: number; blue: number };
|
|
545
547
|
startDelay?: boolean;
|
|
546
548
|
stopDelay?: boolean;
|
|
547
549
|
manualPriorityActive?: boolean;
|
|
@@ -1090,6 +1092,7 @@ export class PumpState extends EqState {
|
|
|
1090
1092
|
case 'vssvrs':
|
|
1091
1093
|
case 'vs':
|
|
1092
1094
|
case 'regalmodbus':
|
|
1095
|
+
case 'neptunemodbus':
|
|
1093
1096
|
c.units = sys.board.valueMaps.pumpUnits.transformByName('rpm');
|
|
1094
1097
|
break;
|
|
1095
1098
|
case 'ss':
|
|
@@ -2073,6 +2076,11 @@ export class CircuitState extends EqState implements ICircuitState {
|
|
|
2073
2076
|
}
|
|
2074
2077
|
public get level(): number { return this.data.level; }
|
|
2075
2078
|
public set level(val: number) { this.setDataVal('level', val); }
|
|
2079
|
+
public get color(): { red: number; green: number; blue: number } { return this.data.color; }
|
|
2080
|
+
public set color(val: { red: number; green: number; blue: number }) {
|
|
2081
|
+
if (typeof val === 'undefined' || val === null) this.setDataVal('color', undefined);
|
|
2082
|
+
else this.setDataVal('color', { red: val.red, green: val.green, blue: val.blue });
|
|
2083
|
+
}
|
|
2076
2084
|
public get commStatus(): number { return this.data.commStatus; }
|
|
2077
2085
|
public set commStatus(val: number) {
|
|
2078
2086
|
if (this.commStatus !== val) {
|
|
@@ -26,6 +26,16 @@ import { conn } from '../comms/Comms';
|
|
|
26
26
|
import { ncp } from "../nixie/Nixie";
|
|
27
27
|
import { utils } from '../Constants';
|
|
28
28
|
|
|
29
|
+
const IAQUALINK_SENDCMD_DEST = 0x00;
|
|
30
|
+
const IAQUALINK_SENDCMD_SOURCE = 0x24;
|
|
31
|
+
const IAQUALINK_SENDCMD_ACTION = 0x73;
|
|
32
|
+
const IAQUALINK_SENDCMD_PREFIX = 0x01;
|
|
33
|
+
const IAQUALINK_SENDCMD_PAYLOAD_LEN = 12;
|
|
34
|
+
const IAQUALINK_OPCODE_THEME = 0x61;
|
|
35
|
+
const IAQUALINK_OPCODE_RGB = 0x63;
|
|
36
|
+
const IAQUALINK_OPCODE_SYNC = 0x65;
|
|
37
|
+
const IAQUALINK_WATERCOLORS_CIRCUIT_TYPE = 18;
|
|
38
|
+
|
|
29
39
|
export class AquaLinkBoard extends SystemBoard {
|
|
30
40
|
constructor(system: PoolSystem) {
|
|
31
41
|
super(system);
|
|
@@ -41,6 +51,32 @@ export class AquaLinkBoard extends SystemBoard {
|
|
|
41
51
|
[32, { name: 'IT5X', part: 'i5X', desc: 'IntelliTouch i5X', circuits: 5 }],
|
|
42
52
|
[33, { name: 'IT10X', part: 'i10X', desc: 'IntelliTouch i10X', circuits: 10 }]
|
|
43
53
|
]);
|
|
54
|
+
this.valueMaps.circuitFunctions.merge([
|
|
55
|
+
[IAQUALINK_WATERCOLORS_CIRCUIT_TYPE, {
|
|
56
|
+
name: 'watercolors',
|
|
57
|
+
desc: 'Infinite WaterColors',
|
|
58
|
+
isLight: true,
|
|
59
|
+
theme: 'watercolors',
|
|
60
|
+
supportsBrightness: true,
|
|
61
|
+
supportsCustomColor: true
|
|
62
|
+
}]
|
|
63
|
+
]);
|
|
64
|
+
this.valueMaps.lightThemes.merge([
|
|
65
|
+
[56, { name: 'alpinewhite', desc: 'Alpine White', types: ['watercolors'], sequence: 1 }],
|
|
66
|
+
[57, { name: 'skyblue', desc: 'Sky Blue', types: ['watercolors'], sequence: 2 }],
|
|
67
|
+
[58, { name: 'cobaltblue', desc: 'Cobalt Blue', types: ['watercolors'], sequence: 3 }],
|
|
68
|
+
[59, { name: 'caribbeanblue', desc: 'Caribbean Blue', types: ['watercolors'], sequence: 4 }],
|
|
69
|
+
[60, { name: 'springgreen', desc: 'Spring Green', types: ['watercolors'], sequence: 5 }],
|
|
70
|
+
[61, { name: 'emeraldgreen', desc: 'Emerald Green', types: ['watercolors'], sequence: 6 }],
|
|
71
|
+
[62, { name: 'emeraldrose', desc: 'Emerald Rose', types: ['watercolors'], sequence: 7 }],
|
|
72
|
+
[63, { name: 'magenta', desc: 'Magenta', types: ['watercolors'], sequence: 8 }],
|
|
73
|
+
[64, { name: 'violet', desc: 'Violet', types: ['watercolors'], sequence: 9 }],
|
|
74
|
+
[65, { name: 'slowcolorsplash', desc: 'Slow Color Splash', types: ['watercolors'], sequence: 10 }],
|
|
75
|
+
[66, { name: 'fastcolorsplash', desc: 'Fast Color Splash', types: ['watercolors'], sequence: 11 }],
|
|
76
|
+
[67, { name: 'americathebeautiful', desc: 'America Beautiful', types: ['watercolors'], sequence: 12 }],
|
|
77
|
+
[68, { name: 'fattuesday', desc: 'Fat Tuesday', types: ['watercolors'], sequence: 13 }],
|
|
78
|
+
[69, { name: 'discotech', desc: 'Disco Tech', types: ['watercolors'], sequence: 14 }]
|
|
79
|
+
]);
|
|
44
80
|
}
|
|
45
81
|
public initExpansionModules(byte1: number, byte2: number) {
|
|
46
82
|
console.log(`Jandy AquaLink System Detected!`);
|
|
@@ -401,6 +437,59 @@ class AquaLinkBodyCommands extends BodyCommands {
|
|
|
401
437
|
}
|
|
402
438
|
}
|
|
403
439
|
class AquaLinkCircuitCommands extends CircuitCommands {
|
|
440
|
+
private isWaterColorsCircuit(circuit: ICircuit): boolean {
|
|
441
|
+
const type = sys.board.valueMaps.circuitFunctions.transform(circuit.type);
|
|
442
|
+
return typeof type !== 'undefined' && type.isLight === true && type.theme === 'watercolors';
|
|
443
|
+
}
|
|
444
|
+
private getWaterColorsTheme(theme: number | any) {
|
|
445
|
+
const thm = sys.board.valueMaps.lightThemes.findItem(theme);
|
|
446
|
+
if (typeof thm === 'undefined' || !Array.isArray(thm.types) || !thm.types.includes('watercolors')) {
|
|
447
|
+
throw new InvalidOperationError(`Theme ${theme} is not valid for Infinite WaterColors`, 'setLightThemeAsync');
|
|
448
|
+
}
|
|
449
|
+
return thm;
|
|
450
|
+
}
|
|
451
|
+
private normalizeWaterColorsBrightness(level: number): number {
|
|
452
|
+
const parsed = parseInt(level as any, 10);
|
|
453
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 100) {
|
|
454
|
+
throw new InvalidEquipmentDataError(`Invalid WaterColors brightness ${level}`, 'Circuit', level);
|
|
455
|
+
}
|
|
456
|
+
return parsed;
|
|
457
|
+
}
|
|
458
|
+
private normalizeWaterColorsComponent(value: number, component: string): number {
|
|
459
|
+
const parsed = parseInt(value as any, 10);
|
|
460
|
+
if (isNaN(parsed) || parsed < 0 || parsed > 255) {
|
|
461
|
+
throw new InvalidEquipmentDataError(`Invalid WaterColors ${component} component ${value}`, 'Circuit', value);
|
|
462
|
+
}
|
|
463
|
+
return parsed;
|
|
464
|
+
}
|
|
465
|
+
private async sendIAquaLinkLightCommandAsync(opcode: number, payload: number[], scope: string): Promise<void> {
|
|
466
|
+
const cmdPayload = payload.slice(0, IAQUALINK_SENDCMD_PAYLOAD_LEN);
|
|
467
|
+
while (cmdPayload.length < IAQUALINK_SENDCMD_PAYLOAD_LEN) cmdPayload.push(0);
|
|
468
|
+
await Outbound.create({
|
|
469
|
+
protocol: Protocol.AquaLink,
|
|
470
|
+
dest: IAQUALINK_SENDCMD_DEST,
|
|
471
|
+
source: IAQUALINK_SENDCMD_SOURCE,
|
|
472
|
+
action: IAQUALINK_SENDCMD_ACTION,
|
|
473
|
+
payload: [IAQUALINK_SENDCMD_PREFIX, opcode, ...cmdPayload],
|
|
474
|
+
retries: 2,
|
|
475
|
+
scope
|
|
476
|
+
}).sendAsync();
|
|
477
|
+
}
|
|
478
|
+
private async sendWaterColorsOnCommandAsync(id: number): Promise<void> {
|
|
479
|
+
await this.sendIAquaLinkLightCommandAsync(IAQUALINK_OPCODE_SYNC, [0x01, 0x00], `aqualink-watercolors-on-${id}`);
|
|
480
|
+
}
|
|
481
|
+
private async sendWaterColorsOffCommandAsync(id: number): Promise<void> {
|
|
482
|
+
await this.sendIAquaLinkLightCommandAsync(IAQUALINK_OPCODE_THEME, [0x00, 0xFF, 0x00], `aqualink-watercolors-off-${id}`);
|
|
483
|
+
}
|
|
484
|
+
private setWaterColorsState(circuit: ICircuit, cstate: any, isOn: boolean): void {
|
|
485
|
+
sys.board.circuits.setEndTime(circuit, cstate, isOn);
|
|
486
|
+
cstate.isOn = isOn;
|
|
487
|
+
}
|
|
488
|
+
private getWaterColorsLevel(circuit: ICircuit, cstate: any): number {
|
|
489
|
+
if (typeof circuit.level !== 'undefined' && circuit.level > 0) return circuit.level;
|
|
490
|
+
if (typeof cstate.level !== 'undefined' && cstate.level > 0) return cstate.level;
|
|
491
|
+
return 100;
|
|
492
|
+
}
|
|
404
493
|
public async setCircuitAsync(data: any): Promise<ICircuit> {
|
|
405
494
|
try {
|
|
406
495
|
let id = parseInt(data.id, 10);
|
|
@@ -413,6 +502,7 @@ class AquaLinkCircuitCommands extends CircuitCommands {
|
|
|
413
502
|
return circ;
|
|
414
503
|
}
|
|
415
504
|
let typeByte = parseInt(data.type, 10) || circuit.type || sys.board.valueMaps.circuitFunctions.getValue('generic');
|
|
505
|
+
this.assertSinglePoolSpaType(id, typeByte);
|
|
416
506
|
let nameByte = 3; // set default `Aux 1`
|
|
417
507
|
if (typeof data.nameId !== 'undefined') nameByte = data.nameId;
|
|
418
508
|
else if (typeof circuit.name !== 'undefined') nameByte = circuit.nameId;
|
|
@@ -445,6 +535,19 @@ class AquaLinkCircuitCommands extends CircuitCommands {
|
|
|
445
535
|
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Circuit or Feature id not valid', id, 'Circuit'));
|
|
446
536
|
let c = sys.circuits.getInterfaceById(id);
|
|
447
537
|
if (c.master !== 0) return await super.setCircuitStateAsync(id, val);
|
|
538
|
+
if (this.isWaterColorsCircuit(c)) {
|
|
539
|
+
const cstate = state.circuits.getInterfaceById(id);
|
|
540
|
+
if (val) await this.sendWaterColorsOnCommandAsync(id);
|
|
541
|
+
else await this.sendWaterColorsOffCommandAsync(id);
|
|
542
|
+
this.setWaterColorsState(c, cstate, val);
|
|
543
|
+
if (val) {
|
|
544
|
+
const level = this.getWaterColorsLevel(c, cstate);
|
|
545
|
+
c.level = level;
|
|
546
|
+
cstate.level = level;
|
|
547
|
+
}
|
|
548
|
+
state.emitEquipmentChanges();
|
|
549
|
+
return cstate;
|
|
550
|
+
}
|
|
448
551
|
if (id === 192 || c.type === 3) return await sys.board.circuits.setLightGroupThemeAsync(id - 191, val ? 1 : 0);
|
|
449
552
|
if (id >= 192) return await sys.board.circuits.setCircuitGroupStateAsync(id, val);
|
|
450
553
|
|
|
@@ -507,8 +610,77 @@ class AquaLinkCircuitCommands extends CircuitCommands {
|
|
|
507
610
|
});
|
|
508
611
|
}
|
|
509
612
|
public async setLightThemeAsync(id: number, theme: number): Promise<ICircuitState> {
|
|
510
|
-
|
|
511
|
-
|
|
613
|
+
const circ = sys.circuits.getItemById(id);
|
|
614
|
+
if (!this.isWaterColorsCircuit(circ)) {
|
|
615
|
+
// Re-route this as we cannot set individual circuit themes in *Touch.
|
|
616
|
+
return this.setLightGroupThemeAsync(id, theme);
|
|
617
|
+
}
|
|
618
|
+
const cstate = state.circuits.getItemById(id);
|
|
619
|
+
const thm = this.getWaterColorsTheme(theme);
|
|
620
|
+
const nop = sys.board.valueMaps.circuitActions.getValue('lighttheme');
|
|
621
|
+
cstate.action = nop;
|
|
622
|
+
cstate.emitEquipmentChange();
|
|
623
|
+
try {
|
|
624
|
+
if (!cstate.isOn) await this.sendWaterColorsOnCommandAsync(id);
|
|
625
|
+
await this.sendIAquaLinkLightCommandAsync(IAQUALINK_OPCODE_THEME, [0x00, thm.sequence, 0x64], `aqualink-watercolors-theme-${id}`);
|
|
626
|
+
this.setWaterColorsState(circ, cstate, true);
|
|
627
|
+
const level = this.getWaterColorsLevel(circ, cstate);
|
|
628
|
+
circ.level = level;
|
|
629
|
+
cstate.level = level;
|
|
630
|
+
circ.lightingTheme = thm.val;
|
|
631
|
+
cstate.lightingTheme = thm.val;
|
|
632
|
+
cstate.color = undefined;
|
|
633
|
+
state.emitEquipmentChanges();
|
|
634
|
+
return cstate;
|
|
635
|
+
}
|
|
636
|
+
catch (err) { return Promise.reject(new InvalidOperationError(err.message, 'setLightThemeAsync')); }
|
|
637
|
+
finally { cstate.action = 0; cstate.emitEquipmentChange(); }
|
|
638
|
+
}
|
|
639
|
+
public async setDimmerLevelAsync(id: number, level: number): Promise<ICircuitState> {
|
|
640
|
+
const circ = sys.circuits.getItemById(id);
|
|
641
|
+
if (!this.isWaterColorsCircuit(circ)) return super.setDimmerLevelAsync(id, level);
|
|
642
|
+
const cstate = state.circuits.getItemById(id);
|
|
643
|
+
const brightness = this.normalizeWaterColorsBrightness(level);
|
|
644
|
+
const nop = sys.board.valueMaps.circuitActions.getValue('lighttheme');
|
|
645
|
+
cstate.action = nop;
|
|
646
|
+
cstate.emitEquipmentChange();
|
|
647
|
+
try {
|
|
648
|
+
if (brightness > 0 && !cstate.isOn) await this.sendWaterColorsOnCommandAsync(id);
|
|
649
|
+
await this.sendIAquaLinkLightCommandAsync(IAQUALINK_OPCODE_THEME, [0x00, 0xFF, brightness], `aqualink-watercolors-brightness-${id}`);
|
|
650
|
+
circ.level = brightness;
|
|
651
|
+
cstate.level = brightness;
|
|
652
|
+
this.setWaterColorsState(circ, cstate, brightness > 0);
|
|
653
|
+
state.emitEquipmentChanges();
|
|
654
|
+
return cstate;
|
|
655
|
+
}
|
|
656
|
+
catch (err) { return Promise.reject(new InvalidOperationError(err.message, 'setDimmerLevelAsync')); }
|
|
657
|
+
finally { cstate.action = 0; cstate.emitEquipmentChange(); }
|
|
658
|
+
}
|
|
659
|
+
public async setLightColorAsync(id: number, color: { red: number; green: number; blue: number }): Promise<ICircuitState> {
|
|
660
|
+
const circ = sys.circuits.getItemById(id);
|
|
661
|
+
if (!this.isWaterColorsCircuit(circ)) return super.setLightColorAsync(id, color);
|
|
662
|
+
const cstate = state.circuits.getItemById(id);
|
|
663
|
+
const red = this.normalizeWaterColorsComponent(color.red, 'red');
|
|
664
|
+
const green = this.normalizeWaterColorsComponent(color.green, 'green');
|
|
665
|
+
const blue = this.normalizeWaterColorsComponent(color.blue, 'blue');
|
|
666
|
+
const nop = sys.board.valueMaps.circuitActions.getValue('lighttheme');
|
|
667
|
+
cstate.action = nop;
|
|
668
|
+
cstate.emitEquipmentChange();
|
|
669
|
+
try {
|
|
670
|
+
if (!cstate.isOn) await this.sendWaterColorsOnCommandAsync(id);
|
|
671
|
+
await this.sendIAquaLinkLightCommandAsync(IAQUALINK_OPCODE_RGB, [0x00, red, green, blue, 0x00], `aqualink-watercolors-rgb-${id}`);
|
|
672
|
+
const level = this.getWaterColorsLevel(circ, cstate);
|
|
673
|
+
circ.level = level;
|
|
674
|
+
cstate.level = level;
|
|
675
|
+
this.setWaterColorsState(circ, cstate, true);
|
|
676
|
+
circ.lightingTheme = sys.board.valueMaps.lightThemes.getValue('none');
|
|
677
|
+
cstate.lightingTheme = sys.board.valueMaps.lightThemes.getValue('none');
|
|
678
|
+
cstate.color = { red, green, blue };
|
|
679
|
+
state.emitEquipmentChanges();
|
|
680
|
+
return cstate;
|
|
681
|
+
}
|
|
682
|
+
catch (err) { return Promise.reject(new InvalidOperationError(err.message, 'setLightColorAsync')); }
|
|
683
|
+
finally { cstate.action = 0; cstate.emitEquipmentChange(); }
|
|
512
684
|
}
|
|
513
685
|
public async runLightGroupCommandAsync(obj: any): Promise<ICircuitState> {
|
|
514
686
|
// Do all our validation.
|
|
@@ -1218,6 +1218,26 @@ class TouchSystemCommands extends SystemCommands {
|
|
|
1218
1218
|
}
|
|
1219
1219
|
}
|
|
1220
1220
|
class TouchBodyCommands extends BodyCommands {
|
|
1221
|
+
public getHeatSources(bodyId: number) {
|
|
1222
|
+
let heatSources = super.getHeatSources(bodyId);
|
|
1223
|
+
for (let i = 0; i < heatSources.length; i++) {
|
|
1224
|
+
let hm = heatSources[i];
|
|
1225
|
+
if (hm?.name === 'ultratemp' && typeof hm.val === 'undefined') {
|
|
1226
|
+
heatSources[i] = this.board.valueMaps.heatSources.transformByName('heatpump');
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return heatSources;
|
|
1230
|
+
}
|
|
1231
|
+
public getHeatModes(bodyId: number) {
|
|
1232
|
+
let heatModes = super.getHeatModes(bodyId);
|
|
1233
|
+
for (let i = 0; i < heatModes.length; i++) {
|
|
1234
|
+
let hm = heatModes[i];
|
|
1235
|
+
if (hm?.name === 'ultratemp' && typeof hm.val === 'undefined') {
|
|
1236
|
+
heatModes[i] = this.board.valueMaps.heatModes.transformByName('heatpump');
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return heatModes;
|
|
1240
|
+
}
|
|
1221
1241
|
public async setBodyAsync(obj: any): Promise<Body> {
|
|
1222
1242
|
// The 168 is a funky packet in *Touch because it can set:
|
|
1223
1243
|
// * Intellichem Installed (byte 3, bit 1)
|
|
@@ -1499,6 +1519,7 @@ export class TouchCircuitCommands extends CircuitCommands {
|
|
|
1499
1519
|
let cstate = state.circuits.getInterfaceById(data.id, true);
|
|
1500
1520
|
let showInFeatures = cstate.showInFeatures = typeof data.showInFeatures !== 'undefined' ? utils.makeBool(data.showInFeatures) : circuit.showInFeatures;
|
|
1501
1521
|
let typeByte = parseInt(data.type, 10) === 0 ? 0 : parseInt(data.type, 10) || circuit.type || sys.board.valueMaps.circuitFunctions.getValue('generic');
|
|
1522
|
+
this.assertSinglePoolSpaType(id, typeByte);
|
|
1502
1523
|
let freeze = typeof data.freeze !== 'undefined' ? utils.makeBool(data.freeze) : circuit.freeze;
|
|
1503
1524
|
let nameByte = 3; // set default `Aux 1`
|
|
1504
1525
|
if (typeof data.nameId !== 'undefined') nameByte = data.nameId;
|
|
@@ -2701,6 +2722,18 @@ class TouchHeaterCommands extends HeaterCommands {
|
|
|
2701
2722
|
let heaters = sys.heaters.get();
|
|
2702
2723
|
let types = sys.board.valueMaps.heaterTypes.toArray();
|
|
2703
2724
|
let inst = { total: 0 };
|
|
2725
|
+
// If NCP directly controls an ultratemp/heatpump for the same body, suppress
|
|
2726
|
+
// corresponding OCP ghost entries for that body only.
|
|
2727
|
+
let hasNcpUltratempForBody = (ocpHeater: Heater): boolean => {
|
|
2728
|
+
let ocpBody = typeof ocpHeater.body === 'number' ? ocpHeater.body : 32;
|
|
2729
|
+
return heaters.some(h => {
|
|
2730
|
+
if (h.master !== 1 || h.isActive === false) return false;
|
|
2731
|
+
let t = types.find(elem => elem.val === h.type);
|
|
2732
|
+
if (!t || (t.name !== 'ultratemp' && t.name !== 'heatpump')) return false;
|
|
2733
|
+
let ncpBody = typeof h.body === 'number' ? h.body : 32;
|
|
2734
|
+
return ncpBody === 32 || ocpBody === 32 || ncpBody === ocpBody;
|
|
2735
|
+
});
|
|
2736
|
+
};
|
|
2704
2737
|
for (let i = 0; i < types.length; i++) if (types[i].name !== 'none') inst[types[i].name] = 0;
|
|
2705
2738
|
for (let i = 0; i < heaters.length; i++) {
|
|
2706
2739
|
let heater = heaters[i];
|
|
@@ -2709,6 +2742,9 @@ class TouchHeaterCommands extends HeaterCommands {
|
|
|
2709
2742
|
}
|
|
2710
2743
|
let type = types.find(elem => elem.val === heater.type);
|
|
2711
2744
|
if (typeof type !== 'undefined') {
|
|
2745
|
+
// Skip OCP ghost heaters only when there is a matching NCP-controlled
|
|
2746
|
+
// ultratemp/heatpump for this heater's body.
|
|
2747
|
+
if (heater.master === 0 && (type.name === 'hybrid' || type.name === 'ultratemp' || type.name === 'solar') && hasNcpUltratempForBody(heater)) continue;
|
|
2712
2748
|
if (inst[type.name] === 'undefined') inst[type.name] = 0;
|
|
2713
2749
|
inst[type.name] = inst[type.name] + 1;
|
|
2714
2750
|
if (heater.coolingEnabled === true && type.hasCoolSetpoint === true) inst['hasCoolSetpoint'] = true;
|
|
@@ -2971,6 +3007,10 @@ class TouchHeaterCommands extends HeaterCommands {
|
|
|
2971
3007
|
[21, { name: 'ultratemp', desc: 'Ultratemp Only', hasCoolSetpoint: htypes.hasCoolSetpoint }]
|
|
2972
3008
|
])
|
|
2973
3009
|
}
|
|
3010
|
+
else if (ultratempInstalled) {
|
|
3011
|
+
sys.board.valueMaps.heatModes.set(1, { name: 'heatpump', desc: 'Heat Pump' });
|
|
3012
|
+
sys.board.valueMaps.heatSources.set(2, { name: 'heatpump', desc: 'Heat Pump', hasCoolSetpoint: htypes.hasCoolSetpoint });
|
|
3013
|
+
}
|
|
2974
3014
|
else {
|
|
2975
3015
|
// only gas
|
|
2976
3016
|
sys.board.valueMaps.heatModes.delete(2);
|
|
@@ -3022,6 +3062,9 @@ class TouchChemControllerCommands extends ChemControllerCommands {
|
|
|
3022
3062
|
let cyanuricAcid = typeof data.cyanuricAcid !== 'undefined' ? parseInt(data.cyanuricAcid, 10) : chem.cyanuricAcid;
|
|
3023
3063
|
let alkalinity = typeof data.alkalinity !== 'undefined' ? parseInt(data.alkalinity, 10) : chem.alkalinity;
|
|
3024
3064
|
let borates = typeof data.borates !== 'undefined' ? parseInt(data.borates, 10) : chem.borates || 0;
|
|
3065
|
+
let intellichemStandalone = sys.controllerType === ControllerType.Nixie
|
|
3066
|
+
? (typeof data.intellichemStandalone !== 'undefined' ? utils.makeBool(data.intellichemStandalone) : chem.intellichemStandalone)
|
|
3067
|
+
: false;
|
|
3025
3068
|
let body = sys.board.bodies.mapBodyAssociation(typeof data.body === 'undefined' ? chem.body : data.body);
|
|
3026
3069
|
if (typeof body === 'undefined') return Promise.reject(new InvalidEquipmentDataError(`Invalid body assignment`, 'chemController', data.body || chem.body));
|
|
3027
3070
|
// Do a final validation pass so we dont send this off in a mess.
|
|
@@ -3107,6 +3150,7 @@ class TouchChemControllerCommands extends ChemControllerCommands {
|
|
|
3107
3150
|
chem.alkalinity = alkalinity;
|
|
3108
3151
|
chem.borates = borates;
|
|
3109
3152
|
chem.body = schem.body = body.val;
|
|
3153
|
+
chem.intellichemStandalone = intellichemStandalone;
|
|
3110
3154
|
schem.isActive = chem.isActive = true;
|
|
3111
3155
|
chem.lsiRange.enabled = lsiRange.enabled;
|
|
3112
3156
|
chem.lsiRange.low = lsiRange.low;
|