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.
Files changed (35) hide show
  1. package/.github/workflows/ghcr-publish.yml +1 -1
  2. package/157_issues.md +101 -0
  3. package/AGENTS.md +17 -1
  4. package/README.md +13 -2
  5. package/controller/Equipment.ts +49 -0
  6. package/controller/State.ts +8 -0
  7. package/controller/boards/AquaLinkBoard.ts +174 -2
  8. package/controller/boards/EasyTouchBoard.ts +44 -0
  9. package/controller/boards/IntelliCenterBoard.ts +360 -172
  10. package/controller/boards/NixieBoard.ts +7 -4
  11. package/controller/boards/SunTouchBoard.ts +1 -0
  12. package/controller/boards/SystemBoard.ts +39 -4
  13. package/controller/comms/Comms.ts +9 -3
  14. package/controller/comms/messages/Messages.ts +218 -24
  15. package/controller/comms/messages/config/EquipmentMessage.ts +34 -0
  16. package/controller/comms/messages/config/ExternalMessage.ts +1051 -989
  17. package/controller/comms/messages/config/GeneralMessage.ts +65 -0
  18. package/controller/comms/messages/config/OptionsMessage.ts +15 -2
  19. package/controller/comms/messages/config/PumpMessage.ts +427 -421
  20. package/controller/comms/messages/config/SecurityMessage.ts +37 -13
  21. package/controller/comms/messages/status/EquipmentStateMessage.ts +0 -218
  22. package/controller/comms/messages/status/HeaterStateMessage.ts +27 -15
  23. package/controller/comms/messages/status/NeptuneModbusStateMessage.ts +217 -0
  24. package/controller/comms/messages/status/VersionMessage.ts +67 -18
  25. package/controller/nixie/chemistry/ChemController.ts +65 -33
  26. package/controller/nixie/heaters/Heater.ts +10 -1
  27. package/controller/nixie/pumps/Pump.ts +145 -2
  28. package/docker-compose.yml +1 -0
  29. package/logger/Logger.ts +75 -64
  30. package/package.json +1 -1
  31. package/tsconfig.json +2 -1
  32. package/web/Server.ts +3 -1
  33. package/web/services/config/Config.ts +150 -1
  34. package/web/services/state/State.ts +21 -0
  35. package/web/services/state/StateSocket.ts +28 -0
@@ -17,45 +17,110 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
  */
18
18
  import { Inbound } from "../Messages";
19
19
  import { sys, General } from "../../../Equipment";
20
+ import { ControllerType } from "../../../Constants";
20
21
  import { logger } from "../../../../logger/Logger";
