# 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 = - '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_ 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