Add 'ultratempdirect' heater type for direct RS-485 UltraTemp control#1152
Add 'ultratempdirect' heater type for direct RS-485 UltraTemp control#1152bestander wants to merge 1 commit intotagyoureit:masterfrom
Conversation
In my original pool setup as configured by the installer, UltraTemp was setup as a Solar heater. This caused a lot of unreliability in HeatPump controls because the EasyTouch controller may override the heatpump operations. I wanted to have a direct control over the heatpump and implemented a new heater type that talks to UltraTemp directly via RS-485 Action 114/115, bypassing the OCP entirely. Previously doing this required hacking existing heater types with workaround flags (nixieOverride). This adds a clean new heater type (id 8, 'ultratempdirect') that: - Always uses the NixieUltratemp RS-485 class (Action 114/115 protocol) - Always has master=1 (njsPC controls it, not the OCP) - Has a user-configurable RS-485 address - Uses heater id >255 so EasyTouch config messages don't interfere - Supports both heating and cooling via the UltraTemp thermostat logic Changes: - Add type 8 to heaterTypes valueMaps (SystemBoard, EasyTouchBoard, IntelliCenterBoard) - Add 'ultratempdirect' case to NixieHeaterBase factory, syncHeaterStates thermostat logic, and heat mode/source availability checks - Count ultratempdirect as heat pump installed so Heat Pump / Heat Pump Preferred modes appear in body heat mode options - Improve processUltraTempStatus() with null-safety checks, state change logging, and guard against unconfigured heat pump addresses - Auto-initialize NCP heaters on first use to handle the race condition where syncHeaterStates runs before ncp.initAsync completes - Sort master=1 heaters before master=0 in syncHeaterStates so NCP-controlled heaters take priority over OCP ghost heaters - Accept 'heatpump'/'heatpumppref' heat modes in the ultratemp thermostat case so the type works with standard heat pump mode selection
PR #1152 Review:
|
| Val | Name | Hardware |
|---|---|---|
| 1 | gas | Gas Heater |
| 2 | solar | Solar Heater |
| 3 | heatpump | Heat Pump |
| 4 | ultratemp | UltraTemp |
| 5 | hybrid | UltraTemp ETi Hybrid |
| 6 | mastertemp | MasterTemp |
| 7 | maxetherm | Max-E-Therm |
An ultratempdirect heater is not a different piece of hardware. It's the exact same UltraTemp heat pump, sitting on the same RS-485 bus, speaking the same Action 114/115 protocol. The only difference is who is sending the commands -- the OCP or njsPC. That's a control method distinction, not a hardware distinction.
The Precedent Problem
If we accept "direct control" as a reason to create a new type, the same argument applies to every other RS-485 heater. Someone with a MasterTemp could want njsPC to bypass their EasyTouch and send Action 112/116 directly. Someone with a Hybrid ETi could want the same via Action 112/113. We'd end up with:
mastertempdirect-- MasterTemp bypasshybriddirect-- UltraTemp ETi Hybrid bypassheatpumpdirect-- generic heat pump bypass- And potentially similar patterns for pumps, chlorinators, and any other equipment
Each new "direct" type adds entries to valueMaps across SystemBoard, EasyTouchBoard, and IntelliCenterBoard. Each needs case statements in syncHeaterStates, getInstalledHeaterTypes, updateHeaterServices, and the dashPanel UI. The maintenance burden grows linearly with every piece of equipment someone wants to control directly.
The Right Abstraction Already Exists
The codebase already solved this problem at a design level. There are two properties on every equipment item that together answer the question "who controls this device and how do they reach it":
-
heater.master(0=OCP, 1=NCP) -- This is the "who controls it" switch. Whenmaster=0, the OCP owns the device and njsPC just listens. Whenmaster=1, njsPC/Nixie takes direct control. TheEasyTouchBoard.setHeaterAsync()already checks for this: ifmaster === 1, it delegates toSystemBoard.setHeaterAsync()which creates a Nixie-managed heater. This delegation is already in the code and works today. -
heater.portId(0=primary, 1+=aux) -- This is the "which wire" switch. It routes RS-485 traffic to a specific USB adapter. Every heater already has this property, and theNixieUltratempclass already usesthis.heater.portIdwhen creating outbound Action 114 messages. -
NixieUltratempclass -- Already knows how to send Action 114 commands and receive Action 115 responses. It already handles thermostat logic, cooling, startup delays, and communication error tracking. No code changes are needed to make it work with amaster=1heater of the existingultratemptype. -
Multi-port RS-485 --
Comms.tsalready supports multiplecontroller.comms*sections, each creating a separateRS485Port. This infrastructure exists for exactly this kind of scenario.
The right answer is master=1 on an existing ultratemp type, not a new type. The PR author's ultratempdirect essentially reimplements what master=1 already provides, but baked into the type system where it doesn't belong.
4. Manual Testing (JSON Editing)
To prove that the existing master=1 mechanism works without a new heater type, you can test it today by editing poolConfig.json directly. This requires no code changes (apart from the cherry-picked bug fixes above). The trade-off is that there's currently no UI for setting master=1 on a heater -- you have to edit JSON. Section 6 addresses closing that gap properly.
Prerequisites
Before doing this, the UltraTemp must be removed from the OCP's heater configuration (set it to "none" on the EasyTouch panel). This is the critical step. The OCP and njsPC cannot both command the same heater on the same bus -- if they do, the UltraTemp receives conflicting Action 114 commands and rapid-cycles between heat/off states. One controller must own the heater exclusively.
Steps
-
Stop njsPC.
-
Edit
data/poolConfig.json. Add a new entry to theheatersarray:
{
"id": 256,
"isActive": true,
"portId": 0,
"type": 4,
"body": 32,
"master": 1,
"cooldownDelay": 5,
"startTempDelta": 5,
"stopTempDelta": 3,
"coolingEnabled": true,
"differentialTemp": 3,
"address": 112,
"name": "UltraTemp Direct"
}Key fields:
id: 256-- Must be >= 256 so EasyTouch config messages (which manage ids 1-255) don't overwrite it.type: 4-- UltraTemp (existing type, not a new one).master: 1-- Tells njsPC this heater is controlled by the Nixie Control Panel, not the OCP.portId: 0-- Use the primary RS-485 bus (same bus as OCP). Set to1if using a 2nd USB adapter.address: 112-- The UltraTemp's RS-485 address (default 112; check the heat pump's INTELL ADDRESS menu setting).body: 32-- Pool/Spa shared body. Use0for pool-only or1for spa-only.
-
Restart njsPC. The
NixieHeaterCollection.initAsync()will findmaster === 1, create aNixieUltratempinstance, and begin polling/commanding the UltraTemp via Action 114/115 on portId 0. -
Verify. Check the njsPC log for:
Initializing Heater UltraTemp Direct-- confirms Nixie picked it upUltraTemp heartbeat: src=112 status=...-- confirms RS-485 communication (requires cherry-pick 2b)
5. Alternative A: 2nd USB Stick (Zero Code Changes)
The manual test in section 4 shares a single RS-485 bus between the OCP and njsPC, which works but requires removing the UltraTemp from the OCP's config. Some users may not want to (or can't) change their OCP configuration. For those cases, a second USB RS-485 adapter (~$10-15) gives the UltraTemp its own dedicated bus, completely eliminating the dual-master concern.
The idea is simple: physically move the UltraTemp's RS-485 wires off the OCP's bus and onto a private bus that only njsPC can see. The OCP will show a heater communication error (it's trying to talk to a device that's no longer on its bus), but that's cosmetic -- the UltraTemp is happily taking orders from njsPC on its own private line.
Setup
-
Physically rewire the UltraTemp's RS-485 (yellow/green wires) from the OCP's com port to the 2nd USB adapter.
-
Add a second comms section to
config.json:
{
"controller": {
"comms": {
"type": "local",
"portId": 0,
"enabled": true,
"rs485Port": "/dev/ttyUSB0"
},
"comms1": {
"type": "local",
"portId": 1,
"enabled": true,
"rs485Port": "/dev/ttyUSB1",
"portSettings": {
"baudRate": 9600,
"dataBits": 8,
"parity": "none",
"stopBits": 1,
"rtscts": false,
"autoOpen": false,
"lock": false
}
}
}
}- Set the heater's
portId: 1inpoolConfig.json(same JSON as section 4, but with"portId": 1).
Advantages
- Complete bus isolation: OCP cannot send conflicting commands to UltraTemp
- OCP config doesn't need to change (it will show a heater comm error, but that's cosmetic)
- Works with the existing codebase today
Known Gap
hasAssignedEquipment() in controller/comms/Comms.ts (line 1256) only checks pumps and chlorinators, not heaters. This means the aux port won't report having assigned equipment. This is a trivial fix (add a heater check) but not strictly required for functionality.
6. Alternative B: Small UI Changes (Recommended)
The manual JSON editing in section 4 proves the concept, but "stop the app and edit JSON" isn't a real solution for end users. The reason the PR author created a new heater type in the first place is that the dashPanel UI has no way to set master=1 on a heater. There's no dropdown, no checkbox, no toggle. The master property exists in the data model, the backend fully supports it, but the UI simply doesn't expose it.
This is already a solved problem elsewhere in the codebase. The chlorinator configuration UI has a "Controlled By" dropdown that lets you choose between OCP and NCP, and a port selector for choosing which USB adapter to use. The heater config UI just needs the same treatment.
The cleanest long-term solution is to expose the existing master and portId properties in the dashPanel heater configuration UI, matching the pattern already used for chlorinators. This is a small change -- roughly two lines on the backend and one dropdown on the frontend -- but it unlocks direct control for every heater type, not just UltraTemp.
njsPC Change
Add equipmentMasters and rs485ports to the /config/options/heaters response in web/services/config/Config.ts. Currently the heater options return:
// Current (line 237-252)
let opts = {
tempUnits: ...,
bodies: ...,
maxHeaters: ...,
heaters: sys.heaters.get(),
heaterTypes: sys.board.valueMaps.heaterTypes.toArray(),
heatModes: ...,
coolDownDelay: ...
};Should be extended to match the chlorinator pattern (line 370-381):
let opts = {
// ... existing fields ...
equipmentMasters: sys.board.valueMaps.equipmentMaster.toArray(),
rs485ports: await conn.listInstalledPorts()
};The equipmentMaster valueMap already exists:
| Val | Name | Description |
|---|---|---|
| 0 | ocp | Outdoor Control Panel |
| 1 | ncp | Nixie Control Panel |
| 2 | ext | External Control Panel |
dashPanel Change
Add a "Controlled By" dropdown to the heater config form (the same pattern used in the chlorinator config UI). When the user selects "Nixie Control Panel" (master=1):
- The
PUT /config/heaterendpoint receives{ master: 1, type: "ultratemp", ... } EasyTouchBoard.setHeaterAsync()seesmaster === 1and delegates toSystemBoard.setHeaterAsync()SystemBoard.setHeaterAsync()assignsid >= 256and creates a Nixie-managed heaterNixieUltratempclass handles all RS-485 communication
Optionally add a "Port" dropdown for users with multiple USB adapters.
Why This is Better Than a New Type
This approach solves the problem generically instead of one heater model at a time:
- No new heater types needed -- Works for UltraTemp, MasterTemp, Hybrid, or any future equipment that someone wants to control directly. One UI change covers them all.
- Follows existing patterns -- The chlorinator UI already does this exact thing. We're not inventing anything new.
- Small change footprint -- ~2 lines in njsPC to add the options to the API response, and one dropdown in dashPanel. Compare that to the 211 lines across 6 files that
ultratempdirectrequired. - Backend already fully supports it --
EasyTouchBoard.setHeaterAsync()already delegates toSystemBoard.setHeaterAsync()when it seesmaster === 1. TheNixieUltratempclass already handles Action 114/115. The only missing piece is letting the user flip the switch from the UI.
7. Single Bus Feasibility
Can njsPC and the OCP share a single RS-485 bus for direct heater control?
Yes, with one critical requirement: the OCP must NOT also be commanding the same heater.
This is a common question because adding a second USB adapter feels like extra complexity. The good news is that RS-485 is inherently a multi-drop bus -- the OCP, pumps, chlorinators, and heaters are already all sharing it. njsPC is already on that same bus (portId=0), listening to everything and occasionally sending commands. Adding heater control to njsPC's responsibilities on that bus is not fundamentally different from what it already does.
The bus contention concern (two devices trying to transmit at the same time) is already handled. njsPC has TX delays and retry logic in Comms.ts that prevent collisions with the OCP's traffic. The NixieUltratemp class retries Action 114 commands up to 3 times if a response isn't received.
The real danger isn't bus contention -- it's dual-master conflict. If EasyTouch has the UltraTemp configured (as Solar, HeatPump, or any type), the OCP will periodically send its own Action 114 commands based on its own thermostat logic. Meanwhile njsPC sends its own Action 114 commands based on Nixie thermostat logic. The UltraTemp doesn't know or care who is talking to it -- it simply obeys the last command it received. The result is the heater flickering between heat/off as two controllers fight over it.
Resolution: Remove the heater from the OCP's configuration. Once the OCP doesn't know about the heater, it stops sending Action 114 entirely. njsPC becomes the sole master. The 2nd USB stick (section 5) is the alternative for users who can't or won't change their OCP config -- it achieves the same isolation through physical separation rather than configuration.
|
For points.
I’ll test the suggestion and will update the pr.
…On Mon, Feb 16, 2026 at 12:11 PM tagyoureit ***@***.***> wrote:
*tagyoureit* left a comment (tagyoureit/nodejs-poolController#1152)
<#1152 (comment)>
PR #1152 <#1152>
Review: ultratempdirect Heater Type
*PRs:* #1152 (njsPC)
<#1152> / #103
(dashPanel)
<rstrouse/nodejs-poolController-dashPanel#103>
*Branch:* heatpumpdirect -> master
*Date:* February 16, 2026
------------------------------
1. Overview
PR #1152 <#1152>
adds a new heater type ultratempdirect (type id 8) that lets njsPC talk
directly to an UltraTemp heat pump via RS-485 Action 114/115, bypassing the
EasyTouch OCP entirely.
The Real-World Problem
The PR author has a legitimate frustration. Their pool installer
configured their UltraTemp heat pump as a "Solar" heater type on the
EasyTouch controller. This is a common installer shortcut, but it causes
real problems: the EasyTouch OCP applies solar differential logic
(start/stop temperature deltas, efficiency calculations) to what is
actually a heat pump. The result is unreliable heat pump behavior -- the
OCP makes heating decisions based on rules designed for solar panels, not
compressor-driven heat pumps.
Beyond this specific misconfiguration, EasyTouch has well-documented
limitations with UltraTemp control in general. The controller cannot
independently schedule when the heat pump runs, and RS-485 remote mode is
known to cause rapid on/off cycling (every 5-8 minutes) due to poor
temperature deadband handling. These are hardware/firmware limitations in
the OCP that users understandably want to work around.
The PR author's solution was to have njsPC take over direct RS-485 control
of the UltraTemp, bypassing the OCP entirely. This is a sound goal -- the
problem is in how it was implemented.
What the PR Does
The PR introduces a brand new heater type (ultratempdirect, type 8) to
represent "an UltraTemp that njsPC controls directly." Under the hood, it
routes to the exact same NixieUltratemp RS-485 class that the existing
ultratemp type already uses when configured with master=1. The new type
exists primarily to signal "don't let the OCP touch this" -- but as we'll
see in section 3, there's already a mechanism for that.
*Files changed (6):*
- controller/boards/SystemBoard.ts -- adds type 8, modifies heat
mode/source availability, adds master=1 sort priority
- controller/boards/EasyTouchBoard.ts -- adds type 8, suppresses OCP
ghost heaters when ultratempdirect present
- controller/boards/IntelliCenterBoard.ts -- adds type 8
- controller/comms/messages/Messages.ts -- adds Protocol.Heater
response matching
- controller/comms/messages/status/HeaterStateMessage.ts --
null-safety, state-change logging
- controller/nixie/heaters/Heater.ts -- routes ultratempdirect to
NixieUltratemp, auto-init race fix
PR #103 <#103>
(dashPanel) is trivial: adds case 'ultratempdirect': fallthrough to the
ultratemp glyph/options in scripts/config/heaters.js.
------------------------------
2. Valid Bug Fixes Worth Cherry-Picking
Not everything in this PR should be rejected. Mixed in with the new heater
type are four genuine bug fixes and improvements that stand on their own.
These should be cherry-picked into master regardless of what happens with
the ultratempdirect type itself, because they improve direct heater
control for any master=1 heater configuration:
2a. Protocol.Heater Response Matching (Messages.ts)
Adds response matching for the heater protocol so njsPC can correctly pair
Action 114 requests with Action 115 responses. Without this, direct heater
control (for any master=1 heater) may fail to match responses.
if (msgOut.protocol === Protocol.Heater) {
if (msgIn.source !== msgOut.dest || (msgIn.dest !== msgOut.source && msgIn.dest !== 16)) { return false; }
if (this.action > 0 && this.action === msgIn.action) return true;
return false;}
2b. Null-Safety + Logging (HeaterStateMessage.ts)
Guards processUltraTempStatus() against unconfigured heater addresses
(prevents crash if an Action 115 arrives from an unknown heater) and adds
state-change logging for debugging:
let heater: Heater = sys.heaters.getItemByAddress(msg.source);if (typeof heater === 'undefined' || !heater.isActive) {
msg.isProcessed = true;
return;}// ... state change logging ...if (prevOn !== sheater.isOn || prevCooling !== sheater.isCooling) {
logger.info(`UltraTemp heartbeat: src=${msg.source} status=${byte} ...`);}
2c. Auto-Init Race Condition Fix (Heater.ts)
Handles the case where syncHeaterStates runs before ncp.initAsync
completes. Instead of rejecting with an error, it auto-initializes the
heater on first use:
if (typeof h === 'undefined') {
let heater = sys.heaters.getItemById(hstate.id);
if (typeof heater !== 'undefined' && heater.master === 1 && heater.isActive !== false) {
logger.info(`NCP: Auto-initializing heater ${hstate.id}-${heater.name}`);
h = await this.initHeaterAsync(heater);
}}
2d. master=1 Sort Priority (SystemBoard.ts)
Sorts NCP-controlled heaters (master=1) before OCP-controlled heaters
(master=0) in syncHeaterStates, so directly controlled heaters get
priority over OCP ghost heaters:
bodyHeaters.sort((a, b) => {
if (a.master !== b.master) return b.master - a.master;
if (heaterTypes.transform(a.type).hasPreference) return -1;
else if (heaterTypes.transform(b.type).hasPreference) return 1;
return 0;});
------------------------------
3. Architectural Concern: Why a New Heater Type is the Wrong Approach
*Recommendation: Do not merge the new ultratempdirect type.*
The Core Issue: Confusing "What It Is" with "Who Controls It"
The heaterTypes valueMap in njsPC answers one question: *what kind of
heater hardware is this?* Each entry represents a physically different
piece of equipment with its own RS-485 protocol:
Val Name Hardware
1 gas Gas Heater
2 solar Solar Heater
3 heatpump Heat Pump
4 ultratemp UltraTemp
5 hybrid UltraTemp ETi Hybrid
6 mastertemp MasterTemp
7 maxetherm Max-E-Therm
An ultratempdirect heater is not a different piece of hardware. It's the
exact same UltraTemp heat pump, sitting on the same RS-485 bus, speaking
the same Action 114/115 protocol. The only difference is *who is sending
the commands* -- the OCP or njsPC. That's a control method distinction,
not a hardware distinction.
The Precedent Problem
If we accept "direct control" as a reason to create a new type, the same
argument applies to every other RS-485 heater. Someone with a MasterTemp
could want njsPC to bypass their EasyTouch and send Action 112/116
directly. Someone with a Hybrid ETi could want the same via Action 112/113.
We'd end up with:
- mastertempdirect -- MasterTemp bypass
- hybriddirect -- UltraTemp ETi Hybrid bypass
- heatpumpdirect -- generic heat pump bypass
- And potentially similar patterns for pumps, chlorinators, and any
other equipment
Each new "direct" type adds entries to valueMaps across SystemBoard,
EasyTouchBoard, and IntelliCenterBoard. Each needs case statements in
syncHeaterStates, getInstalledHeaterTypes, updateHeaterServices, and the
dashPanel UI. The maintenance burden grows linearly with every piece of
equipment someone wants to control directly.
The Right Abstraction Already Exists
The codebase already solved this problem at a design level. There are two
properties on every equipment item that together answer the question "who
controls this device and how do they reach it":
1.
*heater.master* (0=OCP, 1=NCP) -- This is the "who controls it"
switch. When master=0, the OCP owns the device and njsPC just listens.
When master=1, njsPC/Nixie takes direct control. The
EasyTouchBoard.setHeaterAsync() already checks for this: if master ===
1, it delegates to SystemBoard.setHeaterAsync() which creates a
Nixie-managed heater. This delegation is already in the code and works
today.
2.
*heater.portId* (0=primary, 1+=aux) -- This is the "which wire"
switch. It routes RS-485 traffic to a specific USB adapter. Every heater
already has this property, and the NixieUltratemp class already uses
this.heater.portId when creating outbound Action 114 messages.
3.
*NixieUltratemp class* -- Already knows how to send Action 114
commands and receive Action 115 responses. It already handles thermostat
logic, cooling, startup delays, and communication error tracking. No code
changes are needed to make it work with a master=1 heater of the
existing ultratemp type.
4.
*Multi-port RS-485* -- Comms.ts already supports multiple
controller.comms* sections, each creating a separate RS485Port. This
infrastructure exists for exactly this kind of scenario.
The right answer is master=1 on an existing ultratemp type, not a new
type. The PR author's ultratempdirect essentially reimplements what
master=1 already provides, but baked into the type system where it
doesn't belong.
------------------------------
4. Manual Testing (JSON Editing)
To prove that the existing master=1 mechanism works without a new heater
type, you can test it today by editing poolConfig.json directly. This
requires no code changes (apart from the cherry-picked bug fixes above).
The trade-off is that there's currently no UI for setting master=1 on a
heater -- you have to edit JSON. Section 6 addresses closing that gap
properly.
Prerequisites
Before doing this, the UltraTemp must be removed from the OCP's heater
configuration (set it to "none" on the EasyTouch panel). This is the
critical step. The OCP and njsPC cannot both command the same heater on the
same bus -- if they do, the UltraTemp receives conflicting Action 114
commands and rapid-cycles between heat/off states. One controller must own
the heater exclusively.
Steps
1.
*Stop njsPC.*
2.
*Edit data/poolConfig.json.* Add a new entry to the heaters array:
{
"id": 256,
"isActive": true,
"portId": 0,
"type": 4,
"body": 32,
"master": 1,
"cooldownDelay": 5,
"startTempDelta": 5,
"stopTempDelta": 3,
"coolingEnabled": true,
"differentialTemp": 3,
"address": 112,
"name": "UltraTemp Direct"
}
Key fields:
- *id: 256* -- Must be >= 256 so EasyTouch config messages (which
manage ids 1-255) don't overwrite it.
- *type: 4* -- UltraTemp (existing type, not a new one).
- *master: 1* -- Tells njsPC this heater is controlled by the Nixie
Control Panel, not the OCP.
- *portId: 0* -- Use the primary RS-485 bus (same bus as OCP). Set to 1
if using a 2nd USB adapter.
- *address: 112* -- The UltraTemp's RS-485 address (default 112; check
the heat pump's INTELL ADDRESS menu setting).
- *body: 32* -- Pool/Spa shared body. Use 0 for pool-only or 1 for
spa-only.
3.
*Restart njsPC.* The NixieHeaterCollection.initAsync() will find master
=== 1, create a NixieUltratemp instance, and begin polling/commanding
the UltraTemp via Action 114/115 on portId 0.
4.
*Verify.* Check the njsPC log for:
- Initializing Heater UltraTemp Direct -- confirms Nixie picked it up
- UltraTemp heartbeat: src=112 status=... -- confirms RS-485
communication (requires cherry-pick 2b)
------------------------------
5. Alternative A: 2nd USB Stick (Zero Code Changes)
The manual test in section 4 shares a single RS-485 bus between the OCP
and njsPC, which works but requires removing the UltraTemp from the OCP's
config. Some users may not want to (or can't) change their OCP
configuration. For those cases, a second USB RS-485 adapter (~$10-15) gives
the UltraTemp its own dedicated bus, completely eliminating the dual-master
concern.
The idea is simple: physically move the UltraTemp's RS-485 wires off the
OCP's bus and onto a private bus that only njsPC can see. The OCP will show
a heater communication error (it's trying to talk to a device that's no
longer on its bus), but that's cosmetic -- the UltraTemp is happily taking
orders from njsPC on its own private line.
Setup
1.
*Physically rewire* the UltraTemp's RS-485 (yellow/green wires) from
the OCP's com port to the 2nd USB adapter.
2.
*Add a second comms section* to config.json:
{
"controller": {
"comms": {
"type": "local",
"portId": 0,
"enabled": true,
"rs485Port": "/dev/ttyUSB0"
},
"comms1": {
"type": "local",
"portId": 1,
"enabled": true,
"rs485Port": "/dev/ttyUSB1",
"portSettings": {
"baudRate": 9600,
"dataBits": 8,
"parity": "none",
"stopBits": 1,
"rtscts": false,
"autoOpen": false,
"lock": false
}
}
}
}
3. *Set the heater's portId: 1* in poolConfig.json (same JSON as
section 4, but with "portId": 1).
Advantages
- Complete bus isolation: OCP cannot send conflicting commands to
UltraTemp
- OCP config doesn't need to change (it will show a heater comm error,
but that's cosmetic)
- Works with the existing codebase today
Known Gap
hasAssignedEquipment() in controller/comms/Comms.ts (line 1256) only
checks pumps and chlorinators, not heaters. This means the aux port won't
report having assigned equipment. This is a trivial fix (add a heater
check) but not strictly required for functionality.
------------------------------
6. Alternative B: Small UI Changes (Recommended)
The manual JSON editing in section 4 proves the concept, but "stop the app
and edit JSON" isn't a real solution for end users. The reason the PR
author created a new heater type in the first place is that *the
dashPanel UI has no way to set master=1 on a heater*. There's no
dropdown, no checkbox, no toggle. The master property exists in the data
model, the backend fully supports it, but the UI simply doesn't expose it.
This is already a solved problem elsewhere in the codebase. The
chlorinator configuration UI has a "Controlled By" dropdown that lets you
choose between OCP and NCP, and a port selector for choosing which USB
adapter to use. The heater config UI just needs the same treatment.
The cleanest long-term solution is to expose the existing master and
portId properties in the dashPanel heater configuration UI, matching the
pattern already used for chlorinators. This is a small change -- roughly
two lines on the backend and one dropdown on the frontend -- but it unlocks
direct control for every heater type, not just UltraTemp.
njsPC Change
Add equipmentMasters and rs485ports to the /config/options/heaters
response in web/services/config/Config.ts. Currently the heater options
return:
// Current (line 237-252)let opts = {
tempUnits: ...,
bodies: ...,
maxHeaters: ...,
heaters: sys.heaters.get(),
heaterTypes: sys.board.valueMaps.heaterTypes.toArray(),
heatModes: ...,
coolDownDelay: ...};
Should be extended to match the chlorinator pattern (line 370-381):
let opts = {
// ... existing fields ...
equipmentMasters: sys.board.valueMaps.equipmentMaster.toArray(),
rs485ports: await conn.listInstalledPorts()};
The equipmentMaster valueMap already exists:
Val Name Description
0 ocp Outdoor Control Panel
1 ncp Nixie Control Panel
2 ext External Control Panel dashPanel Change
Add a "Controlled By" dropdown to the heater config form (the same pattern
used in the chlorinator config UI). When the user selects "Nixie Control
Panel" (master=1):
- The PUT /config/heater endpoint receives { master: 1, type:
"ultratemp", ... }
- EasyTouchBoard.setHeaterAsync() sees master === 1 and delegates to
SystemBoard.setHeaterAsync()
- SystemBoard.setHeaterAsync() assigns id >= 256 and creates a
Nixie-managed heater
- NixieUltratemp class handles all RS-485 communication
Optionally add a "Port" dropdown for users with multiple USB adapters.
Why This is Better Than a New Type
This approach solves the problem generically instead of one heater model
at a time:
- *No new heater types needed* -- Works for UltraTemp, MasterTemp,
Hybrid, or any future equipment that someone wants to control directly. One
UI change covers them all.
- *Follows existing patterns* -- The chlorinator UI already does this
exact thing. We're not inventing anything new.
- *Small change footprint* -- ~2 lines in njsPC to add the options to
the API response, and one dropdown in dashPanel. Compare that to the 211
lines across 6 files that ultratempdirect required.
- *Backend already fully supports it* --
EasyTouchBoard.setHeaterAsync() already delegates to
SystemBoard.setHeaterAsync() when it sees master === 1. The
NixieUltratemp class already handles Action 114/115. The only missing
piece is letting the user flip the switch from the UI.
------------------------------
7. Single Bus Feasibility
*Can njsPC and the OCP share a single RS-485 bus for direct heater
control?*
*Yes*, with one critical requirement: the OCP must NOT also be commanding
the same heater.
This is a common question because adding a second USB adapter feels like
extra complexity. The good news is that RS-485 is inherently a multi-drop
bus -- the OCP, pumps, chlorinators, and heaters are already all sharing
it. njsPC is already on that same bus (portId=0), listening to everything
and occasionally sending commands. Adding heater control to njsPC's
responsibilities on that bus is not fundamentally different from what it
already does.
The bus contention concern (two devices trying to transmit at the same
time) is already handled. njsPC has TX delays and retry logic in Comms.ts
that prevent collisions with the OCP's traffic. The NixieUltratemp class
retries Action 114 commands up to 3 times if a response isn't received.
The real danger isn't bus contention -- it's *dual-master conflict*. If
EasyTouch has the UltraTemp configured (as Solar, HeatPump, or any type),
the OCP will periodically send its own Action 114 commands based on its own
thermostat logic. Meanwhile njsPC sends its own Action 114 commands based
on Nixie thermostat logic. The UltraTemp doesn't know or care who is
talking to it -- it simply obeys the last command it received. The result
is the heater flickering between heat/off as two controllers fight over it.
*Resolution:* Remove the heater from the OCP's configuration. Once the
OCP doesn't know about the heater, it stops sending Action 114 entirely.
njsPC becomes the sole master. The 2nd USB stick (section 5) is the
alternative for users who can't or won't change their OCP config -- it
achieves the same isolation through physical separation rather than
configuration.
—
Reply to this email directly, view it on GitHub
<#1152 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAQF2WGUHV7DZW45YLOJM3T4MH22PAVCNFSM6AAAAACVGNCCTSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZTSMBZGYZDANRTHA>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
In my original pool setup as configured by the installer, UltraTemp was setup as a Solar heater. This caused a lot of unreliability in HeatPump controls because the EasyTouch controller may override the heatpump operations. I wanted to have a direct control over the heatpump and implemented a new heater type that talks to UltraTemp directly via RS-485 Action 114/115, bypassing the OCP entirely.
Previously doing this required hacking existing heater types with workaround flags (nixieOverride). This adds a clean new heater type (id 8, 'ultratempdirect') that:
Changes: