{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://synacl.com/protocol/v1/schemas/device-cmd.json",
  "title": "Device command (cloud -> gateway or device)",
  "description": "Commands addressed to one device: on `<prefix>/devices/{deviceId}/cmd` for a device behind a gateway, or `tenants/{tenantId}/devices/{deviceId}/cmd` for a directly connected device (QoS 1, not retained). Three families, told apart by their keys: READ CONTROL carries `command` (read/once, set/interval, read/disable, read/enable — gateway devices only); a MODBUS WRITE carries `fc` + `address`; an ACTUATOR WRITE carries neither (`value`, plus `dir` for a motor, or `command:\"move\"` for a stepper). Every command with a `correlationId` expects a reply on the matching `cmd/ack` topic (see cmd-ack) echoing it; the rest are fire-and-forget. Ignore a `command` you do not recognise. The backend does not validate what it publishes — this schema documents it; validate with a plain draft-07 validator (not Ajv `removeAdditional`, which corrupts `oneOf`). New optional keys may be added within v1; ignore keys you do not use.",
  "type": "object",
  "oneOf": [
    {
      "$ref": "#/definitions/readOnce"
    },
    {
      "$ref": "#/definitions/setInterval"
    },
    {
      "$ref": "#/definitions/readDisable"
    },
    {
      "$ref": "#/definitions/readEnable"
    },
    {
      "$ref": "#/definitions/modbusWrite"
    },
    {
      "$ref": "#/definitions/actuatorWrite"
    },
    {
      "$ref": "#/definitions/stepperMove"
    }
  ],
  "definitions": {
    "readOnce": {
      "title": "read/once",
      "description": "Read one tag now — even a tag with `isIntervalRead: false` — and publish the value on the normal data topic; then ack with `correlationId`.",
      "type": "object",
      "required": [
        "command",
        "tag",
        "correlationId"
      ],
      "properties": {
        "command": {
          "const": "read/once"
        },
        "tag": {
          "description": "The tag's name, as in config/push.",
          "type": "string"
        },
        "correlationId": {
          "description": "24 hex characters.",
          "type": "string"
        }
      }
    },
    "setInterval": {
      "title": "set/interval",
      "description": "Change how often this device is polled and published, and persist it across reboots (it overrides the config/push `conn` interval). Applied live; no reboot.",
      "type": "object",
      "required": [
        "command",
        "interval"
      ],
      "properties": {
        "command": {
          "const": "set/interval"
        },
        "interval": {
          "description": "Milliseconds, 250 to 3,600,000 (raised to the account's minimum publish interval when that is higher).",
          "type": "integer",
          "minimum": 250,
          "maximum": 3600000
        }
      }
    },
    "readDisable": {
      "title": "read/disable",
      "description": "Pause polling this device. `manual` = until a read/enable; `restart` = until the gateway next reboots; `timed` = for `durationMs`, then resume on your own.",
      "type": "object",
      "required": [
        "command",
        "mode"
      ],
      "properties": {
        "command": {
          "const": "read/disable"
        },
        "mode": {
          "type": "string",
          "enum": [
            "manual",
            "restart",
            "timed"
          ]
        },
        "durationMs": {
          "description": "Present only with mode `timed`.",
          "type": "number",
          "exclusiveMinimum": 0
        }
      },
      "if": {
        "properties": {
          "mode": {
            "const": "timed"
          }
        }
      },
      "then": {
        "required": [
          "durationMs"
        ]
      }
    },
    "readEnable": {
      "title": "read/enable",
      "description": "Resume polling after a read/disable.",
      "type": "object",
      "required": [
        "command"
      ],
      "properties": {
        "command": {
          "const": "read/enable"
        }
      }
    },
    "modbusWrite": {
      "title": "Modbus write",
      "description": "Write one coil or register. Through a gateway ONLY single writes are sent and `register_type` selects the function — `coil` = FC05, `holding` = FC06; switch on `register_type`, not `fc`. A directly connected device receives the body without `register_type` and may also receive FC15/FC16 with an array `value`. Ack with the result.",
      "type": "object",
      "required": [
        "correlationId",
        "fc",
        "address",
        "value"
      ],
      "properties": {
        "correlationId": {
          "description": "A UUID.",
          "type": "string"
        },
        "fc": {
          "description": "Modbus function code.",
          "type": "integer",
          "enum": [
            5,
            6,
            15,
            16
          ]
        },
        "address": {
          "type": "integer",
          "minimum": 0,
          "maximum": 65535
        },
        "value": {
          "description": "The value to write: a number for FC05/FC06 (coil: 0 = off, non-zero = on); an array of numbers for FC15/FC16.",
          "oneOf": [
            {
              "type": "number"
            },
            {
              "type": "array",
              "minItems": 1,
              "items": {
                "type": "number"
              }
            }
          ]
        },
        "register_type": {
          "description": "Gateway devices only; always present there.",
          "type": "string",
          "enum": [
            "coil",
            "holding"
          ]
        }
      },
      "not": {
        "required": [
          "command"
        ]
      }
    },
    "actuatorWrite": {
      "title": "Actuator write",
      "description": "Drive an actuator device to `value`, then ack with the value actually applied. The meaning of `value` follows the device's protocol: gpio-digital-out 0|1; gpio-pwm duty 0..255; gpio-servo pulse width in microseconds; gpio-motor speed magnitude 0..255 with `dir`. A timed pulse is two writes: the on value now and the off value when the time is up, each with its own correlationId.",
      "type": "object",
      "required": [
        "correlationId",
        "value"
      ],
      "properties": {
        "correlationId": {
          "description": "A UUID.",
          "type": "string"
        },
        "value": {
          "type": "number"
        },
        "dir": {
          "description": "gpio-motor only: 1 = forward, 0 = reverse.",
          "type": "integer",
          "enum": [
            0,
            1
          ]
        }
      },
      "not": {
        "anyOf": [
          {
            "required": [
              "command"
            ]
          },
          {
            "required": [
              "fc"
            ]
          }
        ]
      }
    },
    "stepperMove": {
      "title": "move",
      "description": "gpio-stepper: move `steps` in direction `dir`, then ack when the move finishes (or at once with status `error` if it is rejected).",
      "type": "object",
      "required": [
        "correlationId",
        "command",
        "steps",
        "dir"
      ],
      "properties": {
        "correlationId": {
          "description": "A UUID.",
          "type": "string"
        },
        "command": {
          "const": "move"
        },
        "steps": {
          "type": "integer",
          "minimum": 0
        },
        "dir": {
          "type": "integer",
          "enum": [
            0,
            1
          ]
        }
      }
    }
  }
}
