nodejs-poolcontroller 8.1.1 → 8.3.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.
@@ -0,0 +1,63 @@
1
+ # Copilot Project Instructions
2
+
3
+ Purpose: Help AI agents contribute effectively to nodejs-poolController (njsPC) with minimal ramp-up.
4
+
5
+ ## 1. Core Domain & Architecture
6
+ - Goal: Bridge Pentair / compatible pool automation equipment (RS-485 / ScreenLogic) to REST, WebSockets, MQTT, InfluxDB, Rules, and REM (Relay Equipment Manager) interfaces.
7
+ - Startup sequence (see `app.ts`): config.init -> logger.init -> sys.init -> state.init -> webApp.init -> conn.initAsync (RS485 / network) -> sys.start -> webApp.initAutoBackup -> sl.openAsync (ScreenLogic).
8
+ - Major layers:
9
+ 1. `config/Config.ts`: Loads/merges `defaultConfig.json` + `config.json`, watches disk, applies env overrides (POOL_*). Always mutate via `config.setSection()` / `config.updateAsync()`.
10
+ 2. `controller/`:
11
+ - `comms/Comms.ts` (RS485 transport) + `comms/messages/*` (protocol encode/decode) feeding message objects to system/state.
12
+ - `Equipment.ts` (`sys`): Aggregates boards, pumps, heaters, bodies, chemistry, schedules.
13
+ - `boards/*Board.ts` selected by `BoardFactory.fromControllerType()` (ControllerType enum) to encapsulate model-specific logic.
14
+ - `State.ts`: Higher-level computed/state cache (emits events to interfaces & persistence).
15
+ 3. `web/Server.ts`: Orchestrates multiple server/interface types (http/https/http2, mdns, ssdp, mqtt, influx, rule, rem). Each concrete server extends a ProtoServer pattern (see file) and exposes `emitToClients` / `emitToChannel`.
16
+ 4. `logger/Logger.ts`: Winston wrapper with packet & ScreenLogic capture, optional replay capture mode (`log.app.captureForReplay`).
17
+ - Data Flow: Raw bytes -> `Comms` -> `Messages` decode -> equipment/state mutation -> events -> `webApp.emitToChannel()` -> clients (dashPanel, bindings, MQTT, etc.).
18
+
19
+ ## 2. Key Conventions & Patterns
20
+ - Prefer calling exported singletons (`config`, `logger`, `sys`, `state`, `webApp`, `conn`) — they are initialized once in `app.ts`.
21
+ - Extend support for a new controller board: create `controller/boards/NewBoard.ts` implementing expected interface and add to `BoardFactory` switch.
22
+ - Adding an external interface: implement a server class similar to existing ones in `web/Server.ts` and register in `initInterfaces()` via `type` value in `web.interfaces` section of config.
23
+ - Configuration writes are async & debounced by a semaphore (`_isLoading`). Avoid rapid consecutive writes — batch changes before `setSection`.
24
+ - Logging packets: push through `logger.packet(msg)`; only log when `log.packet.enabled` or capture mode active. Don't bypass logger for protocol-level diagnostics.
25
+ - Use `utils.uuid()` for persistent IDs stored back into config (`web.servers.*` / `web.interfaces.*`).
26
+
27
+ ## 3. Build & Run Workflow
28
+ - Scripts (`package.json`): `npm start` = build (tsc) + run `dist/app.js`; `npm run start:cached` skips build (use only after prior successful build); `npm run build` or `watch` for development.
29
+ - Always rebuild after pulling if Typescript sources changed (common; master is live development).
30
+ - Minimum Node >=16 (see `engines`).
31
+ - Typical dev loop: edit TS -> `npm run build` (or `watch` in one terminal) -> `npm run start:cached` in another.
32
+
33
+ ## 4. Safe Change Guidelines (Project Specific)
34
+ - Never directly edit `config.json` structure assumptions without updating `defaultConfig.json` and migration logic if needed.
35
+ - When adding message types: place in proper `controller/comms/messages/{config|status}` folder; ensure decode populates strongly typed object consumed by equipment/state.
36
+ - Emitting to clients: prefer channel scoping with `webApp.emitToChannel(channel, evt, payload)` over broad `emitToClients` to reduce noise.
37
+ - Shutdown paths must await: see `stopAsync()` ordering in `app.ts`; replicate that order if introducing new long-lived resources.
38
+ - Packet capture integration: if adding new traffic sources, feed capture arrays so `logger.stopCaptureForReplayAsync()` includes them in backups.
39
+
40
+ ## 5. Extending / Examples
41
+ - Add new board: create file, implement constructor(system), override protocol handlers, add case in `BoardFactory`.
42
+ - Add new interface type: define class (e.g., `FooInterfaceServer`) patterned after `MqttInterfaceServer`; map `type: 'foo'` in config to new class in `initInterfaces` switch.
43
+ - Add env override: update `Config.getEnvVariables()` with POOL_* variable mapping.
44
+
45
+ ## 6. Debugging Tips
46
+ - Packet issues: enable `log.packet.logToConsole` & set `log.app.level` to `debug` or `silly` in `config.json` (or via capture mode) then rebuild & restart.
47
+ - Config reload: editing `config.json` on disk triggers fs watch; logger reinitializes log settings automatically.
48
+ - Network discovery problems: inspect `mdns` / `ssdp` sections in `web.servers` config; ensure correct interface binding (`ip` / `0.0.0.0`).
49
+
50
+ ## 7. Common Pitfalls
51
+ - Forgetting to rebuild after TS edits (leads to running stale `dist`).
52
+ - Mutating returned config objects directly after `getSection` (they are deep-cloned; you must re-set via `setSection`).
53
+ - Adding interface without persisting UUID: ensure `utils.uuid()` assigned when undefined.
54
+ - Logging floods: avoid tight loops writing directly to console; use logger buffering (`packet` flush timer) pattern.
55
+
56
+ ## 8. Contribution Checklist (Agent Focused)
57
+ 1. Identify layer (comms / equipment / state / interface / config / logging) impacted.
58
+ 2. Update or add unit-like logic in correct module; keep cross-layer boundaries (no UI assumptions in lower layers).
59
+ 3. Use existing singletons; avoid new global state without need.
60
+ 4. Validate build (`npm run build`) before proposing changes.
61
+ 5. Provide brief rationale in PR tying change to equipment behavior or integration capability.
62
+
63
+ Feedback welcome: clarify any unclear pattern or request examples to extend this guide.
@@ -0,0 +1,67 @@
1
+ name: Publish Docker Image - GHCR
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ tags:
8
+ - 'v*.*.*'
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ build-and-push:
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ contents: read
16
+ packages: write
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Docker meta
22
+ id: meta
23
+ uses: docker/metadata-action@v5
24
+ with:
25
+ images: |
26
+ ghcr.io/${{ github.repository_owner }}/njspc
27
+ tags: |
28
+ type=ref,event=branch
29
+ type=ref,event=pr
30
+ type=semver,pattern={{version}}
31
+ type=semver,pattern={{major}}.{{minor}}
32
+ type=semver,pattern={{major}}
33
+ type=sha
34
+ type=raw,value=latest,enable={{is_default_branch}}
35
+ labels: |
36
+ org.opencontainers.image.source=${{ github.repository }}
37
+ org.opencontainers.image.revision=${{ github.sha }}
38
+ org.opencontainers.image.title=njspc
39
+ org.opencontainers.image.description=Pentair pool controller bridge (GHCR image)
40
+
41
+ - name: Set up QEMU
42
+ uses: docker/setup-qemu-action@v3
43
+
44
+ - name: Set up Docker Buildx
45
+ uses: docker/setup-buildx-action@v3
46
+
47
+ - name: Login to GHCR
48
+ uses: docker/login-action@v3
49
+ with:
50
+ registry: ghcr.io
51
+ username: ${{ github.actor }}
52
+ password: ${{ secrets.GITHUB_TOKEN }}
53
+
54
+ - name: Build and push
55
+ uses: docker/build-push-action@v6
56
+ with:
57
+ context: .
58
+ push: true
59
+ platforms: linux/amd64,linux/arm64,linux/arm/v7
60
+ tags: ${{ steps.meta.outputs.tags }}
61
+ labels: ${{ steps.meta.outputs.labels }}
62
+ cache-from: type=gha
63
+ cache-to: type=gha,mode=max
64
+
65
+ - name: Echo published tags
66
+ run: |
67
+ echo "Published: ${{ steps.meta.outputs.tags }}"
package/Changelog CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 8.3.0
4
+ 1. Configurable RS‑485 transmit pacing via new `controller.txDelays` for finer collision avoidance and throughput tuning.
5
+ 2. Startup & config resilience: empty or invalid `config.json` now auto‑recreated from defaults, corrupt originals backed up.
6
+ 3. Latitude / longitude environment overrides to eliminate early heliotrope warnings prior to UI configuration.
7
+ 4. Version check enhancements: git detection, safer redirects, throttled polling, warning suppression.
8
+ 5. Docker improvements: fixed Dockerfile and added docker‑compose example with named volumes & environment variable guidance.
9
+ 6. Add workflow to build and publish docker images to GitHub Container registry.
10
+ 7. Runtime requirements: elevated minimum Node.js version to 20+, safe dependency and security/patch updates.
11
+ 8. Documentation updates.
12
+
13
+ ## 8.1.2
14
+ 1. Regal Century pump support added.
15
+ 2. Enhanced error handling: replaced EquipmentNotFoundError with InvalidEquipmentIdError for improved clarity.
16
+ 3. Improved error handling in NixieBoard, NixieCircuitCommands, and NixieValveCommands.
17
+ 4. Refactored tolerance handling in TouchChemControllerCommands to ensure data integrity.
18
+ 5. Added new expansion board configuration for SunTouchBoard.
19
+
20
+ ## 8.1.1
21
+ 1. Enhanced REM server integration with packet capture functionality.
22
+ 2. Improved error handling in REMInterfaceServer methods.
23
+ 3. Refined chlorinator message processing logic in MessagesMock.
24
+ 4. Adjusted mock port checks across various components for consistency.
25
+ 5. Refined logic for Nixie schedules.
26
+ 6. Fixed documentation links for bindings integrations.
27
+ 7. Enhanced queueBodyHeatSettings to handle processing timeouts and improve error logging.
28
+ 8. Fixed typo in valveModes.
29
+
3
30
  ## 8.1.0
