Skip to main content

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 (and AIRTMP) registers. A temperature the heater can't currently measure is reported as unavailable instead of as a number.
  • RSSA-over-TCP server — when outputs.tcp is 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 1 instead of AUX7.

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:

RegisterSourceMeaning
POOLTMPJXi poll / displayCurrent pool water temperature (°F) — see When a temperature has no reading
POOLSP / SPASPJXi pollPool / spa heat setpoint (°F)
AIRTMPDisplay panelAir temperature (°F)
PUMP, SPA, AUX1…AUX11Status LEDsEquipment on/off
POOLHTStatus LEDsPool heat enabled
PUMPn_WePumpVariable-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:

SurfaceBehavior 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
RESTThe register stays in POST /action/registers with "value": null and "unavailable": true
MQTTThe heater's jxi/<addr>/status payload carries "waterTemp": null
BACnet / SparkplugNo 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).

Check your historian after upgrading

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 1 instead of AUX7 (the register stays in the tooltip);
  • on POST /action/registers, as an extra circuit field 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/learned each 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.

Labels, not identifiers

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:

  • TogglesPUMP, 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 REST POST /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.
  • SetpointsPOOLSP / 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 REST POST /action/setpoint {"register":"POOLSP","value":90} (or "delta":1 to 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).
Aux registers renumbered — re-check anything bound to AUX4–AUX7

The keypad's second column of aux buttons is Aux 7–10, not Aux 4–7. OmniBus previously labelled that column AUX4AUX7, 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 (AUX7AUX10). 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.

Setpoints are read-only on the heater

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:

SurfaceBehavior while a press is in flight
UIThe button pulses ("waiting for the panel") and stays disabled until the circuit actuates.
RESTpressButton returns 409 (press is already in flight).
RSSA?07 CTRL OPERATION FAILED — the dongle's generic refusal; re-poll and retry.
BACnet / SparkplugThe 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:

SurfaceBehavior while out of AUTO
RSSA?30 SERVICE MODE IS ACTIVE / ?31 TIMEOUT MODE IS ACTIVE — the same codes the real dongle returns
REST409 with that text, on pressButton and setpoint
BACnet / SparkplugCommand refused and logged (the object's present-value settles back on the next status decode)
UIBanner on the port card; toggles and setpoint controls greyed out
MQTTpanel/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

Control paths default to dry-run

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

OutputBehavior
TCPRSSA text protocol (drop-in for the Jandy serial dongle): #REG? reads, #REG=VALUE / #REG± writes.
MQTTPublishes 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.
WebhooksFan 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).