LAN access
Controllers on firmware 2.5.46 and later serve Command Protocol v2 on the local network. Reads need no credential beyond being on the same network; writes need the controller’s envelope key. Place controllers on a trusted network segment.
Discovery
Section titled “Discovery”The controller announces itself with mDNS:
| Item | Value |
|---|---|
| Service type | _splashme._tcp |
| Port | 8080 |
| Hostname | splashme-<last three MAC octets, lower-case hex>.local, for example splashme-000001.local for MAC 02:53:4D:00:00:01 |
TXT id |
Controller identifier, for example 02_53_4D_00_00_01 |
TXT mac |
12 lower-case hex characters |
TXT fw |
Firmware version |
TXT pv |
2 |
Key your records on the mac TXT record: the IP address can change. The hostname can also be resolved with a unicast .local lookup where multicast is blocked. A controller on both Wi-Fi and Ethernet announces one identity.
Endpoint
Section titled “Endpoint”POST http://<ip>:8080/sm/2/lan/pv2Content-Type: application/jsonContent-Length: <1 to 1024>
{ "op": "state" | "a" | "q", "name": "<action or query name>", "env": "<hex of the signed envelope>" }Content-Length is required. name and env may be empty for op: "state". Whitespace in the hex is ignored.
The controller handles one request at a time and closes the connection after each response. It does not accept connections during a firmware update, before it has an IP address, or for a moment while its cloud TLS handshake is in flight; treat a connect timeout as transient and retry. A 10-second client timeout and a poll interval of 15 seconds work well.
Operations
Section titled “Operations”All application-level results come back as HTTP 200 with a JSON body.
op |
Signature | Success | Failure |
|---|---|---|---|
state |
Not required | { "env": "<hex signed full state frame>" } |
{ "ok": false } |
a |
Required, strict counter | { "ok": true, "env": "<hex signed full state frame>" }, taken after the action ran; env is omitted if the frame could not be built |
{ "ok": false } |
q |
Required, strict counter | { "env": "<hex signed query reply>" } |
{ "ok": false } |
{"ok": false} covers a bad tag, a replayed counter, an unknown or refused name, and an action the controller declined. Other bodies: {"ok": false, "err": "bad json"}, {"ok": false, "err": "bad op"}, and {"ok": false, "err": "busy"} when the controller could not free its frame buffer within 2 seconds.
Every env in a response is signed with the controller’s key and its own counter; verify it. The env you send for a and q is byte for byte what you would publish over MQTT, so encoders are shared between transports.
Protocol-level errors
Section titled “Protocol-level errors”| Status | Body | Cause |
|---|---|---|
| 403 | {"ok":false,"err":"not served on LAN"} |
A path other than /sm/2/lan/pv2, such as a v1 command name |
| 405 | {"ok":false,"err":"POST only"} |
|
| 400 | {"ok":false,"err":"invalid Content-Length"} |
Missing, zero or above 1024 |
| 400 | {"ok":false,"err":"short body"} |
Body shorter than Content-Length |
| 408 | {"ok":false,"err":"slow headers"} |
Headers took more than 3 seconds |
What the LAN refuses
Section titled “What the LAN refuses”Even with the key, the following are refused on the LAN and return {"ok": false}.
- Commands reserved for SplashMe installation and service tooling, whether sent through the
cmdpass-through, the matching pass-through queries or reserved actions. If an integration needs one of them, contact support@splashmepool.com.au.
Everything else, including aux, schedule, set_time, evt_rewind, the native queries and equipment, chemistry, heating, lighting and schedule pass-throughs, behaves as it does through the cloud. Events raised by LAN requests carry transport code 8, v2-lan.
A complete exchange
Section titled “A complete exchange”Read the state without a key:
curl -s http://splashme-000001.local:8080/sm/2/lan/pv2 \ -H 'Content-Type: application/json' \ -d '{"op":"state","name":"","env":""}'Verify the returned env with unwrap from Envelope and keys. The payload is a state frame; decoding it requires the wire specification.
Switch slot 5 on with the key:
import time, requestsenv = wrap(key, int(time.time()), bytes([5, 1, 1])) # aux: slot 5, on, trumpr = requests.post("http://splashme-000001.local:8080/sm/2/lan/pv2", json={"op": "a", "name": "aux", "env": env.hex()}, timeout=10)body = r.json()assert body.get("ok"), bodycounter, frame = unwrap(key, bytes.fromhex(body["env"])) # fresh full stateObtaining the key
Section titled “Obtaining the key”Call GET …/envelope-key once with an OAuth token for an account that owns the controller, then store the key with your controller record. See Bring your own authentication for the recommended shape of a LAN-first integration.