4
31
  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
32
 
package/Dockerfile CHANGED
@@ -1,19 +1,62 @@
1
- FROM node:18-alpine AS build
2
- RUN apk add --no-cache make gcc g++ python3 linux-headers udev tzdata
1
+ ### Build stage
2
+ FROM node:20-alpine AS build
3
+ LABEL maintainer="nodejs-poolController"
4
+ LABEL org.opencontainers.image.title="nodejs-poolController"
5
+ LABEL org.opencontainers.image.description="Bridge Pentair / compatible pool automation equipment to modern interfaces (REST, WebSockets, MQTT, Influx, Rules)."
6
+ LABEL org.opencontainers.image.licenses="AGPL-3.0-only"
7
+ LABEL org.opencontainers.image.source="https://github.com/tagyoureit/nodejs-poolController"
8
+
9
+ # Install build toolchain only for native deps (serialport, etc.)
10
+ RUN apk add --no-cache make gcc g++ python3 linux-headers udev tzdata git
11
+
3
12
  WORKDIR /app
13
+
14
+ # Leverage Docker layer caching: copy only manifests first
4
15
  COPY package*.json ./
5
16
  COPY defaultConfig.json config.json
17
+
18
+ # Install all deps (including dev) for build
6
19
  RUN npm ci
20
+
21
+ # Copy source
7
22
  COPY . .
