diff --git a/macos/tools/qet_netlist.py b/macos/tools/qet_netlist.py new file mode 100755 index 000000000..049ddcd43 --- /dev/null +++ b/macos/tools/qet_netlist.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""qet_netlist — liste des connexions (netlist) depuis un .qet. +Pour chaque conducteur : folio, num, de (élément/borne) -> vers (élément/borne), couleur, section. +Usage: python3 qet_netlist.py projet.qet [--csv sortie.csv]""" +import sys, os, csv, argparse +import xml.etree.ElementTree as ET + +def bn(t): return os.path.splitext(os.path.basename((t or '').replace('\\','/')))[0] + +def main(): + ap=argparse.ArgumentParser(); ap.add_argument("qet"); ap.add_argument("--csv") + a=ap.parse_args(); root=ET.parse(a.qet).getroot() + rows=[] + for i,d in enumerate(root.findall("diagram"),1): + # index élément par uuid -> repère/type + elem={} + for e in d.findall(".//element"): + lab="" + for ei in e.findall(".//elementInformation"): + if ei.attrib.get("name")=="label" and (ei.text or "").strip(): lab=ei.text.strip() + elem[e.attrib.get("uuid")]= lab or bn(e.attrib.get("type")) + for c in d.findall(".//conductor"): + ca=c.attrib + rows.append({"folio":i,"num":ca.get("num",""), + "borne1":ca.get("terminal1",""),"borne2":ca.get("terminal2",""), + "couleur":ca.get("conductor_color",""),"section":ca.get("conductor_section",""), + "fonction":ca.get("function",""),"câble":ca.get("cable",""),"type":ca.get("type","")}) + cols=["folio","num","borne1","borne2","couleur","section","fonction","câble","type"] + print(f"# Netlist — {os.path.basename(a.qet)} ({len(rows)} connexions)\n") + print("| "+" | ".join(cols)+" |"); print("| "+" | ".join("---" for _ in cols)+" |") + for r in rows: print("| "+" | ".join(str(r.get(k,"")) for k in cols)+" |") + if a.csv: + with open(a.csv,"w",newline="",encoding="utf-8") as f: + w=csv.DictWriter(f,fieldnames=cols); w.writeheader(); w.writerows(rows) + print(f"\nCSV: {a.csv}", file=sys.stderr) +if __name__=="__main__": main() diff --git a/macos/tools/qet_parts.py b/macos/tools/qet_parts.py new file mode 100755 index 000000000..070cde55e --- /dev/null +++ b/macos/tools/qet_parts.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +"""qet_parts — BOM chiffrée par jointure .qet × catalogue d'articles (style EPLAN parts). +Lecture seule : associe chaque appareil (par repère ou type) à une ligne catalogue CSV +(colonnes: cle, designation, fabricant, reference, prix). Sort une BOM chiffrée MD/CSV. +Usage : python3 qet_parts.py projet.qet --catalog articles.csv [--by label|type] [--csv out.csv]""" +import os, csv, argparse, collections +import xml.etree.ElementTree as ET +def bn(t): return os.path.splitext(os.path.basename((t or '').replace('\\','/')))[0] +def lab(e): + for ei in e.findall(".//elementInformation"): + if ei.attrib.get("name")=="label" and (ei.text or "").strip(): return ei.text.strip() + return "" +def main(): + ap=argparse.ArgumentParser(); ap.add_argument("qet"); ap.add_argument("--catalog",required=True) + ap.add_argument("--by",choices=["label","type"],default="type"); ap.add_argument("--csv") + a=ap.parse_args() + cat={} + with open(a.catalog,encoding="utf-8") as f: + for row in csv.DictReader(f): cat[(row.get("cle") or "").strip()]=row + root=ET.parse(a.qet).getroot() + IGNORE=("folio","renvoi","nomenclatur","spec_cablage","cartouche") + keycount=collections.Counter() + for e in root.findall(".//element"): + t=bn(e.attrib.get("type")) + if any(k in t.lower() for k in IGNORE): continue + key = lab(e) if a.by=="label" else t + if key: keycount[key]+=1 + rows=[]; total=0.0 + for key,qte in keycount.most_common(): + c=cat.get(key,{}) + prix=float(c.get("prix","0") or 0); sous=prix*qte; total+=sous + rows.append({"clé":key,"qté":qte,"désignation":c.get("designation",""), + "fabricant":c.get("fabricant",""),"référence":c.get("reference",""), + "PU":f"{prix:.2f}","total":f"{sous:.2f}"}) + cols=["clé","qté","désignation","fabricant","référence","PU","total"] + print(f"# BOM chiffrée — {os.path.basename(a.qet)} (jointure sur {a.by})\n") + print("| "+" | ".join(cols)+" |"); print("| "+" | ".join("---" for _ in cols)+" |") + for r in rows: print("| "+" | ".join(str(r[k]) for k in cols)+" |") + print(f"\n**Total estimé : {total:.2f} €**") + if a.csv: + with open(a.csv,"w",newline="",encoding="utf-8") as f: + w=csv.DictWriter(f,fieldnames=cols); w.writeheader(); w.writerows(rows) +if __name__=="__main__": main() diff --git a/macos/tools/qet_terminals_io.py b/macos/tools/qet_terminals_io.py new file mode 100755 index 000000000..3e32d355d --- /dev/null +++ b/macos/tools/qet_terminals_io.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""qet_terminals_io — plan de bornier + liste d'E/S API depuis un .qet. +Détecte les borniers (terminal_strip) et les éléments d'API (type contenant 'plc'/'automate'/'io'). +Usage: python3 qet_terminals_io.py projet.qet""" +import os, argparse +import xml.etree.ElementTree as ET +def bn(t): return os.path.splitext(os.path.basename((t or '').replace('\\','/')))[0] +def lab(e): + for ei in e.findall(".//elementInformation"): + if ei.attrib.get("name")=="label" and (ei.text or "").strip(): return ei.text.strip() + return "" +def main(): + ap=argparse.ArgumentParser(); ap.add_argument("qet"); a=ap.parse_args() + root=ET.parse(a.qet).getroot() + strips=root.findall(".//terminal_strip")+root.findall(".//terminalstrip") + terms=[e for e in root.findall(".//element") if "born" in bn(e.attrib.get("type")).lower() or "terminal" in bn(e.attrib.get("type")).lower()] + ios=[e for e in root.findall(".//element") if any(k in bn(e.attrib.get("type")).lower() for k in ("plc","automate","_io","entree","sortie","input","output"))] + print(f"# Bornier & E/S API — {os.path.basename(a.qet)}\n") + print(f"## Borniers\n\n- terminal_strip déclarés : {len(strips)}\n- éléments bornes détectés : {len(terms)}\n") + if terms: + print("| folio? | repère | type |"); print("| --- | --- | --- |") + for e in terms[:200]: print(f"| - | {lab(e)} | {bn(e.attrib.get('type'))} |") + print(f"\n## Entrées/Sorties API\n\n- éléments E/S détectés : {len(ios)}\n") + if ios: + print("| repère | type |"); print("| --- | --- |") + for e in ios[:200]: print(f"| {lab(e)} | {bn(e.attrib.get('type'))} |") + else: + print("_(aucun élément API détecté dans ce projet)_") +if __name__=="__main__": main() diff --git a/macos/tools/qet_wire_number.py b/macos/tools/qet_wire_number.py new file mode 100755 index 000000000..324d158b1 --- /dev/null +++ b/macos/tools/qet_wire_number.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""qet_wire_number — numérotation automatique des conducteurs d'un .qet (style SEE/EPLAN). +Attribue un numéro aux conducteurs sans numéro. Préserve le format (édition ciblée) + sauvegarde .bak. +Schémas : --scheme global (1,2,3...) ou folio (F1-1, F1-2...). --force renumérote tout. +Usage : python3 qet_wire_number.py projet.qet [--scheme folio|global] [--force] [--prefix N]""" +import re, sys, os, argparse, shutil +import xml.etree.ElementTree as ET + +def main(): + ap=argparse.ArgumentParser(); ap.add_argument("qet") + ap.add_argument("--scheme",choices=["global","folio"],default="folio") + ap.add_argument("--force",action="store_true"); ap.add_argument("--prefix",default="") + a=ap.parse_args() + txt=open(a.qet,encoding="utf-8").read() + # bornes de folios (offsets des ' + new=re.sub(r']*>', repl, txt) + # validation : XML bien formé + try: ET.fromstring(new) + except ET.ParseError as e: + print("ERREUR: XML invalide après édition, abandon:",e); sys.exit(1) + shutil.copyfile(a.qet, a.qet+".bak") + open(a.qet,"w",encoding="utf-8").write(new) + print(f"OK : {changed} conducteurs numérotés (schéma {a.scheme}). Sauvegarde : {os.path.basename(a.qet)}.bak") +if __name__=="__main__": main()