{
 "$schema": "https://json-schema.org/draft/2020-12/schema",
 "$id": "https://portrayal.dev/schemas/v3/rack.schema.json",
 "title": "Portrayal rack file (v4)",
 "description": "A Rack Builder document: one or more racks, the devices in them, and the cables between their ports. What the kit's Rack Builder reads and writes.",
 "type": "object",
 "required": [
  "format",
  "version",
  "id",
  "racks"
 ],
 "properties": {
  "format": {
   "const": "portrayal-rack",
   "description": "Always portrayal-rack."
  },
  "version": {
   "const": 4,
   "description": "The file version. Older files are migrated on load; newer ones are refused. Version 4 carries half-unit positions and the keys of the rack file version 4 design note. A reader keeps a key it does not know, at every level of the file."
  },
  "id": {
   "type": "string",
   "minLength": 1,
   "description": "The document's id, unique in a browser's library."
  },
  "racks": {
   "type": "array",
   "minItems": 1,
   "description": "The racks. The Rack Builder edits the first.",
   "items": {
    "$ref": "#/$defs/rack"
   }
  }
 },
 "$defs": {
  "rack": {
   "type": "object",
   "required": [
    "id",
    "name",
    "frame",
    "items",
    "zeroU",
    "cables"
   ],
   "properties": {
    "id": {
     "type": "string",
     "description": "Unique within the document."
    },
    "name": {
     "type": "string",
     "description": "What the rack is called."
    },
    "frame": {
     "$ref": "#/$defs/frame"
    },
    "items": {
     "type": "array",
     "description": "Devices and cable managers on the rails.",
     "items": {
      "$ref": "#/$defs/item"
     }
    },
    "zeroU": {
     "type": "array",
     "description": "Parts that stand beside the rack at an attachment point and take no rack unit, such as vertical cable managers. An entry with an id, a ref and an attachment point is placed; any other entry is kept as written.",
     "items": {
      "$ref": "#/$defs/zeroU"
     }
    },
    "cables": {
     "type": "array",
     "description": "Port-to-port cables.",
     "items": {
      "$ref": "#/$defs/cable"
     }
    },
    "dcim": {
     "type": "object",
     "description": "Names a NetBox or Nautobot import needs. Present only once set; unknown fields are kept.",
     "properties": {
      "site": {
       "type": "string",
       "description": "NetBox's site, Nautobot's location."
      },
      "role": {
       "type": "string",
       "description": "The device role."
      }
     }
    },
    "bundles": {
     "type": "array",
     "description": "Cable bundles: cables combed into one run and strapped. Optional; a rack with none may leave it out.",
     "items": {
      "$ref": "#/$defs/bundle"
     }
    }
   }
  },
  "frame": {
   "type": "object",
   "required": [
    "kind",
    "heightRU",
    "numbering",
    "holes",
    "railDepth",
    "usableDepth",
    "ref"
   ],
   "properties": {
    "kind": {
     "enum": [
      "four-post",
      "two-post"
     ],
     "description": "Four posts, or two."
    },
    "heightRU": {
     "type": "integer",
     "minimum": 1,
     "description": "How many U tall."
    },
    "numbering": {
     "enum": [
      "bottom-up",
      "top-down"
     ],
     "description": "Which way the U labels count. Positions are always stored bottom-up."
    },
    "holes": {
     "type": "object",
     "required": [
      "style",
      "thread"
     ],
     "description": "The rail holes.",
     "properties": {
      "style": {
       "enum": [
        "square",
        "tapped",
        "round"
       ],
       "description": "Square (cage nuts), tapped, or round (unthreaded)."
      },
      "thread": {
       "enum": [
        "12-24",
        "10-32",
        "M6",
        null
       ],
       "description": "The thread of tapped holes; null for square."
      }
     }
    },
    "railDepth": {
     "type": [
      "number",
      "null"
     ],
     "exclusiveMinimum": 0,
     "description": "Millimetres between front and rear rails; null on two posts."
    },
    "usableDepth": {
     "type": "number",
     "exclusiveMinimum": 0,
     "description": "Millimetres from the front rail to the deepest point a device may reach."
    },
    "ref": {
     "type": [
      "string",
      "null"
     ],
     "description": "A catalogue rack this frame was taken from, if any."
    },
    "startU": {
     "type": "integer",
     "minimum": 1,
     "description": "The number of the first unit, for a frame whose labels do not start at 1. Absent is 1."
    },
    "setback": {
     "type": "number",
     "description": "Millimetres a faceplate stands behind its rail in this rack, for an item that states none of its own. Absent until set."
    },
    "width": {
     "type": "number",
     "exclusiveMinimum": 0,
     "description": "The rail width in inches. Absent is 19."
    },
    "options": {
     "description": "The options of a catalogue rack this frame takes. Kept as written; nothing reads one until a product defines it."
    }
   }
  },
  "item": {
   "type": "object",
   "required": [
    "id",
    "ref",
    "cfg",
    "label",
    "ru",
    "face",
    "turned",
    "swaps",
    "fields"
   ],
   "dependentRequired": {
    "on": [
     "unit"
    ],
    "unit": [
     "on"
    ]
   },
   "properties": {
    "id": {
     "type": "string",
     "minLength": 1,
     "description": "Unique within the rack, and never reused while a cable end names it."
    },
    "ref": {
     "type": "string",
     "minLength": 1,
     "description": "The device, by its catalogue ref."
    },
    "cfg": {
     "type": "string",
     "description": "Which of the device's configurations."
    },
    "label": {
     "type": "string",
     "description": "The name shown on the drawing."
    },
    "ru": {
     "type": "number",
     "minimum": 1,
     "multipleOf": 0.5,
     "description": "Its bottom, in rack units counted bottom-up from 1, in steps of 0.5: 12 is the bottom of U12 and 12.5 the middle of it."
    },
    "face": {
     "enum": [
      "front",
      "rear"
     ],
     "description": "The face it is mounted on."
    },
    "turned": {
     "type": "boolean",
     "description": "Mounted back to front."
    },
    "swaps": {
     "type": "object",
     "description": "What the Explorer seated: slot path to part ref."
    },
    "fields": {
     "type": "object",
     "description": "Editable text the Explorer set."
    },
    "on": {
     "type": "string",
     "description": "A cable manager's host: the id of the device it bolts over."
    },
    "unit": {
     "type": "integer",
     "minimum": 1,
     "description": "Which of the host's units, from 1 at its bottom."
    },
    "side": {
     "description": "left or right: the rail a part narrower than the rack opening is on, seen from its face. Absent, it is across both rails."
    },
    "roll": {
     "enum": [
      90,
      180,
      270
     ],
     "description": "Degrees it is rolled clockwise, as seen in the elevation of the face it is on. Absent is upright."
    },
    "states": {
     "$ref": "#/$defs/states"
    },
    "readings": {
     "$ref": "#/$defs/readings"
    },
    "readingsAsWritten": {
     "description": "Readings the loader could not use, kept whole as they were written."
    },
    "setback": {
     "type": "number",
     "description": "Millimetres its faceplate stands behind its rail. Absent, the device default and then the frame setback apply."
    },
    "heldBy": {
     "type": "object",
     "description": "What holds an item that is not bolted to the rails itself: a shelf, a drawer or a rail of another item.",
     "properties": {
      "item": {
       "type": "string",
       "description": "The id of the item that holds it."
      },
      "at": {
       "type": "string",
       "description": "Which place on that item, by its id there."
      },
      "x": {
       "type": "number",
       "description": "Millimetres along that place."
      },
      "z": {
       "type": "number",
       "description": "Millimetres back from its front."
      },
      "turn": {
       "description": "How it is turned on that place."
      }
     }
    }
   }
  },
  "zeroU": {
   "type": "object",
   "description": "A part beside the rack. Its place (id, ref, at, offsetMm) is described but not constrained, so an entry of another shape is still a valid entry; the kit places an entry whose id, ref and at are non-empty strings, and keeps any other as written. What it is set to (swaps, fields, states, readings, heldBy) is typed as on an item.",
   "properties": {
    "id": {
     "description": "Unique within the rack's zeroU entries; z1, z2 and so on. The loader gives an entry without one an id."
    },
    "ref": {
     "description": "The part, by its catalogue ref."
    },
    "cfg": {
     "description": "Which of the part's configurations."
    },
    "label": {
     "description": "The name shown on the drawing."
    },
    "at": {
     "description": "The attachment point it stands at: left or right on a two-post, left-front, right-front, left-rear or right-rear on a four-post. The lane of that name runs beside it."
    },
    "offsetMm": {
     "description": "A number: its bottom, in millimetres above the bottom of the rails; a whole number of U as the kit writes it."
    },
    "between": {
     "description": "True when it stands between this rack and the next one, serving both."
    },
    "swaps": {
     "type": "object",
     "description": "What is seated in it: slot path to part ref, as on an item."
    },
    "fields": {
     "type": "object",
     "description": "Its part settings, as on an item."
    },
    "states": {
     "$ref": "#/$defs/states"
    },
    "readings": {
     "$ref": "#/$defs/readings"
    },
    "heldBy": {
     "type": "object",
     "description": "What it hangs on, such as the two brackets of a PDU.",
     "properties": {
      "parts": {
       "type": "array",
       "description": "The ids of the zero-U entries of this rack that hold it, one or more.",
       "items": {
        "type": "string",
        "minLength": 1
       }
      }
     }
    },
    "channel": {
     "description": "The channel of the frame it stands in, by its id on the catalogue rack the frame names."
    },
    "readingsAsWritten": {
     "description": "Readings the loader could not use, kept whole as they were written."
    }
   }
  },
  "end": {
   "type": "object",
   "required": [
    "item",
    "path",
    "view"
   ],
   "properties": {
    "item": {
     "type": "string",
     "description": "The device's id. A removed device's id is kept, and the end reads as loose."
    },
    "path": {
     "type": "string",
     "description": "The port's path on the face. A power inlet, an outlet, a DC terminal or a ground stud is named by the path of its connector slot."
    },
    "view": {
     "enum": [
      "front",
      "rear"
     ],
     "description": "Which of the device's own panels the port is on."
    },
    "plug": {
     "type": "string",
     "minLength": 1,
     "description": "The plug or lug the cable has at this end, as a component ref. Stated only where the port takes more than one; absent, the plug is derived from what the port accepts."
    }
   }
  },
  "waypoint": {
   "type": "object",
   "description": "One stop on a route: an element of a placed device's drawing (item and via), or a lane beside the rails at a U (lane and ru).",
   "properties": {
    "item": {
     "type": "string",
     "minLength": 1,
     "description": "A placed device's id, with via."
    },
    "via": {
     "type": "string",
     "minLength": 1,
     "description": "The element of that device's drawing the cable passes through, such as a ring or a duct."
    },
    "lane": {
     "type": "string",
     "minLength": 1,
     "description": "A lane beside the rails, with ru."
    },
    "ru": {
     "type": "integer",
     "description": "The U the lane is crossed at."
    }
   },
   "dependentRequired": {
    "item": [
     "via"
    ],
    "via": [
     "item"
    ],
    "lane": [
     "ru"
    ],
    "ru": [
     "lane"
    ]
   }
  },
  "cable": {
   "type": "object",
   "required": [
    "id",
    "a",
    "b",
    "media",
    "purpose",
    "label",
    "route"
   ],
   "properties": {
    "id": {
     "type": "string",
     "minLength": 1,
     "description": "Unique within the rack."
    },
    "a": {
     "$ref": "#/$defs/end"
    },
    "b": {
     "$ref": "#/$defs/end"
    },
    "media": {
     "type": "string",
     "description": "The cable type (os2, om3, om4, om5, cat6, cat6a, dac, aoc, power, ground), or empty. power is a conductor that carries power, an AC cord or a DC feed; ground is a bonding conductor."
    },
    "purpose": {
     "type": "string",
     "description": "What it is for."
    },
    "label": {
     "type": "string",
     "description": "The text on its tags."
    },
    "length": {
     "type": "object",
     "required": [
      "value",
      "unit",
      "source"
     ],
     "description": "A usable length.",
     "properties": {
      "value": {
       "type": "number",
       "exclusiveMinimum": 0,
       "description": "Above 0."
      },
      "unit": {
       "type": "string",
       "minLength": 1,
       "description": "As written; m when the file stated none."
      },
      "source": {
       "type": "string",
       "description": "Where it came from: entered, or routed when the Rack Builder worked it out from the route."
      },
      "measured": {
       "type": "number",
       "exclusiveMinimum": 0,
       "description": "Written before version 4: metres along the route with the allowances of then. A version-4 kit writes need in its place and drops this at the next measure."
      },
      "need": {
       "type": "number",
       "exclusiveMinimum": 0,
       "description": "Metres the cable must at least be: its path, what is inside its two ports and the short tolerance of its maker. Present beside a routed length."
      }
     }
    },
    "lengthAsWritten": {
     "description": "A length the loader could not use, kept whole as it was written."
    },
    "color": {
     "type": "string",
     "pattern": "^#[0-9a-f]{6}$",
     "description": "The cable's own color: # and six lower-case hex digits. Absent, the jacket color of its two ends (when both state one and they agree), else its type's, else its media's is drawn."
    },
    "colorAsWritten": {
     "description": "A color the loader could not use, kept whole as it was written. The cable then has no color of its own."
    },
    "route": {
     "type": "array",
     "description": "The waypoints the cable runs through, in order. Empty means the Rack Builder chooses the way.",
     "items": {
      "$ref": "#/$defs/waypoint"
     }
    },
    "routeEdited": {
     "type": "boolean",
     "description": "True once someone has changed the route by hand, so it is no longer chosen for them."
    },
    "routeAsWritten": {
     "type": "array",
     "description": "A route containing an entry the loader could not read, kept whole as it was written. The readable waypoints are in route."
    },
    "slack": {
     "type": "array",
     "description": "Where the spare length is stored. Absent, nothing has been said; an empty list says it is stored nowhere.",
     "items": {
      "$ref": "#/$defs/slackEntry"
     }
    },
    "slackAsWritten": {
     "description": "A slack list holding an entry the loader could not use, kept whole as it was written. The usable entries are in slack."
    },
    "lay": {
     "type": "array",
     "description": "Where the cable was laid by hand in a pathway.",
     "items": {
      "$ref": "#/$defs/layEntry"
     }
    },
    "straps": {
     "type": "object",
     "description": "The straps of a single cable.",
     "properties": {
      "anchors": {
       "type": "boolean",
       "description": "False for no strap tying it to a tray it passes."
      }
     }
    }
   }
  },
  "bundle": {
   "type": "object",
   "required": [
    "id",
    "number",
    "label",
    "members",
    "route"
   ],
   "description": "Cables sharing part of their route, combed into one run and held with hook-and-loop straps. Membership is here only: a cable carries no bundle key.",
   "properties": {
    "id": {
     "type": "string",
     "minLength": 1,
     "description": "b1, b2 and so on: unique within the rack, and not the id of an item, a part beside the rack or a cable."
    },
    "number": {
     "type": "integer",
     "minimum": 1,
     "description": "Unique within the rack. What a tag prints when there is no label."
    },
    "label": {
     "type": "string",
     "description": "The bundle's name; empty for Bundle <number>."
    },
    "members": {
     "type": "array",
     "description": "The cables, in combing order. A cable is in at most one bundle.",
     "items": {
      "$ref": "#/$defs/member"
     }
    },
    "route": {
     "type": "array",
     "description": "The trunk: the run the members share, as waypoints in order.",
     "items": {
      "$ref": "#/$defs/waypoint"
     }
    },
    "routeAsWritten": {
     "type": "array",
     "description": "A trunk containing an entry the loader could not read, kept whole as it was written. The readable waypoints are in route."
    },
    "straps": {
     "type": "object",
     "description": "The hook-and-loop straps. Absent means every 12 in, with breakout and anchor straps.",
     "properties": {
      "every": {
       "$ref": "#/$defs/strapSpacing"
      },
      "breakout": {
       "type": "boolean",
       "description": "Whether a strap goes beside each point where a cable joins or leaves the bundle. When left out: true, unless every is null."
      },
      "anchors": {
       "type": "boolean",
       "description": "Whether the bundle is strapped to each tray with tie slots it lies on. When left out: true, unless every is null."
      }
     }
    }
   }
  },
  "member": {
   "type": "object",
   "required": [
    "cable"
   ],
   "description": "One cable of a bundle.",
   "properties": {
    "cable": {
     "type": "string",
     "minLength": 1,
     "description": "The cable's id."
    },
    "a": {
     "$ref": "#/$defs/waypoint",
     "description": "The trunk waypoint where it leaves the bundle for its a end. Absent, it joins where its own route first meets the trunk."
    },
    "b": {
     "$ref": "#/$defs/waypoint",
     "description": "The trunk waypoint where it leaves the bundle for its b end. Absent, it leaves where its own route last meets the trunk."
    }
   }
  },
  "strapSpacing": {
   "type": [
    "object",
    "null"
   ],
   "required": [
    "value",
    "unit"
   ],
   "description": "How far apart the spacing straps are, a length with its own unit; null for none.",
   "properties": {
    "value": {
     "type": "number",
     "exclusiveMinimum": 0,
     "description": "Above 0."
    },
    "unit": {
     "enum": [
      "in",
      "mm"
     ],
     "description": "in or mm."
    }
   }
  },
  "states": {
   "type": "object",
   "description": "What each switchable part is set to: part path to state name.",
   "additionalProperties": {
    "type": "string"
   }
  },
  "readings": {
   "type": "object",
   "required": [
    "source",
    "at"
   ],
   "description": "Readings taken from the device, with where they came from and when.",
   "properties": {
    "source": {
     "type": "string",
     "description": "Where the readings came from."
    },
    "at": {
     "type": "string",
     "description": "When they were taken, as an RFC 3339 time."
    },
    "values": {
     "type": "object",
     "description": "Part path to what was read there.",
     "additionalProperties": {
      "type": "object",
      "description": "What was read at one part.",
      "properties": {
       "current-a": {
        "type": "number",
        "minimum": 0,
        "description": "Amperes."
       },
       "power-w": {
        "type": "number",
        "minimum": 0,
        "description": "Watts."
       },
       "energy-kwh": {
        "type": "number",
        "minimum": 0,
        "description": "Kilowatt hours."
       }
      }
     }
    }
   }
  },
  "slackEntry": {
   "type": "object",
   "description": "Spare stored in a tray or on a spool of a placed part (item and via), or left at one end of the cable (end). At most one entry of a cable has no value: it takes the rest.",
   "properties": {
    "item": {
     "type": "string",
     "minLength": 1,
     "description": "The id of the placed item or zero-U part, with via."
    },
    "via": {
     "type": "string",
     "minLength": 1,
     "description": "The tray or spool on that part, with item."
    },
    "end": {
     "enum": [
      "a",
      "b"
     ],
     "description": "The end of the cable the spare is left at."
    },
    "form": {
     "enum": [
      "coil",
      "serpentine",
      "spool"
     ],
     "description": "How it is stored; a coil when left out."
    },
    "value": {
     "type": "number",
     "exclusiveMinimum": 0,
     "description": "How much is stored there."
    },
    "unit": {
     "type": "string",
     "minLength": 1,
     "description": "The unit of value; m when left out."
    }
   },
   "dependentRequired": {
    "item": [
     "via"
    ],
    "via": [
     "item"
    ]
   }
  },
  "layEntry": {
   "type": "object",
   "required": [
    "item",
    "via"
   ],
   "description": "One pathway the cable was laid in by hand. An entry states a position, a face, or both.",
   "properties": {
    "item": {
     "type": "string",
     "minLength": 1,
     "description": "The id of the placed part the pathway is on."
    },
    "via": {
     "type": "string",
     "minLength": 1,
     "description": "The pathway on that part."
    },
    "position": {
     "type": "integer",
     "minimum": 1,
     "description": "Its place across the pathway, counted from 1."
    },
    "face": {
     "enum": [
      "top",
      "underside"
     ],
     "description": "The face of a tray it lies on."
    }
   }
  }
 }
}