23
+
24
+ # Build Typescript
8
25
  RUN npm run build
9
- RUN npm ci --omit=dev
10
26
 
11
- FROM node:18-alpine as prod
12
- RUN apk add git
13
- RUN mkdir /app && chown node:node /app
27
+ # Remove dev dependencies while keeping a clean node_modules with prod deps only
28
+ RUN npm prune --production
29
+
30
+ ### Runtime stage
31
+ FROM node:20-alpine AS prod
32
+ LABEL org.opencontainers.image.title="nodejs-poolController"
33
+ LABEL org.opencontainers.image.description="Bridge Pentair / compatible pool automation equipment to modern interfaces (REST, WebSockets, MQTT, Influx, Rules)."
34
+ LABEL org.opencontainers.image.licenses="AGPL-3.0-only"
35
+ LABEL org.opencontainers.image.source="https://github.com/tagyoureit/nodejs-poolController"
36
+ ENV NODE_ENV=production
37
+
38
+ # Use existing 'node' user from base image; just ensure work directory exists
14
39
  WORKDIR /app
15
- COPY --chown=node:node --from=build /app .
40
+ RUN mkdir -p /app
41
+ RUN mkdir -p /app/logs /app/data /app/backups /app/web/bindings/custom \
42
+ && chown -R node:node /app/logs /app/data /app/backups /app/web/bindings /app/web/bindings/custom || true
43
+
44
+ # Copy only the necessary runtime artifacts from build stage
45
+ COPY --chown=node:node --from=build /app/package*.json ./
46
+ COPY --chown=node:node --from=build /app/node_modules ./node_modules
47
+ COPY --chown=node:node --from=build /app/dist ./dist
48
+ COPY --chown=node:node --from=build /app/defaultConfig.json ./defaultConfig.json
49
+ COPY --chown=node:node --from=build /app/config.json ./config.json
50
+ COPY --chown=node:node --from=build /app/README.md ./README.md
51
+ COPY --chown=node:node --from=build /app/LICENSE ./LICENSE
52
+
16
53
  USER node
