# Remote control

> Open and close barriers, gates, chains, bollards and brains from your backend, and know
> for sure whether each command worked. This guide turns the
> [Quickstart](/get-started/quickstart) into something you can run in production.

## Prerequisites

- An API token on a server; see [Authentication](/api/authentication).
- A subscription with **remote control** for each product; see
  [Plans and permissions](/concepts/plans-and-permissions).
- Each product paired, on a lot, with an online Gateway in Bluetooth range on the same
  lot. See [What you need](/get-started/what-you-need).
- The Socket.IO client for your language; see [Connecting](/realtime/connecting).

## How a command works

You send a command, the API answers at once with an `event_id`, and the result arrives
later as a realtime event: `lot_change` on success, `gateway_error` on failure. The API
tries each Gateway that sees the product, strongest signal first, for up to 30 seconds
each. The details are in [Asynchronous operations](/concepts/asynchronous-operations).

## 1. Check the operation fits the product

The API doesn't check that an operation makes sense for the product type; check it
yourself before sending. `product_type_id` comes from
[`GET /v2/products/{id}`](/api/endpoints/products-find-one).

```ts title="operations.ts"
const BARRIER = 2;
const BRAIN_TYPES = [3, 4, 5, 6]; // gate, brain, chain, bollard

const ALLOWED: Record<string, number[]> = {
  open: [BARRIER, ...BRAIN_TYPES],
  close: [BARRIER, ...BRAIN_TYPES],
  force_close: [BARRIER],
  stay_open: BRAIN_TYPES,
  stay_close: BRAIN_TYPES,
  clear_stay_open: BRAIN_TYPES,
  clear_stay_close: BRAIN_TYPES,
};

export function canRun(product: { status: string; product_type_id: number }, operation: string) {
  return product.status === 'online' && (ALLOWED[operation] ?? []).includes(product.product_type_id);
}
```

