The equipment tree and labels
A plant already has a way of organizing itself, and every SCADA, MES and historian it owns uses it: ISA-95. Enterprise, site, area, line, cell, unit, equipment. So that is what HotLoop uses, one tree of it, and not a separate list for floors, another for areas and a third for devices. See ENTITIES.md for what hangs off it.
Levels
Section titled “Levels”| Level | What it usually is |
|---|---|
enterprise |
The company |
site |
A plant |
area |
A department or building: Packing, Utilities, Tank farm |
line |
A production line |
cell |
A work cell on a line, or a process cell in a batch plant |
unit |
A production unit: a reactor, a chiller, a furnace |
equipment |
A machine or a part of one: a pump, a skid, a motor |
A child is always a lower level than its parent, so a site never goes under a
line. It can skip levels: a unit straight under an area is fine, and so is a
line straight under a site in a plant that has no areas. The one exception is
equipment inside equipment, because a skid holds pumps and a pump holds a
motor, and ISA-95 stops naming levels long before a plant stops nesting
things. A node can be at the root at any level; most plants start at site.
Two siblings with the same level and name are the same place, so the tree refuses the second one.
A node can never become its own ancestor: moving a node under itself or
anything below it is refused. The only way to get a loop is to edit the
equipment table by hand. If somebody does, nothing that walks the tree
hangs, and changing a node in the loop is refused with a message naming the
nodes and telling you to set one of their parent_ids to NULL.
Site, area and line text still work
Section titled “Site, area and line text still work”Every device has had free-text site, area and line fields since before
the tree existed. They are not going anywhere. The 4.15 device editor writes
them, list_devices and describe_device read them, and so does every agent
and script somebody wrote against those. So the text and the tree are kept in
step, in both directions, in the same transaction as the change:
Saving a device with text and no equipmentId finds the site, area and
line the text names, or creates them, each under the one before it, and puts
the device on the deepest one. Two devices with the same text land on the
same node; nothing is ever duplicated. The text you sent is the text that is
stored, exactly: a client that knows nothing about the tree gets back what it
always got, plus an equipmentId it can ignore.
Saving a device with an equipmentId that moves it takes the text from the
node’s path, whatever text came with it. Put a device on a cell under
Plant A / Packing / Line 2 and its text becomes Plant A, Packing,
Line 2.
Two details make that safe for clients that do not know about the tree:
- Text that still says what the device’s current node says keeps the device where it is. The text only has three columns, so a device on a cell or a machine below its line can only say the line. Saving it with that same text does not knock it up to the line.
- A client that reads a device and sends the whole thing back,
equipmentIdand all, with the text edited, gets the text edit. TheequipmentIdit echoed is the one the device already had, so it is not a move, and the text decides.
A site is found wherever it is, not just at the root, so putting Plant A
under an enterprise does not make the next device that says Plant A create a
second one.
Renaming or moving a node rewrites the text of every device at or below it, in the same transaction. If any of that fails, none of it happened: a tree that says one thing and device text that says another is exactly what keeping both was meant to prevent. The answer and the system log list every device whose text changed.
The upgrade
Section titled “The upgrade”Migration 0006_hierarchy.sql builds the tree from the text every existing
device already carries: one site per distinct site name, one area per
distinct area name under its site, one line per distinct line under its area,
and each device on the deepest node its text names. A device with an area and
no site gets a root area; one with a line and no area gets a line straight
under its site. Text that is only spaces names nothing. The text is used
exactly as stored, spaces and case included, because the tree’s names are
what the text gets rewritten from later, and trimming during the upgrade would
be a silent rename of every device that had a stray space. No device’s text
changes in the upgrade.
The Go side places a device by the same rules, and a test holds the two together: every device the upgrade placed, saved again untouched, stays where it is and grows the tree by nothing.
Deleting a node
Section titled “Deleting a node”A node with anything under it is refused: child nodes, or devices placed on it. Move them first. The refusal names what is in the way.
The alternative was re-parenting the children onto the deleted node’s parent. That moves every device below it and rewrites their text, under a node nobody touched, and can put a line straight under a site with no area in between, which is a tree nobody drew. Unplacing the devices instead would leave their text naming a place that no longer exists, and the next save of any of them would quietly create the node again. Both are surprises. A refusal that says what to move is not.
Entities placed on a deleted node lose the placement and go back to following their device.
Entities on the tree
Section titled “Entities on the tree”Every entity response now carries three more fields:
| Field | What |
|---|---|
equipment_id |
Where the entity itself is placed, or null when it just goes where its device goes |
area_id |
The nearest area at or above the entity’s own node, or its device’s node when it has none. Anything grouping by area reads this field. Null when there is no area above it |
labels |
The entity’s label_ids |
An entity’s own place wins over its device’s. That matters for a device that
is a whole PLC: its temperature tag belongs in the furnace, its chiller tag in
Utilities. Place an entity with PUT /api/entities/{entity_id}:
{ "equipment_id": 42, "labels": ["critical"] }"equipment_id": null goes back to following the device. labels replaces the
whole set. Both need entities.write, like every other registry field.
Labels
Section titled “Labels”A label is a tag in the everyday sense: critical, line 1 utilities,
calibrate quarterly. Devices and entities carry any number of them.
A label has a label_id and a name. The label_id is made from the name the
first time (lowercase letters, digits and underscores, _2, _3 on a
collision) and never changes, because it is what automations and screens
refer to. The name can change whenever you like. You can also choose the
label_id yourself when you create one.
Deleting a label takes it off everything carrying it.
Device labels are set with PUT /api/devices/{id}/labels, not through
POST /api/devices. Every client that saves a device today sends the whole
device without labels, and would wipe them on every save.
The equipment and label endpoints use snake_case field names, like the entity
endpoints, because the tree is the entity model’s area registry and is shaped
for the same consumers. The device’s new field is equipmentId, camelCase,
like the rest of the device API.
| Endpoint | Permission | What |
|---|---|---|
GET /api/equipment |
equipment.read |
Every node, with its path of names from the root. Filter: ?level= |
GET /api/equipment/{id} |
equipment.read |
{"equipment", "children", "devices", "entities"}: the node, the nodes directly under it, and what is placed directly on it |
POST /api/equipment |
equipment.write |
{"parent_id", "level", "name", "description"} |
PUT /api/equipment/{id} |
equipment.write |
Any of name, description, level, parent_id (null for the root). Answers {"equipment", "devices_rewritten"} |
DELETE /api/equipment/{id} |
equipment.write |
Refused with 409 while anything is under it |
GET /api/labels |
equipment.read |
Every label |
GET /api/labels/{label_id} |
equipment.read |
{"label", "devices", "entities"} carrying it |
POST /api/labels |
equipment.write |
{"label_id"?, "name", "color", "icon", "description"} |
PUT /api/labels/{label_id} |
equipment.write |
Name, color, icon, description. Never the label_id |
DELETE /api/labels/{label_id} |
equipment.write |
Removes it from everything |
PUT /api/devices/{id}/labels |
equipment.write |
{"labels": [...]}, the whole set |
POST /api/devices |
devices.write |
Gains the optional equipmentId, as above |
GET /api/devices/{id} |
devices.read |
Gains labels next to device and tags |
Every role holds equipment.read: where things are is what a device’s site and
area already told a viewer. Only admin holds equipment.write, because
renaming or moving one node rewrites the site, area and line of every device
under it, and that is configuration.
Every change lands in the system log, category config, subject
equipment:<id> or label:<label_id>.
list_devices and describe_device answer from the text, as they always did,
and their output is unchanged. A test pins it to what the release before the
tree answered, byte for byte, and then rearranges the tree around the device
(an enterprise above its site, a cell below its line, a label) and checks
nothing an agent sees moved. When the tree does change the text, a rename
say, an agent sees the new text, the same as if somebody had typed it into the
device. list_entities is where agents see the tree: it filters by a node
and everything under it, and gives each entity’s place. See
MCP.md.
Backup and restore
Section titled “Backup and restore”equipment, labels, entity_labels and device_labels are in the backup,
and so are the new equipment_id columns. equipment references itself
(parent_id), which a restore cannot order: after a node moves, a parent can
easily be stored after its child. The constraint is DEFERRABLE INITIALLY DEFERRED, so it is checked when the restore commits, once every row is in,
and the same goes for anything else that rewrites the tree in more than one
statement. The backup guard test allows a table to reference itself only when
that constraint is deferred, and the round-trip test stores a child before its
parent on purpose.
A backup taken before this version still restores. Its devices and
entities files have one column fewer; the restore loads them into the
table’s first columns (a new column is always added at the end) and the new
one takes its default. Then, in the same transaction, it places every restored
device on the tree by its text, the way the upgrade would have.
The Plant screen
Section titled “The Plant screen”The tree, down the side, with a card for whichever node is picked: its path,
the devices on it, the entities placed on it, and what is below it. An admin
can add a node at the root or under the one picked, rename it, change its
level, move it, delete it, place a device on it or move one off, and set a
device’s labels. The level list offers only the levels the rules allow, both
against the parent and against the children already below, and a move offers
only the parents a node of that level can go under, never the node itself or
anything below it. The server checks all of it again, and a refusal (a delete
with something still under it, say) is shown exactly as it wrote it. Moving a
device is the same save the device editor makes, with an equipmentId, so the
text follows the tree as described above.
Labels are managed at the bottom of the screen: a name, a color and a
description, with the label_id chosen once and fixed. Entities get their
place and their labels from their drawer on the Entities screen.
Everyone else sees the same screen with nothing to press.