Skip to content

Commit 3334797

Browse files
Merge branch 'pr-1152' into master
Bring PR #1152 changes into master and resolve Messages.ts conflict by preserving both heater protocol response matching and non-IntelliCenter action fallback logic. Made-with: Cursor
2 parents d80ba90 + a83da8e commit 3334797

8 files changed

Lines changed: 255 additions & 21 deletions

File tree

agent_briefs.md

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Agent Briefs (3 Workstreams)
2+
3+
## Goal
4+
5+
Run 3 agents with minimal overlap:
6+
7+
- Agent A: General tab data-path issues (Personal Info + Time/Locality + Delays)
8+
- Agent B: Alerts/Security mapping
9+
- Agent C: Loading-stuck/reliability behavior
10+
11+
This split keeps shared protocol/parser work in one place and avoids conflicting edits across multiple agents.
12+
13+
## Shared Context for All Agents
14+
15+
- Discussion thread: <https://github.com/tagyoureit/nodejs-poolController/discussions/1090#discussioncomment-15925386>
16+
- Newly extracted replay folders:
17+
- `data/1090/v3.008_replay.163`
18+
- `data/1090/v3.008_replay.164`
19+
- `data/1090/v3.008_replay.170`
20+
- `data/1090/v3.008_replay.171`
21+
- `data/1090/v3.008_replay.173`
22+
- Historical comparison replays:
23+
- `data/1090/v3.008_replay.157_config1`
24+
- `data/1090/v3.008_replay.160_config2`
25+
26+
---
27+
28+
# Agent A Brief: General Tab (Personal Info + Time/Locality + Delays)
29+
30+
## Scope
31+
32+
- Personal Information mapping (inbound + outbound)
33+
- Timezone/Clock settings and unintended coupling to body temps/modes
34+
- Delays tab and new v3 fields (`Frz Cycle Time`, `Frz Override`, `Manual Operation Priority`)
35+
36+
## Why this is grouped
37+
38+
These items share the same config/state pathways and overlap in `Action 30` + `Action 168` handling. Keeping them together reduces parser conflicts.
39+
40+
## Exact old comment excerpts to use
41+
42+
> "Personal Information: The information in this section does not appear to link to the OCP (and maybe it isn't intended to). In particular, the City/State/Zip does not link between what is shown on the dashPanel and the WCP. Also, the Pool Alias, Owner and Zip fields on the dashPanel change when other changes are made, with the contents of the Zip field replacing the Pool Alias and then the Zip and Owner fields becoming blank. The Longitude field can't be edited in dashPanel and it does not track changes made with the WCP. The Latitude field can't be edited and is blank in dashPanel."
43+
44+
> "Timezone & Locality: Changing the settings from \"Internet / 12 hour\" to \"Manual / 24 Hour\" causes the Pool Set Point temp to change to 0 and the Spa Set Point Temp to change to 100 and the Spa Heat Mode to change from UltraTemp Only to Solar Only . (Replay 160)."
45+
46+
> "Delays: The settings in the Delays tab do not appear to link between dashPanel and the WCP. Also, dashPanel has a \"Manual Operation Priority\" checkbox that doesn't appear to match anything on the WCP. And, the WCP has \"Frz Cycle Time (min)\" and \"Frz Overide (min)\" fields that do not appear in dashPanel."
47+
48+
## Exact new comment excerpts to use
49+
50+
> "Personal Information All fields except Lat/Lon entered on dashPanel are retained in dashPanel. Lat is blank. Lat/Lon are display only in dashPanel. Zip code, City, State and Country entered in dashPanel are not passed to WCP (other fields don’t exist in WCP). Zip code entered in WCP appears as Pool Alias in dashPanel.City entered in WCP appears as Phone in dashPanelState, Country, Latitude and Longitude entered in WCP are not reflected in dashPanel"
51+
52+
> "There continues to be an incorrect linkage between the time settings and the Pool/Spa temperature settings."
53+
54+
> "Setting Clock Source to “Server” on dashPanel causes error (Replay 163): TypeError: Cannot read properties of undefined (reading 'val') at IntelliCenterSystemCommands.setTZ (/home/pi/nodejs-poolController/controller/boards/SystemBoard.ts:1152:98) ..."
55+
56+
> "Setting Clock Mode (12/24 hour) in dashPanel does transfer to WCP. However, if Clock Source is “Manual” in dashPanel and WCP, changing Clock Mode in dashPanel also causes WCP Mode to switch to “Internet” (Replay 164)."
57+
58+
> "8:04 Changed Frz Cycle Time from 15 to 20 ... 8:24 Frz Override changed from 30 to 90"
59+
60+
## Replay priority
61+
62+
1. `v3.008_replay.171` (personal info + delays + many config changes)
63+
2. `v3.008_replay.170` (timezone/locality coupling)
64+
3. `v3.008_replay.164` (manual/internet mode interaction)
65+
4. `v3.008_replay.163` (server clock source exception)
66+
5. Cross-check with `v3.008_replay.160_config2`
67+
68+
## Deliverables
69+
70+
- Byte-level mapping table (field -> action/type -> payload offsets -> direction)
71+
- Confirmed root-cause list per symptom
72+
- Patch list (file + method + behavior)
73+
- Regression checklist for General tab
74+
75+
---
76+
77+
# Agent B Brief: Alerts and Security
78+
79+
## Scope
80+
81+
- Populate/validate Alerts tab mappings
82+
- Populate/validate Security tab mappings
83+
- Verify round-trip behavior for WCP/OCP <-> njsPC <-> dashPanel
84+
85+
## Exact old comment excerpts to use
86+
87+
> "Alerts and Security: These tabs are currently empty. I will have to take a further look at the OCP/WCP menus to see if there are corresponding entries."
88+
89+
## Exact new comment excerpts to use
90+
91+
> "Alerts and Notifications-Circuits 8:07 Alert Valve Rotation Delay from Off to On 8:08 Heater Cool Down Delay from Off to On"
92+
93+
> "Alerts and Notifications-Pumps NOTE: All of the Alerts in this section were changed from On to Off ... 8:09 Pentair VS/VF/VSF Rate and Power ... 8:11 Communication Lost"
94+
95+
> "Alerts and Notifications-Ultra Temp Heater NOTE: All of the Alerts in this section were changed from On to Off ... 8:17 Communication Lost"
96+
97+
> "Alerts and Notifications-IntelliChlor NOTE: All of the Alerts in this section were changed from On to Off ... 8:19 Communication Lost"
98+
99+
> "Start of REPLAY 173 ... 9:30 Enable Security – Administrator ... 9:32 Change Security Passcode from 7777 to 8888 ... 9:32 Enable Guest ... 9:33 Add “Vacation Mode” access to Guest ... 9:33 Add “All” access to Guest"
100+
101+
## Replay priority
102+
103+
1. `v3.008_replay.171` (alerts changes)
104+
2. `v3.008_replay.173` (security changes)
105+
106+
## Deliverables
107+
108+
- Mapping matrix for each alert/security setting
109+
- Missing fields report (present on WCP/OCP but absent in dashPanel/njsPC)
110+
- Patch list and validation notes
111+
- Any required API schema updates for dashPanel consumption
112+
113+
---
114+
115+
# Agent C Brief: Loading-Stuck and Reliability
116+
117+
## Scope
118+
119+
- Investigate root causes of loading hang/stalls under noisy comms
120+
- Evaluate refresh-trigger behavior (ACK(168)/ACK(184)/piggyback/config queue churn)
121+
- Propose mitigation strategy that does not suppress valid updates
122+
123+
## Exact old comment excerpts to use
124+
125+
> "A general issue which may be specific to my system is that periodically following a settings change initiated in dashPanel, the \"Loading\" message will appear and will hang before completion. That occurred at the end of Replay 157 with the updated hanging at 0%. Although nodejs-poolController continues to run, I have to restart it to clear the problem. My system is showing a lot of failed sends (55% failure rate) which might be related to this."
126+
127+
## Exact new comment excerpts to use
128+
129+
> "I restarted the access point that the Elfin uses and have had significantly fewer transmit errors since then."
130+
131+
> "[12/24/2025, 10:47:00 AM] warn: Registration rejected by OCP via Action 217 (status=4) - duplicate registration attempt?"
132+
133+
## Replay priority
134+
135+
1. `v3.008_replay.157_config1` (stuck at loading 0)
136+
2. `v3.008_replay.160_config2` (comparison run)
137+
3. Correlate with new runs:
138+
- `v3.008_replay.163`
139+
- `v3.008_replay.164`
140+
- `v3.008_replay.170`
141+
- `v3.008_replay.171`
142+
- `v3.008_replay.173`
143+
144+
## Deliverables
145+
146+
- Event-timeline of one failed and one successful config cycle
147+
- Trigger/churn analysis (`Requesting IntelliCenter configuration`, queued items, completion rates)
148+
- Proposed mitigation options with tradeoffs:
149+
- refresh debounce tuning
150+
- in-flight refresh guard
151+
- stale loading-state timeout/reset behavior
152+
- registration retry/backoff adjustments
153+
- Recommended implementation order
154+
155+
---
156+
157+
# Coordination Rules (Important)
158+
159+
- Agent A owns changes in shared General/config parser paths.
160+
- Agent B should not modify Agent A parser logic unless required and documented.
161+
- Agent C should avoid behavior changes that alter field parsing/mapping.
162+
- Final integration step should run all touched replay scenarios before merge.

