Jandy AquaLink RS
The jandy adapter decodes the Jandy AquaLink RS pool/spa bus (9600 8N1).
It discovers equipment, reads setpoints and temperatures, surfaces everything
over MQTT/REST, serves Jandy's RSSA text protocol over TCP, and can drive
the pool — toggling equipment and changing heat setpoints — by emulating an
AllButton keypad.
What it does
- Decodes the bus — display panels, pumps (incl. variable-speed watts), SpaLink, the JXi heater, and aux relays, surfaced via REST and MQTT.
- Reads setpoints + temps passively — pool/spa heat setpoint, water
temperature, and heater-firing state are decoded straight from the JXi
heater's poll (no menu navigation needed to read a value). Surfaced as the
POOLSP,SPASP,POOLTMP(andAIRTMP) registers. A temperature the heater can't currently measure is reported as unavailable instead of as a number. - RSSA-over-TCP server — when
outputs.tcpis enabled, the port's TCP server speaks the Jandy RSSA text protocol. A drop-in replacement for Jandy's serial-adapter dongle, so existing RSSA clients connect straight to OmniBus. - AllButton keypad emulator — claims a keypad slot (
0x08–0x0b) and synthesizes button presses to drive the AquaLink master: equipment toggles and heat-setpoint changes. - Learns your circuit names — the panel announces each circuit by name as
it switches it ("Air Blower 1 ON"), so OmniBus picks up the site's own labels
and shows
Air Blower 1instead ofAUX7.
Reading state
Once the bus is decoding, the port card shows a live register table. Binary circuits (pump, aux relays, heaters) render an ON/off pill, and the AllButton test buttons light up for whatever is currently on, so you can see the pool's state at a glance. Key registers:
| Register | Source | Meaning |
|---|---|---|
POOLTMP | JXi poll / display | Current pool water temperature (°F) — see When a temperature has no reading |
POOLSP / SPASP | JXi poll | Pool / spa heat setpoint (°F) |
AIRTMP | Display panel | Air temperature (°F) |
PUMP, SPA, AUX1…AUX11 | Status LEDs | Equipment on/off |
POOLHT | Status LEDs | Pool heat enabled |
PUMPn_W | ePump | Variable-speed pump power (W) |
Every aux register is readable; only some are writable — see Driving the pool below.
When a temperature has no reading
The JXi only measures water temperature while there is flow across its sensor.
With the filter pump off — or with an open/disconnected probe — it reports a
placeholder byte instead of a number. OmniBus treats that as no reading
rather than passing it through, so POOLTMP never appears as a 255 °F pool:
| Surface | Behavior while there is no reading |
|---|---|
| RSSA | ?18 POOLTMP IS UNAVAILABLE — both as the answer to #POOLTMP? and as a push to COSMSGS=1 subscribers the moment the value drops out |
| REST | The register stays in POST /action/registers with "value": null and "unavailable": true |
| MQTT | The heater's jxi/<addr>/status payload carries "waterTemp": null |
| BACnet / Sparkplug | No new value is published; the temperature point holds its last reading |
The same plausibility gate applies to the pool and spa setpoint bytes: a
placeholder or an obviously wrong value leaves POOLSP / SPASP at their last
decoded reading instead of overwriting it. Any temperature outside a generous
20–130 °F envelope is treated as a placeholder or a mis-framed byte, not a
measurement.
POOLTMP has two independent sources — the JXi's poll and the panel's rotating
display — and they don't fail together. A "no reading" from one source will not
erase a fresh reading from the other; it only retracts once the surviving value
is more than five minutes old. POST /action/registers reports which source a
value came from in a source field (jxi or display).
Earlier builds served the placeholder verbatim, so a head-end trending
POOLTMP may have recorded stretches of 255 °F (or 0 °F) whenever the
pump was off. Those samples are artifacts, not readings — clean them out of any
trend or alarm limit derived from that history.
Circuit names
AUX7 tells you nothing about what is on the other end of it. The panel does:
whenever it switches a circuit it puts that circuit's name on the keypad
display ("Air Blower 1 ON"), and OmniBus pairs that announcement with the
status LED that moved at the same moment. After a circuit has been switched
once — from the panel, a wall keypad, a schedule, or OmniBus — the site's own
label is attached to the register.
Learned names show up:
- on the port card's test buttons, which read
Air Blower 1instead ofAUX7(the register stays in the tooltip); - on
POST /action/registers, as an extracircuitfield alongside each register; - on
POST /action/circuits, which lists every learned name with the LED index it was seen on and whether that circuit can be commanded; - over MQTT on
<prefix>/circuit/learnedeach time a name is learned or changes.
Names are saved with the port config, so a restart starts out knowing them. To
label a circuit that has never been switched, or to override what the panel
calls one, edit circuitNames in the port's jandy config block
("AUX7": "Air Blower 1"); OmniBus will still update it if the panel later
announces a different name.
A circuit name is display text. It can be blank, duplicated across circuits, or
renamed at the panel at any time — so bind your head-end to the register
(AUX7) and display the name. Register names never move.
OmniBus only binds a name when exactly one circuit changed state within a
couple of seconds of the announcement, and the direction matches (ON with a
circuit turning on). The panel's idle display also cycles status text like
"Chiller OFF" that looks identical; text that repeats without any circuit
actually moving is ignored rather than attached to the next circuit that
switches.
Driving the pool
Two kinds of write, both through the AllButton emulator:
- Toggles —
PUMP,SPA,AUX1,AUX2,AUX3,AUX7,AUX8,AUX9,AUX10,POOLHT,SPAHT. A single keypad press toggles the circuit. From the UI use the test buttons; over RSSA send#AUX3=1; over RESTPOST /action/pressButton {"register":"AUX3"}. From a BMS they're also commandable Binary Value objects over the BACnet/IP object server (or a Sparkplug DCMD): writing the present-value drives the keypad. Because the bus press is a toggle, OmniBus only presses when the commanded on/off differs from the decoded current state, so a write behaves as an absolute set; the real result flows back on the next status decode. Repeat presses of the same circuit are debounced while the first is still taking effect — see Repeat presses and slow circuits below. - Setpoints —
POOLSP/SPASP. OmniBus walks the panel's Set Temp → Set Pool/Spa Temp menu to the target, then confirms the new value against the JXi read-back. From the UI use the Setpoints row (− / + or type a value and Set); over RSSA send#POOLSP=90; over RESTPOST /action/setpoint {"register":"POOLSP","value":90}(or"delta":1to nudge). The pool/spa setpoints are also writable from a BMS: over the BACnet/IP object server they surface as commandable Analog Value objects (°F), and over Sparkplug they accept a DCMD write — writing the present-value drives the same menu walk, and the committed value flows back to the object on the next JXi poll. Setpoints are clamped to 40–104 °F; a menu walk takes a moment (~one step per degree).
The keypad's second column of aux buttons is Aux 7–10, not Aux 4–7.
OmniBus previously labelled that column AUX4–AUX7, so a write to any of
those four registers pressed the button three circuits along: commanding
AUX4 switched Aux 7, AUX7 switched Aux 10, and so on. Reads were never
affected — only writes.
Those four registers are now named for the circuits they actually drive
(AUX7–AUX10). Anything that writes them — an RSSA client, a BACnet Binary
Value binding, a Sparkplug DCMD, a stored scene — must be re-pointed, and the
old names are the wrong target by three either way. Confirm each write against
the equipment before trusting it.
AUX4, AUX5 and AUX6 are readable but not commandable: the keypad
codes for the first column past Aux 3 have not been confirmed on a live panel,
so rather than guess, OmniBus refuses the press. pressButton returns 400
(No keypad mapping for register), and those circuits are not published as
commandable BACnet or Sparkplug objects.
The JXi echoes the setpoint the controller pushed to it — it does not own it. Changing a setpoint therefore means driving the panel's menu (the keypad walk above); there is no faster direct-to-heater write path.
Repeat presses and slow circuits
A keypad press is a toggle, and some circuits are slow to show it. A color light is the worst case: the panel power-cycles the fixture through its color sequence and only lights the status LED 17–19 s after the press. Until the LED moves, the natural reactions — a user clicking again because the button "didn't work", or a BMS write-then-verify loop re-writing the value — would each send a second toggle and switch the circuit straight back off.
So OmniBus holds each press in flight until the panel's status LED reflects it (or ~30 s passes), and refuses a repeat press of the same register inside that window:
| Surface | Behavior while a press is in flight |
|---|---|
| UI | The button pulses ("waiting for the panel") and stays disabled until the circuit actuates. |
| REST | pressButton returns 409 (press is already in flight). |
| RSSA | ?07 CTRL OPERATION FAILED — the dongle's generic refusal; re-poll and retry. |
| BACnet / Sparkplug | The repeat write is dropped and logged. The first press is already on its way; the object's present-value follows on the next status decode. |
Fast circuits barely notice: the hold lifts the moment the LED changes,
typically within a second or two of the press. Presses of other registers are
never blocked, and setpoint writes are unaffected. The ~30 s ceiling is only
the backstop for a press the panel never acts on; if a site's fixtures take
even longer to latch, raise it with pressInflightTtlMs (milliseconds) in the
port's jandy.allbutton config block.
Service and timeout mode
The power center's AUTO / OFF / SERVICE switch is physical. There is no keypad button behind it and no bus command that moves it, so no integration — OmniBus, the Jandy dongle, or anything else — can put the panel into service mode or take it out. Do that at the panel.
What OmniBus does do is notice. The panel broadcasts a SERVICE MODE /
TIMEOUT MODE banner to every keypad, and while that banner is live the panel
ignores keypad presses entirely. So OmniBus refuses control writes instead of
reporting a success that never reached the equipment:
| Surface | Behavior while out of AUTO |
|---|---|
| RSSA | ?30 SERVICE MODE IS ACTIVE / ?31 TIMEOUT MODE IS ACTIVE — the same codes the real dongle returns |
| REST | 409 with that text, on pressButton and setpoint |
| BACnet / Sparkplug | Command refused and logged (the object's present-value settles back on the next status decode) |
| UI | Banner on the port card; toggles and setpoint controls greyed out |
| MQTT | panel/mode published on each transition; POST /action/mode reports it on demand |
Reads are unaffected — temperatures, setpoints, and equipment state keep flowing normally.
The mode is latched from the banner and lapses back to auto after 60 s without one, so a panel returned to AUTO restores control within a minute without needing a restart.
Control safety
The AllButton keypad emulator defaults to dry-run: it logs the intended
transmit without writing to the bus, so toggles no-op and setpoint writes
return ?08 SETPT OPERATION FAILED. Observe the dry-run log and confirm the
master is probing your slot, then switch the emulator to live on the port
card to actually drive the bus. Validate against a real panel before shipping
a pool deployment.
Outputs
| Output | Behavior |
|---|---|
| TCP | RSSA text protocol (drop-in for the Jandy serial dongle): #REG? reads, #REG=VALUE / #REG± writes. |
| MQTT | Publishes discovered equipment, setpoints, temps, and heater state, plus circuit/learned when the panel names a circuit. |
| REST | /action/equipment, /action/registers, /action/circuits, /action/stats, /action/mode, /action/pressButton, /action/setpoint. |
| Webhooks | Fan out equipment/status events. |
With BACnet output enabled, decoded points are also surfaced through the BACnet/IP object server: temperatures, pump power, and heater-firing as read-only Inputs; the pool/spa setpoints as commandable Analog Value objects; and equipment toggles (pump, aux relays, heaters) as commandable Binary Value objects (see Driving the pool above).