macOS/tools: suite SEE/EPLAN (netlist, bornier+E/S, num. auto fils, BOM articles)

- qet_netlist.py : liste des connexions (de/vers, couleur, section) MD/CSV
- qet_terminals_io.py : plan de bornier + liste E/S API
- qet_wire_number.py : numerotation auto des conducteurs (.qet, format preserve + .bak)
- qet_parts.py : BOM chiffree par jointure catalogue articles (CSV)
This commit is contained in:
2026-05-29 22:45:14 +02:00
parent af415fd8c6
commit 29efd794a7
4 changed files with 154 additions and 0 deletions
+36
View File
@@ -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()
+43
View File
@@ -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()
+29
View File
@@ -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()
+46
View File
@@ -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 <diagram )
dia_pos=[m.start() for m in re.finditer(r'<diagram\b', txt)]
def folio_of(pos):
f=0
for p in dia_pos:
if p<=pos: f+=1
else: break
return max(f,1)
counters={}; gcount=0; changed=0
def repl(m):
nonlocal gcount,changed
tag=m.group(0); pos=m.start()
cur=re.search(r'\bnum="([^"]*)"',tag)
has=cur and cur.group(1).strip()
if has and not a.force: return tag
f=folio_of(pos)
if a.scheme=="folio":
counters[f]=counters.get(f,0)+1; val=f"{a.prefix}F{f}-{counters[f]}"
else:
gcount+=1; val=f"{a.prefix}{gcount}"
changed+=1
if cur: return tag[:cur.start()]+f'num="{val}"'+tag[cur.end():]
return tag[:-1]+f' num="{val}">'
new=re.sub(r'<conductor\b[^>]*>', 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()