320 lines
9.3 KiB
Python
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()
|