controller/boards/EasyTouchBoard.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,26 @@ class TouchSystemCommands extends SystemCommands {
12181218
}
12191219
}
12201220
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+
}
12211241
public async setBodyAsync(obj: any): Promise<Body> {
12221242
// The 168 is a funky packet in *Touch because it can set:
12231243
// * Intellichem Installed (byte 3, bit 1)
@@ -2702,6 +2722,18 @@ class TouchHeaterCommands extends HeaterCommands {
27022722
let heaters = sys.heaters.get();
27032723
let types = sys.board.valueMaps.heaterTypes.toArray();
27042724
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+
};
27052737
for (let i = 0; i < types.length; i++) if (types[i].name !== 'none') inst[types[i].name] = 0;
27062738
for (let i = 0; i < heaters.length; i++) {
27072739
let heater = heaters[i];
@@ -2710,6 +2742,9 @@ class TouchHeaterCommands extends HeaterCommands {
27102742
}
27112743
let type = types.find(elem => elem.val === heater.type);
27122744
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;
27132748
if (inst[type.name] === 'undefined') inst[type.name] = 0;
27142749
inst[type.name] = inst[type.name] + 1;
27152750
if (heater.coolingEnabled === true && type.hasCoolSetpoint === true) inst['hasCoolSetpoint'] = true;
@@ -2972,6 +3007,10 @@ class TouchHeaterCommands extends HeaterCommands {
29723007
[21, { name: 'ultratemp', desc: 'Ultratemp Only', hasCoolSetpoint: htypes.hasCoolSetpoint }]
29733008
])
29743009
}
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+
}
29753014
else {
29763015
// only gas
29773016
sys.board.valueMaps.heatModes.delete(2);

controller/boards/SystemBoard.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4059,7 +4059,7 @@ export class HeaterCommands extends BoardCommands {
40594059
public updateHeaterServices() {
40604060
let htypes = sys.board.heaters.getInstalledHeaterTypes();
40614061
let solarInstalled = htypes.solar > 0;
4062-
let heatPumpInstalled = htypes.heatpump > 0;
4062+
let heatPumpInstalled = htypes.heatpump > 0 || htypes.ultratemp > 0;
40634063
let gasHeaterInstalled = htypes.gas > 0;
40644064
if (sys.heaters.length > 0) sys.board.valueMaps.heatSources = new byteValueMap([[0, { name: 'off', desc: 'Off' }]]);
40654065
if (gasHeaterInstalled) sys.board.valueMaps.heatSources.set(3, { name: 'heater', desc: 'Heater' });
@@ -4250,6 +4250,9 @@ export class HeaterCommands extends BoardCommands {
42504250
// so that if we have a heater preference set up then we do not have to evaluate the other heater.
42514251
let heaterTypes = sys.board.valueMaps.heaterTypes;
42524252
bodyHeaters.sort((a, b) => {
4253+
// Sort master=1 (NCP-controlled) heaters before master=0 (OCP-controlled)
4254+
// so directly controlled heaters get priority over OCP ghosts.
4255+
if (a.master !== b.master) return b.master - a.master;
42534256
if (heaterTypes.transform(a.type).hasPreference) return -1;
42544257
else if (heaterTypes.transform(b.type).hasPreference) return 1;
42554258
return 0;
@@ -4327,11 +4330,11 @@ export class HeaterCommands extends BoardCommands {
43274330
// This is the default operation on IntelliCenter and it appears to simply not start on the setpoint. We can do better
43284331
// than this by heating 1 degree past the setpoint then applying this rule for 30 minutes. This allows for a more
43294332
// responsive heater.
4330-
//
4333+
//
43314334
// For Ultratemp we need to determine whether the differential temp
43324335
// is within range. The other thing that needs to be calculated here is
43334336
// whether Ultratemp can effeciently heat the pool.
4334-
if (mode === 'ultratemp' || mode === 'ultratemppref') {
4337+
if (mode === 'ultratemp' || mode === 'ultratemppref' || mode === 'heatpump' || mode === 'heatpumppref') {
43354338
if (hstate.isOn) {
43364339
// For the preference mode we will try to reach the setpoint for a period of time then
43374340
// switch over to the gas heater. Our algorithm for this is to check the rate of

controller/comms/messages/Messages.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,13 @@ export class Response extends OutboundCommon {
14881488
//
14891489
// NOTE: IntelliCenter response matching is handled in the IntelliCenter-specific block below
14901490
// to keep the logic in one place.
1491+
if (msgOut.protocol === Protocol.Heater) {
1492+
// Heater protocol: request action 114 → response action 115, etc.
1493+
// Verify response comes from the heater we addressed.
1494+
if (msgIn.source !== msgOut.dest || (msgIn.dest !== msgOut.source && msgIn.dest !== 16)) { return false; }
1495+
if (this.action > 0 && this.action === msgIn.action) return true;
1496+
return false;
1497+
}
14911498
//
14921499
// Restore Response-level action matching for non-IntelliCenter protocols (e.g., Hayward).
14931500
// The Hayward Outbound action getter has a known index mismatch (reads source instead of action),
@@ -1497,7 +1504,7 @@ export class Response extends OutboundCommon {
14971504
if (this.action === msgIn.action) return true;
14981505
else return false;
14991506
}
1500-
if (msgOut.protocol === Protocol.Pump) {
1507+
else if (msgOut.protocol === Protocol.Pump) {
15011508
switch (msgIn.action) {
15021509
case 7:
15031510
// Scenario 1. Request for pump status.

controller/comms/messages/status/HeaterStateMessage.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
1818
import { Inbound, Protocol } from "../Messages";
1919
import { state, BodyTempState, HeaterState } from "../../../State";
2020
import { sys, ControllerType, Heater } from "../../../Equipment";
21+
import { logger } from '../../../../logger/Logger';
2122

2223
export class HeaterStateMessage {
2324
public static process(msg: Inbound) {
@@ -85,33 +86,44 @@ export class HeaterStateMessage {
8586
msg.isProcessed = true;
8687
}
8788
public static processUltraTempStatus(msg: Inbound) {
88-
// RKS: 07-03-21 - We only know byte 2 at this point for Ultratemp for the 115 message we are processing here. The
89+
// RKS: 07-03-21 - UltraTemp RS-485 protocol reverse engineering notes.
90+
// The heat pump communicates via Action 114 (command) / 115 (response) messages.
91+
//
92+
// Action 115 - inbound response (heat pump -> controller, heartbeat every ~1s)
93+
// [165, 0, 16, 112, 115, 10][160, 1, 0, 3, 0, 0, 0, 0, 0, 0][2, 70]
8994
// byte description
9095
// ------------------------------------------------
91-
// 0 Unknown (always seems to be 160 for response)
92-
// 1 Unknown (always 1)
93-
// 2 Current heater status 0=off, 1=heat, 2=cool
94-
// 3-9 Unknown
95-
96-
// 114 message - outbound response
97-
//[165, 0, 112, 16, 114, 10][144, 0, 0, 0, 0, 0, 0, 0, 0, 0][2, 49] // OCP to Heater
96+
// 0 Always 160 for response
97+
// 1 Always 1
98+
// 2 Current heater status: 0=off, 1=heat, 2=cool
99+
// 3 Believed to be offset temp
100+
// 4-9 Unknown
101+
//
102+
// Action 114 - outbound command (controller -> heat pump)
103+
// [165, 0, 112, 16, 114, 10][144, 0, 0, 0, 0, 0, 0, 0, 0, 0][2, 49]
98104
// byte description
99105
// ------------------------------------------------
100-
// 0 Unknown (always seems to be 144 for request)
101-
// 1 Current heater status 0=off, 1=heat, 2=cool
102-
// 3 Believed to be ofset temp
106+
// 0 Always 144 for request
107+
// 1 Sets heater mode: 0=off, 1=heat, 2=cool
108+
// 3 Believed to be offset temp
103109
// 4-9 Unknown
104-
105-
// byto 0: always seems to be 144 for outbound
106-
// byte 1: Sets heater mode to 0 = Off 1 = Heat 2 = Cool
107-
//[165, 0, 16, 112, 115, 10][160, 1, 0, 3, 0, 0, 0, 0, 0, 0][2, 70] // Heater Reply
108110
let heater: Heater = sys.heaters.getItemByAddress(msg.source);
111+
if (typeof heater === 'undefined' || !heater.isActive) {
112+
// Heat pump not configured for this address
113+
msg.isProcessed = true;
114+
return;
115+
}
109116
let sheater = state.heaters.getItemById(heater.id);
110117
let byte = msg.extractPayloadByte(2);
118+
let prevOn = sheater.isOn;
119+
let prevCooling = sheater.isCooling;
111120
sheater.isOn = byte >= 1;
112121
sheater.isCooling = byte === 2;
113122
sheater.commStatus = 0;
114123
state.equipment.messages.removeItemByCode(`heater:${heater.id}:comms`);
124+
if (prevOn !== sheater.isOn || prevCooling !== sheater.isCooling) {
125+
logger.info(`UltraTemp heartbeat: src=${msg.source} status=${byte} (${byte === 0 ? 'OFF' : byte === 1 ? 'HEAT' : 'COOL'}) heater=${heater.name}`);
126+
}
115127
msg.isProcessed = true;
116128
}
117129
public static processMasterTempStatus(msg: Inbound) {

0 commit comments

Comments
 (0)