Scripts/AutoCAD/DPM/build_network_tables.py

320 lines
9.3 KiB
Python

#!/usr/bin/env python
"""
Generate DPM network tables from a Summary sheet.
Usage:
python build_network_tables.py INPUT.xlsx OUTPUT.xlsx
python build_network_tables.py INPUT.xlsx OUTPUT.xlsx --net2 DPM1,DPM2,...
Examples:
python build_network_tables.py MCM08_Network.xlsx MCM08_Network_NET1_NET2.xlsx
python build_network_tables.py Amazon_CDW5_Bypass.xlsx Bypass_NET1_NET2.xlsx --net2 BYAD_6_DPM1,BYCD_14_DPM1,BYDB_6_DPM1
Requirements: pip install pandas openpyxl
"""
import argparse
import re
from itertools import groupby
import pandas as pd
from openpyxl import Workbook
from openpyxl.styles import PatternFill, Alignment, Border, Side, Font
# ---------- CONSTANTS YOU PROBABLY KEEP THE SAME ----------
SUMMARY_SHEET = "Summary" # name of sheet with DPM / P_TAG1 / HP
RING_BASE = "11.200.1." # ring IPs: 11.200.1.2, 11.200.1.3, ...
STAR_NET1_BASE = "11.200.1." # NET1 STAR base
STAR_NET2_BASE = "11.200.2." # NET2 STAR base
STAR_START = 20 # first STAR IP is *.20
# VFD HP -> part number
HP_TO_PN = {
"2HP": "35S-6D2-P101",
"3HP": "35S-6D3-P101",
"5HP": "35S-6D4-P111",
"7.5HP": "35S-6D5-P111",
"10HP": "35S-6D6-P111",
"30HP": "25B-D043N114"
}
# ---------- HELPERS ----------
def pick_column(colnames, candidates):
"""Pick first matching column name by case-insensitive list of options."""
cand_upper = [c.upper() for c in candidates]
for c in colnames:
if str(c).strip().upper() in cand_upper:
return c
return None
def dpm_sort_key(dpm):
"""Natural sort: prefix (letters/underscore) + list of all numbers."""
s = str(dpm).strip()
m = re.match(r"^([A-Za-z_]+)", s)
prefix = m.group(1) if m else ""
nums = [int(x) for x in re.findall(r"\d+", s)]
return prefix, nums
def device_sort_key(tag):
"""
Sort devices inside a DPM:
1) FIO Masters/hubs (FIOM/FIOH/FIO/MASTER) first
2) Then by numbers inside name
3) VFD before others among non-masters
"""
s = str(tag).strip()
u = s.upper()
is_master = ("FIOM" in u) or ("FIOH" in u) or ("FIO" in u) or ("MASTER" in u)
master_prio = 0 if is_master else 1
nums = [int(x) for x in re.findall(r"\d+", s)]
typ_match = re.search(r"_([A-Za-z]+)\d*$", s)
typ = typ_match.group(1).upper() if typ_match else ""
if typ == "VFD":
t_order = 0
elif typ in ("FIOM", "FIO", "FIOH"):
t_order = 1
else:
t_order = 2
return (master_prio, nums, t_order, typ, s)
def part_number_for(device_name, rating_map):
"""Return part number string for a device."""
if not isinstance(device_name, str):
return ""
u = device_name.upper()
if "SPARE" in u:
return ""
# FIO masters / hubs
if ("FIOM" in u) or ("FIO" in u) or ("FIOH" in u) or ("MASTER" in u):
return "Murr 54631"
hp = rating_map.get(device_name, "")
return HP_TO_PN.get(hp, "")
def build_assignments(df, col_dpm, col_tag, col_rating):
"""Build sorted DPM list, device assignments and rating map."""
df = df[~df[col_dpm].isna()].copy()
unique_dpms = sorted(
{str(x).strip() for x in df[col_dpm].tolist()},
key=dpm_sort_key,
)
assign = {dpm: [] for dpm in unique_dpms}
rating_map = {}
for _, row in df.iterrows():
dpm = str(row[col_dpm]).strip()
tag = (
str(row[col_tag]).strip()
if col_tag and not pd.isna(row[col_tag])
else None
)
rating = (
str(row[col_rating]).strip()
if col_rating and not pd.isna(row[col_rating])
else ""
)
if tag:
assign.setdefault(dpm, []).append(tag)
rating_map[tag] = rating
for dpm in assign:
assign[dpm] = sorted(assign[dpm], key=device_sort_key)
return unique_dpms, assign, rating_map
def generate_rows(unique_dpms, assign, rating_map, net2_dpms):
"""
Generate rows for NET1 and NET2.
Each row: (dpm, ring_ip, assigned_dev, part_num, star_ip, dpm_port)
"""
# ring IP per DPM
ring_ip_map = {}
ring_counter = 2
for dpm in unique_dpms:
ring_ip_map[dpm] = f"{RING_BASE}{ring_counter}"
ring_counter += 1
rows_net1 = []
rows_net2 = []
star1_counter = STAR_START
star2_counter = STAR_START
for dpm in unique_dpms:
ring_ip = ring_ip_map[dpm]
tags = assign.get(dpm, [])
# Generate ports starting from 5, continuing beyond 28 if needed
num_devices = len(tags)
ports = list(range(5, 5 + num_devices))
for idx, port in enumerate(ports):
dev = tags[idx]
pn = part_number_for(dev, rating_map)
if dpm in net2_dpms:
star_ip = f"{STAR_NET2_BASE}{star2_counter}"
star2_counter += 1
rows_net2.append(
(dpm, ring_ip, dev, pn, star_ip, f"{dpm}_P{port}")
)
else:
star_ip = f"{STAR_NET1_BASE}{star1_counter}"
star1_counter += 1
rows_net1.append(
(dpm, ring_ip, dev, pn, star_ip, f"{dpm}_P{port}")
)
return rows_net1, rows_net2
def write_sheet(ws, rows, title):
"""Write one NET sheet with merged yellow bands, D..I layout."""
ws.title = title
headers = ["DPM", "IP", "Assigned Device", "Part Number", "IP", "DPM PORT"]
for col, h in enumerate(headers, start=4):
c = ws.cell(row=3, column=col, value=h)
c.font = Font(bold=True)
c.alignment = Alignment(horizontal="center", vertical="center")
c.border = Border(
left=Side(style="thin"),
right=Side(style="thin"),
top=Side(style="thin"),
bottom=Side(style="thin"),
)
yellow = PatternFill("solid", fgColor="FFC000")
red = PatternFill("solid", fgColor="FF0000")
thin = Side(style="thin", color="000000")
border = Border(left=thin, right=thin, top=thin, bottom=thin)
center = Alignment(horizontal="center", vertical="center")
left = Alignment(horizontal="left", vertical="center")
widths = {4: 18, 5: 16, 6: 30, 7: 18, 8: 16, 9: 22}
for col, w in widths.items():
ws.column_dimensions[chr(64 + col)].width = w
row_idx = 4
if not rows:
ws.freeze_panes = "D4"
return
for dpm, group in groupby(rows, key=lambda x: x[0]):
g = list(group)
num_rows = len(g)
start = row_idx
end = row_idx + num_rows - 1
ws.merge_cells(start_row=start, start_column=4, end_row=end, end_column=4)
ws.merge_cells(start_row=start, start_column=5, end_row=end, end_column=5)
cD = ws.cell(row=start, column=4, value=g[0][0])
cE = ws.cell(row=start, column=5, value=g[0][1])
for cell in (cD, cE):
cell.fill = yellow
cell.alignment = center
cell.border = border
cell.font = Font(bold=False)
for i in range(num_rows):
row_vals = {
6: g[i][2], # Assigned Device
7: g[i][3], # Part Number
8: g[i][4], # IP
9: g[i][5], # DPM PORT
}
# Highlight rows beyond 24th device in red
is_overflow = i >= 24
fill_color = red if is_overflow else None
for col, val in row_vals.items():
c = ws.cell(row=start + i, column=col, value=val)
c.alignment = left if col in (6, 7, 9) else center
c.border = border
if fill_color:
c.fill = fill_color
for r in range(start + 1, end + 1):
for col in (4, 5):
c = ws.cell(row=r, column=col)
c.fill = yellow
c.border = border
row_idx = end + 1
ws.freeze_panes = "D4"
def main():
parser = argparse.ArgumentParser(description="Build DPM network tables.")
parser.add_argument("input_file", help="Input Excel file (with Summary sheet)")
parser.add_argument("output_file", help="Output Excel file")
parser.add_argument(
"--net2",
help="Comma-separated list of DPM names that should go to NET2 (.2 subnet)",
default="",
)
args = parser.parse_args()
net2_dpms = {
x.strip()
for x in args.net2.split(",")
if x.strip()
}
# Load Summary
df = pd.read_excel(args.input_file, sheet_name=SUMMARY_SHEET)
df.columns = [str(c).strip() for c in df.columns]
col_dpm = pick_column(df.columns, ["DPM"])
col_tag = pick_column(df.columns, ["P_TAG1", "P TAG1", "ASSIGNED DEVICE"])
col_rating = pick_column(df.columns, ["RATING", "HP", "POWER", "RATED"])
if not col_dpm or not col_tag:
raise RuntimeError(
f"Could not find DPM or P_TAG1/Assigned Device columns in sheet '{SUMMARY_SHEET}'"
)
unique_dpms, assign, rating_map = build_assignments(
df, col_dpm, col_tag, col_rating
)
rows_net1, rows_net2 = generate_rows(unique_dpms, assign, rating_map, net2_dpms)
# Build workbook
wb = Workbook()
ws1 = wb.active
write_sheet(ws1, rows_net1, "NET1 STRUCTURE")
ws2 = wb.create_sheet()
write_sheet(ws2, rows_net2, "NET2 STRUCTURE")
wb.save(args.output_file)
print(f"Saved: {args.output_file}")
if __name__ == "__main__":
main()