WJ import loader: metrology PCs are computers that control a synthesized tool
Some checks failed
CI / backend (push) Successful in 1m39s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 8s

Routing by pctype wrongly swept ~105 metrology PCs (CMM/Genspect/Keyence/Wax)
into measuringtools - but a PC that drives an instrument is still a computer;
the physical CMM/gauge is the tool. Dropped the pctype override: those PCs now
import as computers (measuringtools drops to the 48 real instrument rows).

Classic has no separate tool row for a metrology PC, so they'd be orphaned. New
metrology stage synthesizes a measuring-tool asset per metrology PC (typed by
its pctype: CMM / Form Tracer / Vision System / Genspect) and a Controls
relationship PC -> tool, mirroring what the runtime collector does.

Result: computers 663->751, measuringtools = 48 real + 88 synthesized (each
linked to its controlling PC), 88 new Controls relationships, no orphans.
This commit is contained in:
cproudlock
2026-07-13 14:38:00 -04:00
parent c9641a3c62
commit 9c909af66d

View File

@@ -50,8 +50,10 @@ def _upsert(h, path, payload, unique_field, idfield, list_path=None):
# --- classic machinetype routing (per the resolved import decisions) ---------
# measuringtools are the physical instruments (the CMM/gauge machine types). A
# PC that DRIVES one (pctype CMM/Genspect/Keyence/Wax) is still a computer, not a
# measuring tool - routing by pctype wrongly swept ~105 PCs into measuringtools.
MEASURING_MTYPES = {3, 5, 8, 23, 47, 48} # CMM, Wax, Eddy, Measuring, Inspection, Spline
MEASURING_PCTYPES = {5, 6, 7, 8} # PC subtypes: CMM, Wax/Trace, Keyence, Genspect
COMPUTER_MTYPES = {33, 20} # PC, Server
NETWORK_MTYPES = {16, 17, 18, 19, 46} # Access, IDF, Camera, Switch, Firewall
SKIP_MTYPES = {15, 44} # Printer (printers table), USB (cmmc source)
@@ -79,9 +81,7 @@ def _route(machinetypeid, pctypeid, islocationonly=False):
if machinetypeid in MEASURING_MTYPES:
return 'measuringtool'
if machinetypeid in COMPUTER_MTYPES:
if machinetypeid == 33 and pctypeid in MEASURING_PCTYPES:
return 'measuringtool'
return 'computer'
return 'computer' # a metrology PC is still a PC (it CONTROLS a tool)
return 'machine' # includes the 134 untyped (machinetypeid=1) shop machines
@@ -349,6 +349,52 @@ def stage_printers(h):
return {'printers': made}
# classic pctype -> the measuring instrument that PC drives
METROLOGY_PCTYPE_TOOL = {5: 'CMM', 6: 'Form Tracer', 7: 'Vision System', 8: 'Genspect'}
def stage_metrology(h):
"""Metrology PCs (CMM/Genspect/Keyence/Wax) are computers that CONTROL an
instrument. Classic has no separate tool row, so synthesize a measuring-tool
asset per metrology PC and a Controls relationship (PC -> tool), mirroring
what the runtime collector does. Runs after the asset hub (needs the PC's
assetid from the crosswalk)."""
controls_id = _upsert(h, '/api/assets/relationshiptypes',
{'relationshiptype': 'Controls'}, 'relationshiptype',
'relationshiptypeid', '/api/assets/relationshiptypes')
made = linked = 0
for m in h.source.rows('shopdb_src',
'SELECT machineid, alias, hostname, machinenumber, businessunitid, '
'mapleft, maptop, pctypeid FROM machines '
'WHERE machinetypeid=33 AND pctypeid IN (5,6,7,8) '
'AND (islocationonly IS NULL OR islocationonly=0)'):
pc_assetid = h.ids.get('asset', m['machineid'])
if not pc_assetid:
continue # PC not imported (dup-skipped etc.)
toolname = METROLOGY_PCTYPE_TOOL[m['pctypeid']]
typeid = _upsert(h, '/api/measuringtools/types', {'name': toolname},
'name', 'measuringtooltypeid', '/api/measuringtools/types')
pcname = (m['alias'] or m['hostname'] or m['machinenumber'] or '').strip()
status, data = h.post('/api/measuringtools', {
'assetnumber': f"MT-{m['machineid']}",
'name': (f"{pcname} {toolname}").strip() or toolname,
'measuringtooltypeid': typeid,
'businessunitid': h.ids.get('businessunit', m['businessunitid']),
'mapx': m['mapleft'], 'mapy': m['maptop']})
if status not in (200, 201):
continue
tool_assetid = _id_of(data, 'assetid')
made += 1
if tool_assetid and controls_id:
h.ids.put('metrologytool', m['machineid'], tool_assetid)
st, _ = h.post('/api/assets/relationships', {
'sourceassetid': pc_assetid, 'targetassetid': tool_assetid,
'relationshiptypeid': controls_id})
if st in (200, 201):
linked += 1
return {'measuringtools_synth': made, 'controls_links': linked}
def stage_communications(h):
"""Fold each asset's primary IP from the source communications table. There
is no bulk-communications endpoint, so this is one of the plan's documented
@@ -722,6 +768,7 @@ STAGES = {
'assets': stage_assets,
'locations': stage_locations,
'printers': stage_printers,
'metrology': stage_metrology,
'communications': stage_communications,
'applications': stage_applications,
'warranties': stage_warranties,
@@ -739,7 +786,7 @@ def main():
parser.add_argument(
'--stages',
default='reference,employees,catalog,assets,locations,printers,'
'communications,applications,warranties,notifications,'
'metrology,communications,applications,warranties,notifications,'
'knowledgebase,relationships,subnets,usb,verify',
help='comma list of stages to run')
args = parser.parse_args()