21
22
  export class GeneralMessage {
23
+ private static isIntellicenterV3(): boolean {
24
+ return sys.controllerType === ControllerType.IntelliCenter && sys.equipment.isIntellicenterV3 === true;
25
+ }
26
+ private static getTrimmed(msg: Inbound, start: number, len: number): string {
27
+ return (msg.extractPayloadString(start, len) || '').replace(/\0+$/g, '').trim();
28
+ }
22
29
  public static process(msg: Inbound): void {
30
+ const isIntellicenterV3 = GeneralMessage.isIntellicenterV3();
23
31
  switch (msg.extractPayloadByte(1)) {
24
32
  case 0:
33
+ if (isIntellicenterV3) {
34
+ // v3.008+ sends location-focused data for item 0.
35
+ const zip = GeneralMessage.getTrimmed(msg, 2, 6);
36
+ if (zip.length > 0) sys.general.location.zip = zip;
37
+ // In captured v3.008 packets, bytes 13/14 map cleanly to longitude magnitude.
38
+ const lonLo = msg.extractPayloadByte(13, 255);
39
+ const lonHi = msg.extractPayloadByte(14, 255);
40
+ if (lonLo !== 255 && lonHi !== 255) {
41
+ const lon = ((lonHi * 256) + lonLo) / 100;
42
+ if (!isNaN(lon) && lon > 0 && lon <= 180) sys.general.location.longitude = -lon;
43
+ }
44
+ msg.isProcessed = true;
45
+ break;
46
+ }
25
47
  sys.general.alias = msg.extractPayloadString(2, 16);
26
48
  sys.general.owner.name = msg.extractPayloadString(18, 16);
27
49
  sys.general.location.zip = msg.extractPayloadString(34, 6);
28
50
  msg.isProcessed = true;
29
51
  break;
30
52
  case 1:
53
+ if (isIntellicenterV3) {
54
+ // v3.008+ item 1 carries city text (not phone fields).
55
+ const city = GeneralMessage.getTrimmed(msg, 2, 32);
56
+ if (city.length > 0) sys.general.location.city = city;
57
+ msg.isProcessed = true;
58
+ break;
59
+ }
31
60
  sys.general.owner.phone = msg.extractPayloadString(2, 20);
32
61
  sys.general.owner.phone2 = msg.extractPayloadString(21, 15);
33
62
  sys.general.location.latitude = ((msg.extractPayloadByte(35) * 256) + msg.extractPayloadByte(34)) / 100;
34
63
  msg.isProcessed = true;
35
64
  break;
36
65
  case 2:
66
+ if (isIntellicenterV3) {
67
+ const owner = GeneralMessage.getTrimmed(msg, 2, 16);
68
+ if (owner.length > 0) sys.general.owner.name = owner;
69
+ msg.isProcessed = true;
70
+ break;
71
+ }
37
72
  sys.general.location.address = msg.extractPayloadString(2, 32);
38
73
  sys.general.location.longitude = -(((msg.extractPayloadByte(35) * 256) + msg.extractPayloadByte(34)) / 100);
39
74
  msg.isProcessed = true;
40
75
  break;
41
76
  case 3:
77
+ if (isIntellicenterV3) {
78
+ const email = GeneralMessage.getTrimmed(msg, 2, 32);
79
+ if (email.length > 0) sys.general.owner.email = email;
80
+ msg.isProcessed = true;
81
+ break;
82
+ }
42
83
  sys.general.owner.email = msg.extractPayloadString(2, 32);
43
84
  sys.general.location.timeZone = msg.extractPayloadByte(34);
44
85
  msg.isProcessed = true;
45
86
  break;
46
87
  case 4:
88
+ if (isIntellicenterV3) {
89
+ const email2 = GeneralMessage.getTrimmed(msg, 2, 32);
90
+ if (email2.length > 0) sys.general.owner.email2 = email2;
91
+ msg.isProcessed = true;
92
+ break;
93
+ }
47
94
  sys.general.owner.email2 = msg.extractPayloadString(2, 32);
48
95
  msg.isProcessed = true;
49
96
  break;
50
97
  case 5:
98
+ if (isIntellicenterV3) {
99
+ const country = GeneralMessage.getTrimmed(msg, 2, 32);
100
+ if (country.length > 0) sys.general.location.country = country;
101
+ msg.isProcessed = true;
102
+ break;
103
+ }
51
104
  sys.general.location.country = msg.extractPayloadString(2, 32);
52
105
  msg.isProcessed = true;
53
106
  break;
54
107
  case 6:
108
+ if (isIntellicenterV3) {
109
+ const city = GeneralMessage.getTrimmed(msg, 2, 32);
110
+ if (city.length > 0) sys.general.location.city = city;
111
+ msg.isProcessed = true;
112
+ break;
113
+ }
55
114
  sys.general.location.city = msg.extractPayloadString(2, 32);
56
115
  msg.isProcessed = true;
57
116
  break;
58
117
  case 7:
118
+ if (isIntellicenterV3) {
119
+ const stateText = GeneralMessage.getTrimmed(msg, 2, 32);
120
+ if (stateText.length > 0) sys.general.location.state = stateText;
121
+ msg.isProcessed = true;
122
+ break;
123
+ }
59
124
  sys.general.location.state = msg.extractPayloadString(2, 32);
60
125
  msg.isProcessed = true;
61
126
  break;
@@ -20,6 +20,10 @@ import { sys } from "../../../Equipment";
20
20
  import { state } from "../../../State";
21
21
  import { ControllerType } from "../../../Constants";
22
22
  export class OptionsMessage {
23
+ private static decodeFreezeOverride(raw: number): number {
24
+ // v3.008 captures show this as a compact code where 0 => 30 min and 1 => 90 min.
25
+ return raw <= 3 ? (30 + (raw * 60)) : raw;
26
+ }
23
27
  public static process(msg: Inbound): void {
24
28
  switch (sys.controllerType) {
25
29
  case ControllerType.IntelliCenter:
@@ -55,7 +59,17 @@ export class OptionsMessage {
55
59
  // cooldownDelay
56
60
  //[255, 0, 255][165, 63, 15, 16, 30, 40][0, 0, 1, 129, 0, 0, 0, 0, 0, 0, 0, 0, 0, 176, 149, 29, 35, 3, 0, 0, 92, 81, 91, 81, 3, 3, 0, 0, 15, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0][4, 194]
57
61
  sys.general.options.cooldownDelay = msg.extractPayloadByte(30) === 1;
58
- sys.general.options.manualPriority = msg.extractPayloadByte(38) === 1;
62
+ let manualPriorityByte = msg.extractPayloadByte(38, 255);
63
+ const isIntellicenterV3 = (sys.controllerType === ControllerType.IntelliCenter && sys.equipment.isIntellicenterV3);
64
+ if (isIntellicenterV3) {
65
+ const v3ManualPriorityByte = msg.extractPayloadByte(28, 255);
66
+ if (v3ManualPriorityByte === 0 || v3ManualPriorityByte === 1) manualPriorityByte = v3ManualPriorityByte;
67
+ const freezeCycleTime = msg.extractPayloadByte(26, 255);
68
+ if (freezeCycleTime !== 255) sys.general.options.freezeCycleTime = freezeCycleTime;
69
+ const freezeOverrideRaw = msg.extractPayloadByte(27, 255);
70
+ if (freezeOverrideRaw !== 255) sys.general.options.freezeOverride = OptionsMessage.decodeFreezeOverride(freezeOverrideRaw);
71
+ }
72
+ if (manualPriorityByte !== 255) sys.general.options.manualPriority = manualPriorityByte === 1;
59
73
  sys.general.options.manualHeat = msg.extractPayloadByte(39) === 1;
60
74
  let fnTranslateByte = (byte):number => { return (byte & 0x007F) * (((byte & 0x0080) > 0) ? -1 : 1); }
61
75
  sys.equipment.tempSensors.setCalibration('water1', fnTranslateByte(msg.extractPayloadByte(3)));
@@ -79,7 +93,6 @@ export class OptionsMessage {
79
93
 
80
94
  // v3.004+: payload layout shifted by 1 byte vs v1.x (timestamp insertion earlier in the packet).
81
95
  // Evidence: replay.21 Action 30 type 0 has [.., 85,100,94,103, 3,3 ..] at bytes 19-24.
82
- const isIntellicenterV3 = (sys.controllerType === ControllerType.IntelliCenter && sys.equipment.isIntellicenterV3);
83
96
  const poolHeatNdx = isIntellicenterV3 ? 19 : 20;
84
97
  const poolCoolNdx = isIntellicenterV3 ? 20 : 21;
85
98
  const spaHeatNdx = isIntellicenterV3 ? 21 : 22;