The matrix, and what `open` and `close` mean on a barrier (`open` lowers the arm), is in
[Products and operations](/concepts/products-and-operations#operations).

## 2. Connect, send, and wait

Connect to realtime **before** you send anything, and keep the connection open for the
life of your service. Then, for each command, send it and wait for the event that
carries the same `event_id`:

**Node.js**

```ts title="run-operation.ts"
const API = 'https://api.parklio.com/v2';
const headers = {
  Authorization: `Bearer ${process.env.PARKLIO_API_TOKEN}`,
  'Content-Type': 'application/json',
};

// Connect once, before sending anything.
const socket = io(`${API}/realtime`, {
  path: '/socketio',
  query: { token: process.env.PARKLIO_API_TOKEN },
});

export async function runOperation(productId: number, operation: string) {
  // Listen before sending: the result can arrive before the HTTP response does.
  const results = new Map<string, { ok: boolean; event: any }>();
  let arrived = () => {};
  const record = (ok: boolean) => (raw: string) => {
    const event = JSON.parse(raw);
    if (!event.event_id) return; // a status update, not a command result
    results.set(event.event_id, { ok, event });
    arrived();
  };
  const onSuccess = record(true);
  const onFailure = record(false);
  socket.on('lot_change', onSuccess);
  socket.on('gateway_error', onFailure);

  let timedOut = false;
  const timer = setTimeout(() => {
    timedOut = true;
    arrived();
  }, 120_000);

  try {
    const res = await fetch(`${API}/products/${productId}/state`, {
      method: 'POST',
      headers,
      body: JSON.stringify({ operation }),
    });
    const body = await res.json();
    if (!body.success) {
      // Rejected before anything was sent to the device.
      return { ok: false, rejected: true, code: body.errors[0].code, message: body.errors[0].message };
    }

    const eventId: string = body.data.event_id;
    while (!results.has(eventId) && !timedOut) {
      await new Promise<void>((resolve) => (arrived = resolve));
    }
    const result = results.get(eventId);
    if (!result) return { ok: false, unknown: true };

    const { event } = result;
    if (result.ok) {
      const { data } = event;
      const block = data.barrier ?? data.gate ?? data.chain ?? data.bollard ?? data.brain;
      return { ok: true, state: block?.state };
    }
    return { ok: false, type: event.type, message: event.message };
  } finally {
    clearTimeout(timer);
    socket.off('lot_change', onSuccess);
    socket.off('gateway_error', onFailure);
  }
}
```

**Python**

```python title="run_operation.py"
import json
import os
import threading

import httpx
import socketio

API = "https://api.parklio.com/v2"
TOKEN = os.environ["PARKLIO_API_TOKEN"]
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
NS = "/v2/realtime"

sio = socketio.Client()
results = {}  # event_id -> (ok, event)
arrived = threading.Condition()


def record(ok):
    def handler(raw):
        event = json.loads(raw)
        if not event.get("event_id"):
            return  # a status update, not a command result
        with arrived:
            results[event["event_id"]] = (ok, event)
            arrived.notify_all()
    return handler


sio.on("lot_change", record(True), namespace=NS)
sio.on("gateway_error", record(False), namespace=NS)

# Connect once, before sending anything.
sio.connect(f"https://api.parklio.com?token={TOKEN}", namespaces=[NS], socketio_path="socketio")


def run_operation(product_id, operation):
    body = httpx.post(
        f"{API}/products/{product_id}/state", headers=HEADERS, json={"operation": operation}
    ).json()
    if not body["success"]:
        # Rejected before anything was sent to the device.
        error = body["errors"][0]
        return {"ok": False, "rejected": True, "code": error["code"], "message": error["message"]}

    event_id = body["data"]["event_id"]
    with arrived:
        if not arrived.wait_for(lambda: event_id in results, timeout=120):
            return {"ok": False, "unknown": True}
        ok, event = results.pop(event_id)

    if ok:
        data = event["data"]
        block = next(
            (data[k] for k in ("barrier", "gate", "chain", "bollard", "brain") if data.get(k)),
            None,
        )
        return {"ok": True, "state": block and block["state"]}
    return {"ok": False, "type": event["type"], "message": event.get("message")}
```

The function has four outcomes: **rejected** (an HTTP error, nothing was sent), **ok**,
**failed** (a `gateway_error`), and **unknown** (you gave up waiting). Handle each one.

## 3. Handle failures

### Rejected: an HTTP error

Nothing reached the device. Act on the error `code`:

| Code | Meaning | What to do |
| --- | --- | --- |
| `214` | Another operation on this product is still running. | Wait for its result, then send yours. |
| `704` | No Gateway sees the product right now. | Don't retry at once; check the product's `status` and the Gateway. |
| `30` | The product's subscription doesn't include remote control. | Fix the subscription; retrying won't help. |
| `206` | The product isn't paired. | Pair it with the Connect app. |
| `6` / `4` | The product doesn't exist, or your token can't see it. | Check the id and the product's lot. |
| `23` | Invalid body. | Fix the request. |

`214` can also come from operations you didn't send: someone using the Connect app, or
Parklio setting the device's clock. It clears as soon as that operation's result is
sent, which can take up to 30 seconds per Gateway.

### Failed: a `gateway_error`

The command reached at least one Gateway and didn't succeed. `type` tells you why; the
full table is in [Events](/realtime/events#gateway_error).

| `type` | What to do |
| --- | --- |
| `6` Car on top | The barrier won't rise with a car over it. Tell the user; don't retry until the car has left. |
| `5` Device jammed, `7` Sensor dirty | Needs someone on site. Alert, don't retry. |
| `0` Auth error, `1` Invalid guest key, `8` Device not initialized | The device rejected the command. Retrying won't help; contact [support](/resources/support) if it persists. |
| `2`, `3`, `4`, `10`, `11`, `"Timeout"` | The API already tried every Gateway that sees the product. One later retry is reasonable; alert if it fails again. |
| `9` Rtcc not set | The device's clock isn't set. Parklio sets it automatically when a Gateway sees the device (at most one attempt an hour per device); retry later. |

### Unknown: you gave up waiting

The command may still have run. Re-read the product with
[`GET /v2/products/{id}`](/api/endpoints/products-find-one) and compare its state with
what you asked for before doing anything else. Don't resend blindly: on a barrier, a
second `close` after a successful one is harmless, but your users may see the arm move
twice.

## 4. Send one command per product at a time

Queue commands per product in your service, and send the next only when the previous
one has a result (or you gave up and re-read the state). This avoids `214` and keeps
results easy to match. Commands for **different** products can run in parallel.

## Change barrier settings

[`POST /v2/products/{id}/settings`](/api/endpoints/products-change-settings) works the
same way: send **one** setting per call, get an `event_id`, wait for the result.

```bash title="Set the auto-close delay to 30 seconds"
curl -s -X POST https://api.parklio.com/v2/products/123/settings \
  -H "Authorization: Bearer $PARKLIO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"autoclose_time": 30}'
```

`autoclose_time` is 5–60 seconds. To turn auto-close on or off, send
`{"is_autoclose": true}` or `false` (a JSON boolean). In the `lot_change` result the
barrier block calls these `auto_close_time` and `is_auto_close`; see
[Events](/realtime/events#lot_change).

## Brains without input sensors

A brain wired to a gate or door with no open/closed sensor can't report whether it
moved. Send `"wait_for_completion": false` with the operation, so the Gateway doesn't
wait for a state change that will never come:

```json title="Request body"
{ "operation": "open", "wait_for_completion": false }
```

## Verify it worked

1. `runOperation` returns `ok: true`, and the state matches the operation: `opened`
   after `open`, `closed` after `close`.
2. [`GET /v2/products/{id}`](/api/endpoints/products-find-one) shows the same state in
   its type block.
3. On site, the product moved. On a barrier, `open` **lowers** the arm.

## Common errors

| Symptom | Cause | Fix |
| --- | --- | --- |
| Always `704` | No Gateway sees the product. | Check the Gateway is online, on the same lot and in range; see [Troubleshooting](/resources/troubleshooting). |
| Frequent `214` | Commands for one product overlap. | Queue per product (step 4). |
| No result, then `"Timeout"` | The Gateway couldn't reach the product over Bluetooth. | Check range and power; a result can take 30 seconds per Gateway. |
| Results never arrive | You connected after sending, or the connection dropped. | Connect first; resync after reconnects ([Patterns](/realtime/patterns#resync-after-a-reconnect)). |
| An unexpected error (such as HTTP 500, code `0`) for `stay_open` on a barrier | Unsupported operation for the product type. | Check the matrix (step 1). |
| `403` code `30` | No remote control in the subscription. | See [Plans and permissions](/concepts/plans-and-permissions). |

## Next

- [Monitor devices](/guides/monitor-devices): know when a product needs attention.
- [Events](/realtime/events)