17
- ENV NODE_ENV=production
18
- EXPOSE 5150
54
+
55
+ # Default HTTP / HTTPS (if enabled) ports from defaultConfig (http 4200, https 4201)
56
+ EXPOSE 4200 4201
57
+
58
+ # Basic healthcheck (container considered healthy if process responds to tcp socket open)
59
+ HEALTHCHECK --interval=45s --timeout=6s --start-period=40s --retries=4 \
60
+ CMD node -e "const n=require('net');const s=n.createConnection({host:'127.0.0.1',port:4200},()=>{s.end();process.exit(0)});s.on('error',()=>process.exit(1));setTimeout(()=>{s.destroy();process.exit(1)},5000);" || exit 1
61
+
19
62
  ENTRYPOINT ["node", "dist/app.js"]
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.1
4
+ # nodejs-poolController - Version 8.3.0
5
5
 
6
6
  ## What is nodejs-poolController
7
7
 
@@ -26,6 +26,16 @@ 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.3.0?
30
+
31
+ 1. Configurable RS‑485 transmit pacing via new `controller.txDelays` for finer collision avoidance and throughput tuning.
32
+ 2. Startup & config resilience: empty or invalid `config.json` now auto‑recreated from defaults, corrupt originals backed up.
33
+ 3. Latitude / longitude environment overrides to eliminate early heliotrope warnings prior to UI configuration.
34
+ 4. Version check enhancements: git detection, safer redirects, throttled polling, warning suppression.
35
+ 5. Docker improvements: fixed Dockerfile and added docker‑compose example with named volumes & environment variable guidance.
36
+ 6. Add workflow to build and publish docker images to GitHub Container registry.
37
+ 7. Runtime requirements: elevated minimum Node.js version to 20+, safe dependency and security/patch updates.
38
+
29
39
  ## What's new in 8.1?
30
40
 
31
41
  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).
