Skip to main content

Jandy AquaLink RS

Driver: jandy

Control Jandy AquaLink RS pool/spa controllers over their RS485 bus via a serial-to-Ethernet bridge (e.g. USR-TCP232). The driver speaks the raw #-command protocol, supports circuit on/off and heater setpoints, and monitors per-zone state, temperature, and setpoint. On connect it subscribes to asynchronous change pushes (#COSMSGS=1), so most updates arrive unsolicited rather than from polling.

Prerequisites

  • A Jandy AquaLink RS system wired to a serial-to-Ethernet bridge on the RS485 bus.
  • The bridge reachable on the LAN from the GEM server (typically TCP port 23).

Setup

  1. Connect the serial-to-Ethernet bridge to the Jandy AquaLink RS485 bus.
  2. In GEM, System → Devices → Add Device with driver jandy. Enter the bridge ip and port.
  3. Let the controller run for a minute so registers populate the device discovered_addresses list, then run the sync_zones command to auto-create one zone per register.
  4. (Optional) For a composite zone whose state, temperature, and setpoint live at different registers, create it by hand and set jandy_state_address / jandy_temperature_address / jandy_setpoint_address instead of relying on the zone address.

Addressing

Every Jandy zone is bound to one or more AquaLink registers (e.g. S01, POOLTMP, POOLHTR). A register name is a letter followed by up to 15 more letters or digits, with no spaces. There are two ways to bind a zone:

  1. Single register (auto-create path). Put one register in the zone address field. The driver decides what that register means — on/off state, temperature, or setpoint — by classifying it (see below) and tracks that one role. This is exactly what sync_zones creates: one register per zone.
  2. Composite zone (hand-built). When one piece of equipment exposes its state, temperature, and setpoint at different registers (e.g. a heater whose on/off is one register and target temperature another), make a single zone and fill in the per-role attributes. An explicit attribute always wins for its role; leave the ones you don't need empty.
RoleAttributeFalls back to the zone address when the attribute is empty?
on/offjandy_state_addressYes — always, as long as the address looks like a register.
setpointjandy_setpoint_addressOnly when the address itself classifies as a setpoint register.
temperaturejandy_temperature_addressOnly when the address itself classifies as a temperature register.

So the fallback is asymmetric: on/off always falls through to the address, but setpoint and temperature fall through only when the address is itself that kind of register. A bare-address zone therefore tracks exactly one role (whatever its register classifies as) — use the composite attributes when one zone must follow more than one register.

How a register is classified

Whenever the driver has only the address field to go on, it classifies the register to decide which role it drives — and, at sync_zones time, which control type the new zone gets. A live value wins when there is one; otherwise the register name decides:

  • value on / off (or 1/2 → on, 0 → off) → on/off state
  • value like 78 F or 24 Ctemperature
  • name ends in TMP or contains TEMPtemperature
  • name contains HTR, SETPT, STPT, or ends in SPsetpoint
  • anything else → on/off state

So POOLTMP is read as a temperature, POOLHTR as a heater setpoint, and S01 (a spa circuit) as on/off — with no per-role attributes at all. sync_zones classifies by name only (it runs before any value is on the wire), so name your registers accordingly where you can.

sync_zones

sync_zones reads device.discovered_addresses and creates one zone per register, then:

  • classifies each register by name (no value is known yet) and gives the new zone a matching control type: on/off → water_onoff, setpoint → water_onoff_setpoint, temperature → water_temperature;
  • places every zone under the water subsystem. water is one of GEM's default subsystems, so it normally already exists. sync_zones does not split pool vs. spa and does not create the subsystem — if water is missing, every register is skipped with a could not resolve subsystem warning;
  • names each new zone after its register, so the zone label is the raw register (e.g. POOLHTR). Rename it to something friendly like "Pool Heater" afterward — the address stays the same, so the binding is unaffected;
  • is idempotent — registers already represented by an existing zone (by address or any jandy_*_address attribute) are skipped, never modified.

Run it any time after discovered_addresses has populated. If it returns "no discovered addresses yet", let the controller run longer so registers appear on the wire, then re-run.

Attributes

Device

AttributeRequiredDefaultDescription
ipyesSerial bridge IP.
portyesSerial bridge TCP port.
polling_rateno3000Delay (ms) between individual register reads in the heartbeat sweep. RS485 is single-master — don't set below the panel's own poll cadence.
heartbeat_intervalno60000How often (ms) to re-sweep configured zone registers as a sanity check against the COSMSGS push subscription.
command_throttleno1000Minimum spacing (ms) between commands.
discovered_addresses— (read-only)Comma-separated registers seen on the wire. Auto-populated; the menu for hand-wiring zones and the source list sync_zones builds from.

Zone

AttributeDescription
jandy_state_addressAquaLink register for on/off state (composite override).
jandy_temperature_addressAquaLink register for temperature (composite override).
jandy_setpoint_addressAquaLink register for the heater/temperature setpoint (composite override).

Commands

CommandArgsEffect
getaddressQuery the value at an AquaLink register.
onaddressTurn on the circuit at the register (or the zone's state register).
offaddressTurn off the circuit at the register (or the zone's state register).
setpointaddress, setpointSet the heater/temperature setpoint (or the zone's setpoint register).
setaddress, valueSet an arbitrary register to a value.
passthroughcommandSend a raw AquaLink command string.
resetReset the AquaLink controller.
sync_zonesAuto-create one zone per discovered register (idempotent — see above).

Known behavior

  • On every (re)connect the driver sends #COSMSGS=1 so the bridge pushes register changes asynchronously; most state updates come in unsolicited. A slow heartbeat sweep (every heartbeat_interval, default 60 s) re-reads each configured register as a fallback in case the push subscription silently dies.
  • On/off circuits report in several forms — on/off, 1/0, yes/no, true/false — and a circuit running on a schedule may report 2; the driver treats 1 and 2 alike as on. Temperatures arrive as 75 F or 23 C and are normalized to a whole number, converting Celsius to Fahrenheit, before storage.
  • on/off/setpoint commands schedule a delayed re-poll (about 1 s later) of the matching register as a belt-and-suspenders confirmation, on top of whatever #COSMSGS=1 pushes.

Troubleshooting

SymptomCheck
jandy error response in logs / no zone updatesVerify the register address — invalid AquaLink registers are echoed back with a leading ?.
Status updates lag behind realityLive updates ride the #COSMSGS=1 push subscription; if it stops delivering, the only backstop is the heartbeat sweep (heartbeat_interval, default 60 s). Lower heartbeat_interval for a faster backstop, and/or lower polling_rate to tighten the per-register cadence within a sweep — but watch for RS485 bus contention with the AquaLink panel itself.
Setpoint command silently does nothingConfirm a setpoint register resolves for the zone (its address classifies as a setpoint, or jandy_setpoint_address is set) and that the register supports writes — some are read-only.
no discovered addresses yet from sync_zonesLet the controller run so registers appear in discovered_addresses, then run sync_zones again.
sync_zones skips every register / creates no zonesConfirm a water subsystem exists — sync_zones places all Jandy zones under it and skips registers (logging could not resolve subsystem) when it's missing.