{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://openagriculturetechnology.com/standard/oat-ods-0.3.schema.json",
  "title": "OAT Observation Data Schema (oat-ods) v0.3",
  "description": "One measurement per message. Logical stream is the primary key; physical source is swappable provenance. Consumers MUST ignore unknown fields (forward-compatibility).",
  "type": "object",
  "additionalProperties": true,
  "required": ["schema", "observed_at", "source"],
  "properties": {
    "schema": {
      "type": "string",
      "pattern": "^oat-ods/[0-9]+\\.[0-9]+$",
      "description": "Format + version tag, e.g. oat-ods/0.3"
    },
    "$schema": {
      "type": "string",
      "format": "uri",
      "description": "Optional pointer to this JSON Schema (self-description)."
    },
    "msg_type": {
      "type": "string",
      "enum": ["observation", "status"],
      "default": "observation"
    },
    "state": {
      "type": "string",
      "enum": ["online", "offline"],
      "description": "Availability on a status message (Sparkplug-flavored liveness)."
    },
    "observed_at": {
      "type": "string",
      "format": "date-time",
      "description": "ISO-8601 UTC instant the value was measured or the aggregate closed."
    },
    "stream": {
      "type": "object",
      "additionalProperties": true,
      "required": ["id"],
      "description": "Logical identity — the primary key. Survives hardware swaps.",
      "properties": {
        "id": { "type": "string", "minLength": 1, "description": "Stable logical slot id (location/role). Defaults to the MAC if unassigned." },
        "name": { "type": "string", "description": "Human-friendly label." },
        "location": { "type": "string", "description": "Free-text place / HA area." }
      }
    },
    "measurement": {
      "type": "string",
      "minLength": 1,
      "description": "Quantity kind from the oat-measurands.yaml dictionary (§5). Open string (consumers MUST ignore unknown); a key the dictionary hasn't mapped is forwarded raw.",
      "examples": ["temperature", "humidity", "dewpoint", "vpd", "battery", "rssi", "soil_moisture", "soil_conductivity", "co2", "pressure", "illuminance", "pm25", "pm10", "tvoc", "voltage", "contact", "motion"]
    },
    "value": {
      "type": ["number", "string", "boolean"],
      "description": "The reading."
    },
    "unit": {
      "type": "string",
      "description": "SenML unit symbol. Canonical units only on the wire (Cel, %RH, %, dBm, ppm, Pa, lx)."
    },
    "agg": {
      "type": "object",
      "additionalProperties": true,
      "description": "Optional aggregation metadata. Absent => instantaneous reading.",
      "properties": {
        "window_s": { "type": "integer", "minimum": 0 },
        "samples": { "type": "integer", "minimum": 0 },
        "method": { "type": "string", "enum": ["mean", "last", "min", "max", "sum"] }
      }
    },
    "source": {
      "type": "object",
      "additionalProperties": true,
      "description": "Physical provenance — swappable. At least one of gateway_id / physical_id is required.",
      "anyOf": [
        { "required": ["gateway_id"] },
        { "required": ["physical_id"] }
      ],
      "properties": {
        "tier": { "type": "string", "description": "Emitting tier, e.g. oat-ble-listener, home_assistant." },
        "gateway_id": { "type": "string", "description": "Assignable node identity (NOT the raw MAC)." },
        "device_id": { "type": "string", "description": "Status message only: the node hardware id (efuse-MAC, e.g. oat-3c8a1fa77d14). Informative provenance — the node-level twin of physical_id; identity still keys on gateway_id." },
        "physical_id": { "type": "string", "description": "The hardware now filling the slot (MAC/serial)." },
        "brand": { "type": "string" },
        "model": { "type": "string" },
        "battery_pct": { "type": "number" },
        "rssi": { "type": "number" }
      }
    },
    "health": {
      "type": "object",
      "additionalProperties": true,
      "description": "Node self-report, carried on a status/heartbeat message (see contract 7.2).",
      "properties": {
        "uptime_s": { "type": "integer", "minimum": 0 },
        "boot_count": { "type": "integer", "minimum": 0 },
        "reset": { "type": "string", "description": "Last reset reason (poweron|usb|panic|task_wdt|brownout|...); usb = post-flash reset on native-USB chips." },
        "free_heap": { "type": "integer", "minimum": 0 },
        "min_free_heap": { "type": "integer", "minimum": 0 },
        "largest_block": { "type": "integer", "minimum": 0 },
        "rssi": { "type": "number" },
        "wifi_reconnects": { "type": "integer", "minimum": 0 },
        "push_ok": { "type": "integer", "minimum": 0 },
        "push_fail": { "type": "integer", "minimum": 0 },
        "transport": { "type": "string", "enum": ["https", "http"] },
        "tls_ok": { "type": "boolean" },
        "chip": { "type": "string" },
        "temp_c": { "type": "number", "description": "Internal die temperature C (uncalibrated on classic ESP32)." },
        "loops_per_sec": { "type": "integer", "minimum": 0, "description": "Main-loop rate; liveness/overwork signal." },
        "lan_ip": { "type": "string", "description": "Station IP on the site LAN — how to reach the node's setup page." },
        "ssid": { "type": "string", "description": "Wi-Fi network currently joined." },
        "mdns": { "type": "string", "description": "Reachable mDNS name, e.g. oat-gh2.local." },
        "ble_seen": { "type": "integer", "minimum": 0, "description": "BLE adverts heard since boot (BLE-tier nodes)." },
        "ble_decoded": { "type": "integer", "minimum": 0, "description": "BLE adverts decoded since boot." },
        "ble_matched": { "type": "integer", "minimum": 0, "description": "BLE adverts matched to a reportable sensor since boot." }
      }
    }
  },
  "allOf": [
    {
      "if": { "required": ["msg_type"], "properties": { "msg_type": { "const": "status" } } },
      "then": { "required": ["state"] },
      "else": { "required": ["stream", "measurement", "value"] }
    }
  ]
}