@@ -56,7 +66,7 @@ This is only the server code. See [clients](#module_nodejs-poolController--clie
56
66
  ### Prerequisites
57
67
  If you don't know anything about NodeJS, these directions might be helpful.
58
68
 
59
- 1. Install Nodejs (v16+ required). (https://nodejs.org/en/download/)
69
+ 1. Install Nodejs (v20+ required). (https://nodejs.org/en/download/)
60
70
  1. Update NPM (https://docs.npmjs.com/getting-started/installing-node).
61
71
  1. It is recommended to clone the source code as updates are frequently pushed while releases are infrequent
62
72
  clone with `git clone https://github.com/tagyoureit/nodejs-poolController.git`
@@ -73,14 +83,99 @@ For a very thorough walk-through, see [this](https://www.troublefreepool.com/thr
73
83
  #### Upgrade Instructions
74
84
  Assuming you cloned the repo, the following are easy steps to get the latest version:
75
85
  1. Change directory to the njsPC app
76
- 2. `git pull`
77
- 3. `npm i` (not always necessary, but if dependencies are upgraded this will bring them up to date)
78
- 4. Start application as normal, or if using `npm run start:cached` then run `npm run build` to compile the code.
86
+ 2. **Important**: Ensure you have Node.js v20 or higher installed (`node --version`). If not, upgrade Node.js first.
87
+ 3. `git pull`
88
+ 4. **Important**: Run `npm i` to update dependencies. This is especially important when upgrading to version 8.3.0+ as it requires Node 20+ and has updated dependencies.
89
+ 5. Start application as normal, or if using `npm run start:cached` then run `npm run build` to compile the code.
79
90
 
80
91
  ### Docker instructions
81
92
 
82
93
  See the [wiki](https://github.com/tagyoureit/nodejs-poolController/wiki/Docker). Thanks @wurmr @andylippitt @emes.
83
94
 
95
+ ### Docker Compose (Controller + Optional dashPanel UI)
96
+
97
+ 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.
98
+
99
+ ```yaml
100
+ services:
101
+ njspc:
102
+ image: ghcr.io/sam2kb/njspc
103
+ container_name: njspc
104
+ restart: unless-stopped
105
+ environment:
106
+ - TZ=${TZ:-UTC}
107
+ - NODE_ENV=production
108
+ # Serial vs network connection options
109
+ # - POOL_NET_CONNECT=true
110
+ # - POOL_NET_HOST=raspberrypi
111
+ # - POOL_NET_PORT=9801
112
+ # Provide coordinates so sunrise/sunset (heliotrope) works immediately - change as needed
113
+ - POOL_LATITUDE=28.5383
114
+ - POOL_LONGITUDE=-81.3792
115
+ ports:
116
+ - "4200:4200"
117
+ devices:
118
+ - /dev/ttyACM0:/dev/ttyUSB0
119
+ # Persistence (create host directories/files first)
120
+ volumes:
121
+ - ./server-config.json:/app/config.json # Persisted config file on host
122
+ - njspc-data:/app/data # State & equipment snapshots
123
+ - njspc-backups:/app/backups # Backup archives
124
+ - njspc-logs:/app/logs # Logs
125
+ - njspc-bindings:/app/web/bindings/custom # Custom bindings
126
+ # OPTIONAL: If you get permission errors accessing /dev/tty*, prefer adding the container user to the host dialout/uucp group;
127
+ # only as a last resort temporarily uncomment the two lines below to run privileged/root (less secure).
128
+ # privileged: true
129
+ # user: "0:0"
130
+
131
+ njspc-dash:
132
+ image: ghcr.io/sam2kb/njspc-dash
133
+ container_name: njspc-dash
134
+ restart: unless-stopped
135
+ depends_on:
136
+ - njspc
137
+ environment:
138
+ - TZ=${TZ:-UTC}
139
+ - NODE_ENV=production
140
+ - POOL_WEB_SERVICES_IP=njspc # Link to backend service name
141
+ ports:
142
+ - "5150:5150"
143
+ volumes:
144
+ - ./dash-config.json:/app/config.json
145
+ - njspc-dash-data:/app/data
146
+ - njspc-dash-logs:/app/logs
147
+ - njspc-dash-uploads:/app/uploads
148
+
149
+ volumes:
150
+ njspc-data:
151
+ njspc-backups:
152
+ njspc-logs:
153
+ njspc-bindings:
154
+ njspc-dash-data:
155
+ njspc-dash-logs:
156
+ njspc-dash-uploads:
157
+ ```
158
+
159
+ Quick start:
160
+ 1. Save compose file.
161
+ 2. (Optional) create an empty config file: `touch dash-config.json`.
162
+ 3. `docker compose up -d`
163
+ 4. Visit Dash UI at: `http://localhost:5150`.
164
+
165
+ Notes:
166
+ * Provide either RS-485 device OR enable network (ScreenLogic) connection.
167
+ * Coordinates env vars prevent heliotrope warnings before the panel reports location.
168
+ * Persistence (controller):
169
+ * `./server-config.json:/app/config.json` main runtime config. You can either:
170
+ * Seed it with a copy of `defaultConfig.json` (`cp defaultConfig.json server-config.json`), OR
171
+ * Start with an empty file and the app will auto-populate it from defaults on first launch. If the file exists but contains invalid JSON it will be backed up to `config.corrupt-<timestamp>.json` and regenerated.
172
+ * Remaining state (data, backups, logs, custom bindings) is typically stored in named volumes in the provided compose for cleaner host directories. If you prefer bind mounts instead, replace the named volumes with host paths similar to the example below.
173
+ * Data artifacts: `poolConfig.json`, `poolState.json` etc. live under `/app/data` (volume `njspc-data`).
174
+ * Backups: `/app/backups` (volume `njspc-backups`).
175
+ * Logs: `/app/logs` (volume `njspc-logs`).
176
+ * Custom bindings: `/app/web/bindings/custom` (volume `njspc-bindings`).
177
+ * To migrate from bind mounts to named volumes, stop the stack, `docker run --rm -v oldPath:/from -v newVolume:/to alpine sh -c 'cp -a /from/. /to/'` for each path, then update compose.
178
+
84
179
  ### Automate startup of app
85
180
  See the [wiki](https://github.com/tagyoureit/nodejs-poolController/wiki/Automatically-start-at-boot---PM2-&-Systemd).
86
181
 
@@ -106,11 +201,11 @@ To do anything with this app, you need a client to connect to it. A client can
106
201
  ## Home Automation Bindings (previously Integrations)
107
202
 
108
203
  Available automations:
109
- * [Vera Home Automation Hub](https://github.com/rstrouse/nodejs-poolController-veraPlugin) - A plugin that integrates with nodejs-poolController. [Bindings Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Bindings-Integrations-in-2.0#vera)
110
- * [Hubitat](https://github.com/bsileo/hubitat_poolcontroller) by @bsileo (prev help from @johnny2678, @donkarnag, @arrmo). [Bindings Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Bindings-Integrations-in-2.0#smartthingshubitat)
204
+ * [Vera Home Automation Hub](https://github.com/rstrouse/nodejs-poolController-veraPlugin) - A plugin that integrates with nodejs-poolController. [Bindings Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Bindings-Integrations#vera)
205
+ * [Hubitat](https://github.com/bsileo/hubitat_poolcontroller) by @bsileo (prev help from @johnny2678, @donkarnag, @arrmo). [Bindings Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Bindings-Integrations#smartthingshubitat)
111
206
  * [Homebridge/Siri/EVE](https://github.com/gadget-monk/homebridge-poolcontroller) by @gadget-monk, adopted from @leftyflip
112
- * InfluxDB - [Bindings Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Bindings-Integrations-in-2.0#influx)
113
- * [MQTT](https://github.com/crsherman/nodejs-poolController-mqtt) original release by @crsherman, re-write by @kkzonie, testing by @baudfather and others. [Bindings Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Bindings-Integrations-in-2.0#mqtt)
207
+ * InfluxDB - [Bindings Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Bindings-Integrations#influx)
208
+ * [MQTT](https://github.com/crsherman/nodejs-poolController-mqtt) original release by @crsherman, re-write by @kkzonie, testing by @baudfather and others. [Bindings Directions](https://github.com/tagyoureit/nodejs-poolController/wiki/Bindings-Integrations#mqtt)
114
209
  * [Homeseer](https://github.com/tagyoureit/nodejs-poolController/wiki/Homeseer-Setup-Instructions) - Integration directions by @miamijerry to integrate Homeseer through MQTT
115
210
 
116
211
  Outdated:
@@ -138,6 +233,29 @@ Most of these can be configured directly from the UI in dashPanel.
138
233
  * `netConnect` - used to connect via [Socat](https://github.com/tagyoureit/nodejs-poolController/wiki/Socat)
139
234
  * `netHost` and `netPort` - host and port for Socat connection.
140
235
  * `inactivityRetry` - # of seconds the app should wait before trying to reopen the port after no communications. If your equipment isn't on all the time or you are running a virtual controller you may want to dramatically increase the timeout so you don't get console warnings.
236
+ * `txDelays` - (optional) fine‑grained transmit pacing controls added in 8.1+ to better coexist with busy or bridged (socat / multiple panel / dual chlorinator) RS‑485 buses. These values are all in milliseconds. If the block is omitted, internal defaults are used (see `defaultConfig.json`). All values can be hot‑reloaded from config.
237
+ * `idleBeforeTxMs` – Minimum quiet time on the bus (no RX or TX seen) before a new outbound frame may start. Helps avoid collisions just after another device finishes talking. Typical: 40‑80. Set to 0 to disable.
238
+ * `interFrameDelayMs` – Delay inserted between completed outbound attempts (success, retry scheduling, or queue drain) and evaluation of the next outbound message. Replaces the previous fixed 100ms. Typical: 30‑75. Lower values increase throughput but may raise collision / rewind counts.
239
+ * `interByteDelayMs` – Optional per‑byte pacing inside a single frame. Normally 0 (disabled). Set to 1‑2ms only if you observe hardware or USB adapter overruns, or are experimenting with very marginal wiring / long cable runs.
240
+
241
+ Example tuning block - more conservative pacing for SunTouch that works way better than defaults:
242
+
243
+ ```json
244
+ "txDelays": {
245
+ "idleBeforeTxMs": 60,
246
+ "interFrameDelayMs": 50,
247
+ "interByteDelayMs": 1
248
+ }
249
+ ```
250
+
251
+ Tuning guidance:
252
+ - Start with the defaults. Only change one value at a time and observe stats (collisions, retries, rewinds) via rs485PortStats.
253
+ - If you see frequent outbound retries or receive rewinds, first raise `idleBeforeTxMs` in small steps (e.g. +10ms) before touching `interFrameDelayMs`.
254
+ - If overall throughput feels sluggish but collisions are low, you may lower `interFrameDelayMs` gradually.
255
+ - Use `interByteDelayMs` only as a last resort; it elongates every frame and reduces total bus capacity.
256
+ - Setting any value too high will simply slow configuration bursts (e.g. on startup); setting them too low can cause more retries and ultimately lower effective throughput.
257
+
258
+ All three parameters are safe to adjust without restarting; edits to `config.json` are picked up by the existing config watcher.
141
259
 
142
260
  ## Web section - controls various aspects of external communications
143
261
  * `servers` - setting for different servers/services
package/config/Config.ts CHANGED
@@ -36,21 +36,52 @@ class Config {
36
36
  // RKS 05-18-20: This originally had multiple points of failure where it was not in the try/catch.
37
37
  try {
38
38
  this._isLoading = true;
39
- this._cfg = fs.existsSync(this.cfgPath) ? JSON.parse(fs.readFileSync(this.cfgPath, "utf8").trim()) : {};
39
+ // Read user config (if present) with graceful handling of empty or invalid JSON.
40
+ let userCfg: any = {};
41
+ if (fs.existsSync(this.cfgPath)) {
42
+ try {
43
+ const raw = fs.readFileSync(this.cfgPath, "utf8");
44
+ const trimmed = raw.trim();
45
+ if (trimmed.length > 0) userCfg = JSON.parse(trimmed);
46
+ else {
47
+ console.log(`Config file '${ this.cfgPath }' is empty. Populating with defaults.`);
48
+ }
49
+ } catch (parseErr: any) {
50
+ // Backup corrupt file then continue with defaults.
51
+ try {
52
+ const backupName = this.cfgPath.replace(/\.json$/i, `.corrupt-${ Date.now() }.json`);
53
+ fs.copyFileSync(this.cfgPath, backupName);
54
+ console.log(`Config file '${ this.cfgPath }' contained invalid JSON and was backed up to '${ backupName }'. Using defaults.`);
55
+ } catch (backupErr: any) {
56
+ console.log(`Failed to backup corrupt config file '${ this.cfgPath }': ${ backupErr.message }`);
57
+ }
58
+ userCfg = {};
59
+ }
60
+ }
40
61
  const def = JSON.parse(fs.readFileSync(path.join(process.cwd(), "/defaultConfig.json"), "utf8").trim());
41
62
  const packageJson = JSON.parse(fs.readFileSync(path.join(process.cwd(), "/package.json"), "utf8").trim());
42
- this._cfg = extend(true, {}, def, this._cfg, { appVersion: packageJson.version });
63
+ this._cfg = extend(true, {}, def, userCfg, { appVersion: packageJson.version });
43
64
  this._isInitialized = true;
44
65
  this.updateAsync((err) => {
45
66
  if (typeof err === 'undefined') {
46
67
  fs.watch(this.cfgPath, (event, fileName) => {
47
68
  if (fileName && event === 'change') {
48
- if (self._isLoading) return; // Need a debounce here. We will use a semaphore to cause it not to load more than once.
69
+ if (self._isLoading) return; // Debounce via semaphore.
49
70
  console.log('Updating config file');
50
71
  const stats = fs.statSync(self.cfgPath);
51
72
  if (stats.mtime.valueOf() === self._fileTime.valueOf()) return;
52
- this._cfg = fs.existsSync(this.cfgPath) ? JSON.parse(fs.readFileSync(this.cfgPath, "utf8")) : {};
53
- this._cfg = extend(true, {}, def, this._cfg, { appVersion: packageJson.version });
73
+ let changedCfg: any = {};
74
+ if (fs.existsSync(self.cfgPath)) {
75
+ try {
76
+ const raw2 = fs.readFileSync(self.cfgPath, "utf8");
77
+ const trimmed2 = raw2.trim();
78
+ if (trimmed2.length > 0) changedCfg = JSON.parse(trimmed2);
79
+ else console.log(`Watched config file is empty; continuing with defaults + existing overrides.`);
80
+ } catch (e: any) {
81
+ console.log(`Error parsing updated config file. Retaining existing configuration. Error: ${ e.message }`);
82
+ }
83
+ }
84
+ this._cfg = extend(true, {}, def, changedCfg, { appVersion: packageJson.version });
54
85
  logger.init(); // only reload logger for now; possibly expand to other areas of app
55
86
  logger.info(`Reloading app config: ${fileName}`);
56
87
  this.emitter.emit('reloaded', this._cfg);
@@ -63,8 +94,7 @@ class Config {
63
94
  this.getEnvVariables();
64
95
  } catch (err) {
65
96
  console.log(`Error reading configuration information. Aborting startup: ${ err }`);
66
- // Rethrow this error so we exit the app with the appropriate pause in the console.
67
- throw err;
97
+ throw err; // Only throw if defaults/package.json could not be read.
68
98
  }
69
99
  }
70
100
  public async updateAsync(callback?: (err?) => void) {
@@ -189,6 +219,26 @@ class Config {
189
219
  this._cfg.controller.comms.netPort = env.POOL_NET_PORT;
190
220
  bUpdate = true;
191
221
  }
222
+ // Allow overriding location coordinates for heliotrope calculations
223
+ if (typeof env.POOL_LATITUDE !== 'undefined') {
224
+ const lat = parseFloat(env.POOL_LATITUDE as any);
225
+ if (!isNaN(lat) && (!this._cfg.controller?.general?.location || this._cfg.controller.general.location.latitude !== lat)) {
226
+ // Ensure nested objects exist
227
+ this._cfg.controller.general = this._cfg.controller.general || {};
228
+ this._cfg.controller.general.location = this._cfg.controller.general.location || {};
229
+ this._cfg.controller.general.location.latitude = lat;
230
+ bUpdate = true;
231
+ }
232
+ }
233
+ if (typeof env.POOL_LONGITUDE !== 'undefined') {
234
+ const lon = parseFloat(env.POOL_LONGITUDE as any);
235
+ if (!isNaN(lon) && (!this._cfg.controller?.general?.location || this._cfg.controller.general.location.longitude !== lon)) {
236
+ this._cfg.controller.general = this._cfg.controller.general || {};
237
+ this._cfg.controller.general.location = this._cfg.controller.general.location || {};
238
+ this._cfg.controller.general.location.longitude = lon;
239
+ bUpdate = true;
240
+ }
241
+ }
192
242
  if (bUpdate) this.updateAsync();
193
243
  }
194
244
  }