The MECHANISM was already a documented platform contract - ADR-001 defines partof as composition, makes controls propagate through it, and walks it first for map-position inheritance. The part-marker work used that rail rather than inventing one. What was undocumented is the PATTERN built on it: several devices answering to one identifier, each becoming its own asset filed under a parent. It existed only as a collector behaviour for part markers plus a docstring in the device map, so nothing told anyone how to apply it to another device type, or when not to. ASSET-COMPOSITION.md covers when to reach for it and when the shared identifier is a numbering fault instead, what propagation buys, how to declare a device type through the map or a per-site setting, what a backup kind must do to follow the device rather than the parent, how to find the next case with check-shared-machines, and why the parent is not disposable once devices hang off it - deactivating it breaks filing, and a hard delete cascades through backuprevisions.
155 lines
6.4 KiB
Markdown
155 lines
6.4 KiB
Markdown
# Composition: several devices under one parent
|
|
|
|
When more than one physical device answers to a single identifier, model each
|
|
device as its own asset and file it under a parent. This page is the pattern:
|
|
when to reach for it, what you get for free, and how to make the collector do
|
|
it without writing code.
|
|
|
|
The mechanism is not new - `partof` and its propagation rails are the ADR-001
|
|
platform contract. What is new is the recipe.
|
|
|
|
## The problem it solves
|
|
|
|
A machine number is supposed to identify one thing. Sometimes it does not.
|
|
|
|
At West Jefferson, several Telesis part markers serve one operation number:
|
|
0613, 0615 and WJPRT each have more than one. Their configurations differ, most
|
|
often by COM port. Treating the operation as the device collapsed them into a
|
|
single record, and the damage was quiet:
|
|
|
|
- Their backups overwrote each other, so the history read as one device
|
|
flip-flopping between configurations that were really two devices.
|
|
- No question about an individual device could be asked at all - how many there
|
|
are, which port one is on, which one failed.
|
|
- Every PC driving one contested the single `controls` link to the operation,
|
|
so whichever reported last appeared to own it.
|
|
|
|
None of that announced itself. It surfaced as duplicate backup rows, weeks
|
|
later.
|
|
|
|
## When to use it
|
|
|
|
Reach for composition when ALL of these hold:
|
|
|
|
- Several physical devices share one identifier, and that is CORRECT - not a
|
|
numbering mistake. Two PCs carrying the same machine number by accident is a
|
|
fault to fix on the PC, not a model to build. `flask relationships
|
|
check-shared-machines` tells the two apart.
|
|
- The devices are individually interesting: they fail, get replaced, carry
|
|
their own configuration or calibration.
|
|
- Something already identifies each device. One device per PC is the easy case,
|
|
because the PC names it.
|
|
|
|
Do NOT reach for it when the parent is simply a location. A room holding six
|
|
printers wants a Location, not a parent asset.
|
|
|
|
## What you get
|
|
|
|
Filing a device `partof` a parent buys behaviour already built:
|
|
|
|
- **Control propagates.** `controls` propagates through `partof` (ADR-001, and
|
|
seeded by `flask seed reference-data`). A PC controlling a device therefore
|
|
controls its parent by inheritance, so the PC does NOT need - and must not
|
|
have - a direct link to the parent. That is what stops several devices
|
|
contesting a link only one can hold.
|
|
- **Position inherits.** Map-position resolution walks `partof` first, so a
|
|
device with no coordinates of its own shows at its parent's position.
|
|
- **History separates.** Anything keyed on the asset - backup revisions,
|
|
relationships, notes, audit - is per device instead of merged.
|
|
|
|
## Making the collector do it
|
|
|
|
A PC that drives a subordinate device declares it in
|
|
`plugins/computers/pctypemap.py`:
|
|
|
|
```python
|
|
SUBORDINATE_DEVICE_MAP = {
|
|
'gea-shopfloor-partmarker': {
|
|
'assettype': 'machine', # core AssetType for the device
|
|
'typename': 'Part Marker', # type within that vocabulary
|
|
'description': 'Telesis part marker',
|
|
'suffix': 'PARTMARKER', # device asset number = <PC>-<suffix>
|
|
'partof': True, # file under the reported machine number
|
|
'label': 'collector:partmarker',
|
|
},
|
|
}
|
|
```
|
|
|
|
`partof: True` is the switch that matters. It files the device under the
|
|
operation the PC reports AND stops that PC claiming the operation directly.
|
|
Leave it False for a device that does not share an identifier - a CMM is a
|
|
subordinate device too, but no two CMMs answer to one number, so it links to
|
|
its PC and nothing more.
|
|
|
|
`label` is the relationship origin marker. Only rows carrying it are archived
|
|
by a collector push, so links made by hand are never touched. **Do not change
|
|
an existing label**: those exact strings are in production databases.
|
|
|
|
A site adds or retargets an entry without a code change, per ADR-015:
|
|
|
|
```
|
|
Setting: subordinatedevice_<pctype> category: pctypemapping
|
|
Value: {"assettype": "machine", "typename": "Marking Laser",
|
|
"suffix": "LASER", "partof": true, "label": "collector:partmarker"}
|
|
```
|
|
|
|
A malformed override falls back to the built-in default rather than failing the
|
|
collector push - a bad setting must not stop a bay reporting its inventory.
|
|
|
|
## Making a backup kind follow the device
|
|
|
|
A backup posted by a PC that drives a device belongs to the DEVICE, not the
|
|
parent. `plugins/backups/services/registry.py` resolves this through
|
|
`markerforsource`: it finds the PC by the reported `sourcehostname`, follows the
|
|
PC's active device link, and files against what it finds. It falls back to the
|
|
machine number whenever the device cannot be resolved - no hostname on the
|
|
payload, a lean build without the computers plugin, or a PC that has not
|
|
reported yet - because filing under the parent beats rejecting a backup.
|
|
|
|
Two things that matter if you add a kind:
|
|
|
|
- **`sourcehostname` is load-bearing**, not informational. Without it a backup
|
|
files against the parent and merges two devices' histories.
|
|
- **A revision chain is (asset, kind, source hostname)**, so two devices on one
|
|
parent keep separate chains even before they become separate assets.
|
|
|
|
## Finding the next one
|
|
|
|
```
|
|
flask relationships check-shared-machines
|
|
```
|
|
|
|
Read-only. Lists every machine number claimed by more than one PC and separates
|
|
the two cases by whether child assets exist:
|
|
|
|
```
|
|
0615 11 PCs, 11 child asset(s) - modelled
|
|
2026 2 PCs, NO child assets - F31N20R3, F4Z7S7J4
|
|
```
|
|
|
|
The first is composition working. The second is two PCs carrying one number -
|
|
fix that on the PC.
|
|
|
|
## The parent is not disposable
|
|
|
|
Once devices are filed under it, the parent is doing a real job even though it
|
|
may look empty: it is the thing that says those devices belong together.
|
|
Deactivating it (`isactive = 0`, which is what "delete" does in the UI) breaks
|
|
filing, because the resolver requires an ACTIVE parent - every subsequent
|
|
report warns and files nothing.
|
|
|
|
A hard SQL `DELETE` is worse: `backuprevisions.assetid` is `ON DELETE CASCADE`,
|
|
so it destroys history still attached to the parent.
|
|
|
|
If the parent looks wrong in a list - an operation number appearing among
|
|
machines - that is a CLASSIFICATION question, not a deletion one.
|
|
|
|
## See also
|
|
|
|
- `docs/adr/ADR-001-asset-as-platform-contract.md` - relationship types,
|
|
propagation rails, position inheritance
|
|
- `docs/adr/ADR-015-site-specific-configuration.md` - why the device map is
|
|
settings-overridable
|
|
- `docs/COLLECTOR-INTEGRATION.md` - the collector contract and the part-marker
|
|
case end to end
|