-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfpc_utilisasi.py
More file actions
7848 lines (6827 loc) · 390 KB
/
fpc_utilisasi.py
File metadata and controls
7848 lines (6827 loc) · 390 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
FPC Utilization capture script
- Module names (Type Module FPC / Module (per FPC)) are now taken ONLY from
'show chassis hardware detail' / 'show chassis fpc' XML outputs (tags <fpc> / <chassis-module>).
- If a slot has no valid module name, cell is left empty and a short preview is logged
to chassis_missing_modules.log for debugging.
"""
from concurrent.futures import ThreadPoolExecutor, as_completed
import datetime
import gc
import logging
import os
import re
import sys
import threading
import time
import traceback
def get_indonesia_timezone():
"""
Deteksi zona waktu Indonesia berdasarkan UTC offset sistem
WIB (UTC+7) - Waktu Indonesia Barat: Jakarta, Sumatra, Jawa, Kalimantan Barat dan Tengah
WITA (UTC+8) - Waktu Indonesia Tengah: Sulawesi, Bali, NTB, NTT, Kalimantan Timur dan Selatan
WIT (UTC+9) - Waktu Indonesia Timur: Papua, Maluku
"""
try:
# Dapatkan offset UTC dalam detik
utc_offset_seconds = time.timezone if time.daylight == 0 else time.altzone
utc_offset_hours = -utc_offset_seconds / 3600 # Konversi ke jam (negatif karena timezone)
# Mapping zona waktu Indonesia berdasarkan UTC offset
if abs(utc_offset_hours - 7) < 0.5: # UTC+7 (WIB)
return "WIB"
elif abs(utc_offset_hours - 8) < 0.5: # UTC+8 (WITA)
return "WITA"
elif abs(utc_offset_hours - 9) < 0.5: # UTC+9 (WIT)
return "WIT"
else:
# Default ke WIB jika tidak terdeteksi
return "WIB"
except Exception:
# Fallback default ke WIB
return "WIB"
# external libs
try:
import paramiko
except Exception:
sys.stderr.write("Missing dependency: paramiko. Install: pip install paramiko\n")
sys.exit(1)
try:
from xml.dom import minidom
from openpyxl import Workbook, load_workbook
from openpyxl.styles import NamedStyle, PatternFill, Border, Side, Alignment, Font
from openpyxl.worksheet.table import Table, TableStyleInfo
except Exception:
sys.stderr.write("Missing dependency: openpyxl or xml.dom. Install: pip install openpyxl\n")
sys.exit(1)
logging.getLogger('paramiko').setLevel(logging.ERROR)
# defaults - SEQUENTIAL EXECUTION FOR 100% SUCCESS
BANNER_TIMEOUT = 180 # 3 minutes for maximum reliability
INITIAL_TEST_RETRIES = 5 # Increased from 3 to 5
INITIAL_TEST_RETRY_DELAY = 10 # Increased delay
PER_NODE_RETRIES = 5 # Increased from 2 to 5 for better reliability
PER_NODE_RETRY_DELAY = 10 # Increased delay for better stability
MAX_WORKERS = 1 # SEQUENTIAL PROCESSING - One node at a time for 100% success
MAIN_SHEET = 'Utilisasi FPC'
UTIL_SHEET = 'Utilisasi Port'
ALARM_SHEET = 'Alarm Status'
HARDWARE_SHEET = 'Hardware Inventory'
SYSTEM_SHEET = 'System Performance'
DASHBOARD_SHEET = 'Dashboard Summary'
# globals set in main()
folder_daily_global = None
folder_monthly_global = None
capture_time_global = None
debug_folder_global = None
# ---------------- helper IO / logging ----------------
def print_banner(title, subtitle=None, width=80, style="main"):
"""
Print enhanced professional banner with multiple styles
Args:
title: Main title text
subtitle: Optional subtitle text
width: Banner width (default 80)
style: Banner style - 'main', 'section', 'sub', 'simple'
"""
styles = {
'main': {'border': '=', 'padding': 2, 'uppercase': True},
'section': {'border': '=', 'padding': 1, 'uppercase': False},
'sub': {'border': '-', 'padding': 1, 'uppercase': False},
'simple': {'border': '=', 'padding': 0, 'uppercase': False}
}
config = styles.get(style, styles['main'])
border_char = config['border']
padding = config['padding']
# Apply uppercase if specified
display_title = title.upper() if config['uppercase'] else title
display_subtitle = subtitle.upper() if subtitle and config['uppercase'] else subtitle
# Print top border
print(border_char * width)
# Add padding lines
for _ in range(padding):
print(border_char + " " * (width - 2) + border_char)
# Print title (centered)
title_padding = (width - len(display_title) - 2) // 2
remaining_padding = width - len(display_title) - 2 - title_padding
print(f"{border_char}{' ' * title_padding}{display_title}{' ' * remaining_padding}{border_char}")
# Print subtitle if provided
if display_subtitle:
subtitle_padding = (width - len(display_subtitle) - 2) // 2
remaining_padding = width - len(display_subtitle) - 2 - subtitle_padding
print(f"{border_char}{' ' * subtitle_padding}{display_subtitle}{' ' * remaining_padding}{border_char}")
# Add padding lines
for _ in range(padding):
print(border_char + " " * (width - 2) + border_char)
# Print bottom border
print(border_char * width)
def print_section_header(title, width=80, style="section"):
"""
Print enhanced section header with professional formatting
Args:
title: Section title
width: Header width (default 80)
style: Header style - 'section', 'subsection', 'info'
"""
print() # Add spacing before header
if style == "section":
print("=" * width)
title_padding = (width - len(title)) // 2
remaining_padding = width - len(title) - title_padding
print(f"{' ' * title_padding}{title}{' ' * remaining_padding}")
print("=" * width)
elif style == "subsection":
print("-" * width)
title_padding = (width - len(title) - 4) // 2 # Account for "-- " and " --"
remaining_padding = width - len(title) - 4 - title_padding
print(f"--{' ' * title_padding}{title}{' ' * remaining_padding}--")
print("-" * width)
elif style == "info":
# Compact info header
border_line = "▓" * width
print(border_line)
title_padding = (width - len(title) - 2) // 2
remaining_padding = width - len(title) - 2 - title_padding
print(f"▓{' ' * title_padding}{title}{' ' * remaining_padding}▓")
print(border_line)
def print_divider(text=None, width=80, style="light"):
"""
Print professional divider lines (Windows compatible)
Args:
text: Optional text in the middle of divider
width: Divider width
style: 'light', 'heavy', 'double', 'dots'
"""
dividers = {
'light': '-',
'heavy': '=',
'double': '=',
'dots': '.',
'simple': '-'
}
char = dividers.get(style, '-')
if text:
text = f" {text} "
text_width = len(text)
side_width = (width - text_width) // 2
remaining_width = width - text_width - side_width
print(f"{char * side_width}{text}{char * remaining_width}")
else:
print(char * width)
def print_info_box(title, items, width=80):
"""
Print professional information box
Args:
title: Box title
items: List of information items
width: Box width
"""
# Top border (Windows compatible)
print("+" + "-" * (width - 2) + "+")
# Title
title_padding = (width - len(title) - 4) // 2 # Account for borders and spaces
remaining_padding = width - len(title) - 4 - title_padding
print(f"| {' ' * title_padding}{title}{' ' * remaining_padding} |")
# Separator
print("+" + "-" * (width - 2) + "+")
# Items
for item in items:
if len(item) <= width - 4: # Fits in one line
item_padding = width - len(item) - 3
print(f"│ {item}{' ' * item_padding}│")
else: # Multi-line item
# Split long items
words = item.split()
lines = []
current_line = ""
for word in words:
if len(current_line + " " + word) <= width - 4:
current_line += (" " if current_line else "") + word
else:
if current_line:
lines.append(current_line)
current_line = word
if current_line:
lines.append(current_line)
for line in lines:
line_padding = width - len(line) - 3
print(f"| {line}{' ' * line_padding}|")
# Bottom border (Windows compatible)
print("+" + "-" * (width - 2) + "+")
def print_status(level, message, node_name=None, width=None, prefix=""):
"""Print consistent status messages with professional formatting (Windows compatible)"""
if width is None:
width = 80
# Define status level formatting (Windows compatible symbols)
status_formats = {
'INFO': {'color': '', 'symbol': 'i', 'tag': 'INFO'},
'SUCCESS': {'color': '', 'symbol': '+', 'tag': 'OK'},
'WARNING': {'color': '', 'symbol': '!', 'tag': 'WARN'},
'ERROR': {'color': '', 'symbol': 'X', 'tag': 'ERR'},
'PROCESSING': {'color': '', 'symbol': '*', 'tag': 'PROC'},
'CONNECTION': {'color': '', 'symbol': '@', 'tag': 'CONN'},
'DATA': {'color': '', 'symbol': '#', 'tag': 'DATA'},
'TIME': {'color': '', 'symbol': 'T', 'tag': 'TIME'},
'LOG': {'color': '', 'symbol': '>', 'tag': 'LOG'}
}
fmt = status_formats.get(level.upper(), {'symbol': '•', 'tag': level.upper()})
if node_name:
# Format with node name
if prefix:
print(f"{prefix}[{fmt['tag']}] [{node_name}] {message}")
else:
print(f" [{fmt['tag']}] [{node_name}] {message}")
else:
# Format without node name
if prefix:
print(f"{prefix}[{fmt['tag']}] {message}")
else:
print(f"[{fmt['tag']}] {message}")
def print_progress(current, total, node_name, operation="Processing", width=80):
"""Print consistent progress information (Windows compatible)"""
percentage = (current / total) * 100 if total > 0 else 0
progress_bar_width = 30
filled_width = int((current / total) * progress_bar_width) if total > 0 else 0
progress_bar = "#" * filled_width + "." * (progress_bar_width - filled_width)
print(f"[{current:3d}/{total}] {operation}: {node_name:<25} [{progress_bar}] {percentage:5.1f}%", end="", flush=True)
def get_desktop_path():
if os.name == 'nt':
try:
from ctypes import windll, create_unicode_buffer
buf = create_unicode_buffer(260)
CSIDL_DESKTOPDIRECTORY = 0x0010
result = windll.shell32.SHGetFolderPathW(None, CSIDL_DESKTOPDIRECTORY, None, 0, buf)
if result == 0:
return buf.value
except Exception:
pass
return os.path.join(os.path.expanduser('~'), 'Desktop')
def setup_debug_folder():
"""Setup All Debug folder untuk mengorganisir semua file debug"""
global debug_folder_global
if not debug_folder_global:
return
try:
# Buat struktur folder debug
debug_logs = os.path.join(debug_folder_global, 'Debug Logs')
debug_xml = os.path.join(debug_folder_global, 'Debug XML')
debug_temp = os.path.join(debug_folder_global, 'Temp Files')
os.makedirs(debug_logs, exist_ok=True)
os.makedirs(debug_xml, exist_ok=True)
os.makedirs(debug_temp, exist_ok=True)
# Buat README file
readme_content = f"""# All Debug Files - {capture_time_global.strftime('%d %B %Y %H:%M')}
Folder ini berisi semua file debug yang dihasilkan dari eksekusi script FPC Utilization.
## 📁 Debug Logs/
- File log debugging dari berbagai komponen
- Informasi parsing hardware, alarm, dan utilization
## 📁 Debug XML/
- File XML debug dan sample data
- RPC response samples untuk troubleshooting
## 📁 Temp Files/
- File temporary dan intermediate processing
## 📋 Generated Files:
"""
readme_file = os.path.join(debug_folder_global, 'README.md')
with open(readme_file, 'w', encoding='utf-8') as f:
f.write(readme_content)
except Exception as e:
print(f"Warning: Could not setup debug folder: {e}")
def get_debug_log_path(filename):
"""Get path for debug log file, organized in All Debug folder"""
global debug_folder_global
if not debug_folder_global:
return os.path.join(folder_daily_global, filename)
# Tentukan subfolder berdasarkan ekstensi file
if filename.endswith('.log'):
return os.path.join(debug_folder_global, 'Debug Logs', filename)
elif filename.endswith('.xml'):
return os.path.join(debug_folder_global, 'Debug XML', filename)
else:
return os.path.join(debug_folder_global, 'Temp Files', filename)
def save_log(path, content):
try:
with open(path, 'w', encoding='utf-8', errors='ignore') as f:
f.write(content)
except Exception:
try:
with open(path, 'wb') as f:
f.write(str(content).encode('utf-8', errors='ignore'))
except Exception:
pass
def append_error_log(path, msg):
try:
with open(path, 'a', encoding='utf-8', errors='ignore') as f:
f.write(msg + '\n')
except Exception:
pass
# ---------------- Excel helpers ----------------
def _add_named_style_safe(wb, style):
try:
wb.add_named_style(style)
except Exception:
pass
def dynamic_auto_resize_all_columns(ws, exclude_columns=None):
"""
Dynamic auto-resize semua kolom berdasarkan content aktual dengan analisis mendalam
Sistem ini menganalisis setiap cell dan menghitung width optimal secara real-time
Menangani merged cells dengan aman untuk menghindari error
Args:
ws: Worksheet object
exclude_columns: List kolom yang tidak akan di-resize (misal ['D', 'I'] untuk Interface Description)
"""
if exclude_columns is None:
exclude_columns = []
try:
from openpyxl.worksheet.cell_range import CellRange
from openpyxl.cell import MergedCell
# Analyze all columns dynamically
for col_num in range(1, ws.max_column + 1):
try:
# Get column letter safely
col_letter = chr(ord('A') + col_num - 1) if col_num <= 26 else None
if col_letter is None:
continue
# Skip excluded columns
if col_letter in exclude_columns:
continue
max_width_needed = 8 # Absolute minimum
column_header = ""
# Analyze all cells in this column
for row_num in range(1, ws.max_row + 1):
try:
cell = ws.cell(row=row_num, column=col_num)
# Skip merged cells to avoid attribute errors
if isinstance(cell, MergedCell):
continue
if cell.value is None:
continue
# Get cell content
cell_content = str(cell.value).strip()
if not cell_content:
continue
# Store header for reference
if row_num == 5: # Header row
column_header = cell_content
# Calculate content width with different factors
if '\n' in cell_content:
# Multi-line content - use longest line
lines = cell_content.split('\n')
content_length = max(len(line.strip()) for line in lines) if lines else 0
else:
content_length = len(cell_content)
# Apply multipliers based on formatting and content type
try:
if cell.font and cell.font.bold:
# Bold text (headers, node names) need more space
calculated_width = int(content_length * 1.4) + 6
elif cell.font and cell.font.size and cell.font.size > 11:
# Larger fonts need more space
calculated_width = int(content_length * 1.3) + 5
else:
# Regular content with padding
calculated_width = content_length + 4
except:
# Fallback if font attributes are not accessible
calculated_width = content_length + 4
# Track maximum width needed
if calculated_width > max_width_needed:
max_width_needed = calculated_width
except Exception:
continue
# Apply intelligent column-specific adjustments
final_width = max_width_needed
# Specific adjustments based on column header or position
if 'No.' in column_header or col_num == 1:
# Number columns - keep compact
final_width = max(8, min(final_width, 12))
elif 'Node Name' in column_header or 'Host Name' in column_header:
# Node names need generous space for readability
final_width = max(final_width, 20)
elif 'Divre' in column_header:
# Division/region codes
final_width = max(final_width, 12)
elif 'Interface' in column_header and 'Description' not in column_header:
# Interface IDs
final_width = max(final_width, 15)
elif 'Module' in column_header or 'Type' in column_header:
# Module types and descriptions
final_width = max(final_width, 18)
elif 'Capacity' in column_header:
# Port capacity
final_width = max(final_width, 14)
elif 'Traffic' in column_header:
# Traffic data
final_width = max(final_width, 16)
elif 'Utilization' in column_header or '%' in column_header:
# Utilization percentages
final_width = max(final_width, 14)
elif 'Status' in column_header:
# Status information
final_width = max(final_width, 12)
elif 'SFP' in column_header:
# SFP status
final_width = max(final_width, 12)
elif 'Configuration' in column_header:
# Configuration status
final_width = max(final_width, 14)
elif 'Time' in column_header or 'Date' in column_header:
# Date/time columns
final_width = max(final_width, 18)
elif 'Alarm' in column_header:
# Alarm information
final_width = max(final_width, 16)
elif 'Severity' in column_header:
# Severity levels
final_width = max(final_width, 14)
elif 'Component' in column_header:
# Component types
final_width = max(final_width, 16)
elif 'Position' in column_header or 'Slot' in column_header:
# Slot/position info
final_width = max(final_width, 14)
elif 'Part Number' in column_header or 'Serial' in column_header:
# Hardware identifiers - need more space
final_width = max(final_width, 20)
elif 'Version' in column_header:
# Version information
final_width = max(final_width, 12)
elif 'Operational' in column_header:
# Operational status
final_width = max(final_width, 16)
elif 'Remarks' in column_header:
# Remarks/comments
final_width = max(final_width, 18)
elif 'Description' in column_header:
# Description columns - need dynamic width based on actual content
# Set minimum for readability but allow expansion based on content
final_width = max(final_width, 25)
# For description columns, allow wider maximum to accommodate long text
final_width = min(final_width, 80) # Higher max for descriptions
elif 'Model' in column_header and ('Description' in column_header or col_num == 8):
# Model/Description columns in hardware sheet
final_width = max(final_width, 30)
# Allow wider maximum for model descriptions
final_width = min(final_width, 80) # Higher max for model descriptions
# Apply reasonable maximum to prevent overly wide columns (lower max for non-description columns)
if 'Description' not in column_header and not ('Model' in column_header and col_num == 8):
final_width = min(final_width, 50)
else:
# Description columns already handled above with higher max
pass
# Set the calculated width
ws.column_dimensions[col_letter].width = final_width
except Exception as e:
# If there's an error processing this column, skip it
continue
except Exception as e:
# Fallback to safe defaults if anything goes wrong
try:
for col_num in range(1, min(ws.max_column + 1, 15)): # Limit to reasonable range
col_letter = chr(ord('A') + col_num - 1) if col_num <= 26 else None
if col_letter and col_letter not in exclude_columns:
if col_num == 1:
ws.column_dimensions[col_letter].width = 8
elif col_num == 2:
ws.column_dimensions[col_letter].width = 20
else:
ws.column_dimensions[col_letter].width = 15
except:
pass
def dynamic_adjust_row_heights(ws):
"""
Dynamic row height adjustment berdasarkan content yang ada di setiap baris
Menganalisis content multi-line dan menyesuaikan tinggi baris secara otomatis
Args:
ws: Worksheet object
"""
try:
# Skip header rows (1-4) and start from data rows
for row_num in range(6, ws.max_row + 1): # Data starts from row 6
max_lines_in_row = 1
# Check all cells in this row for multi-line content
for col_num in range(1, ws.max_column + 1):
try:
cell = ws.cell(row=row_num, column=col_num)
if cell.value is None:
continue
cell_content = str(cell.value).strip()
if not cell_content:
continue
# Count lines in this cell
if '\n' in cell_content:
lines_count = len(cell_content.split('\n'))
max_lines_in_row = max(max_lines_in_row, lines_count)
# Also check if content is very long (might wrap)
elif len(cell_content) > 50: # Long content might wrap
estimated_lines = (len(cell_content) // 50) + 1
max_lines_in_row = max(max_lines_in_row, min(estimated_lines, 3)) # Cap at 3 lines
except Exception:
continue
# Calculate row height based on content
if max_lines_in_row > 1:
# Base height + additional height per line
calculated_height = 20 + (max_lines_in_row - 1) * 15
# Cap maximum height to prevent excessive row heights
final_height = min(calculated_height, 80)
ws.row_dimensions[row_num].height = final_height
else:
# Standard row height for single-line content
ws.row_dimensions[row_num].height = 20
except Exception as e:
# Fallback - set standard row heights
try:
for row_num in range(6, ws.max_row + 1):
ws.row_dimensions[row_num].height = 20
except:
pass
def ensure_styles(wb):
thin = Side(border_style='thin', color='D3D3D3') # Light gray borders
medium = Side(border_style='medium', color='2E4A6B') # Professional blue borders
thick = Side(border_style='thick', color='2E4A6B')
# Professional Header Style - Premium Blue Theme
header = NamedStyle(name='header_style')
header.font = Font(bold=True, size=12, color='FFFFFF', name='Calibri')
header.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
header.fill = PatternFill('solid', fgColor='2E4A6B') # Professional blue
header.border = Border(top=medium, left=medium, bottom=medium, right=medium)
# Enhanced Data Style with professional look
data = NamedStyle(name='data_style')
data.font = Font(size=11, name='Calibri')
data.alignment = Alignment(horizontal='center', vertical='center')
data.border = Border(top=thin, left=thin, bottom=thin, right=thin)
data.fill = PatternFill('solid', fgColor='FFFFFF') # Pure white background
# Center Style with subtle styling
center = NamedStyle(name='center_style')
center.font = Font(size=11, name='Calibri')
center.alignment = Alignment(horizontal='center', vertical='center')
center.border = Border(top=thin, left=thin, bottom=thin, right=thin)
center.fill = PatternFill('solid', fgColor='F8F9FA') # Very light gray
# Left-aligned Style for descriptions with better readability
left = NamedStyle(name='left_style')
left.font = Font(size=11, name='Calibri')
left.alignment = Alignment(horizontal='left', vertical='center', wrap_text=True, indent=1)
left.border = Border(top=thin, left=thin, bottom=thin, right=thin)
left.fill = PatternFill('solid', fgColor='FFFFFF')
# Professional Title Style
title = NamedStyle(name='title_style')
title.font = Font(bold=True, size=18, color='2E4A6B', name='Calibri')
title.alignment = Alignment(horizontal='center', vertical='center')
# Subtitle Style for section headers
subtitle = NamedStyle(name='subtitle_style')
subtitle.font = Font(bold=True, size=14, color='2E4A6B', name='Calibri')
subtitle.alignment = Alignment(horizontal='left', vertical='center')
# Period/Date Style with consistent formatting
period = NamedStyle(name='period_style')
period.font = Font(bold=True, size=12, color='2E4A6B', name='Calibri')
period.alignment = Alignment(horizontal='left', vertical='center')
# Alternating row style for better readability
alt_row = NamedStyle(name='alt_row_style')
alt_row.font = Font(size=11, name='Calibri')
alt_row.alignment = Alignment(horizontal='center', vertical='center')
alt_row.border = Border(top=thin, left=thin, bottom=thin, right=thin)
alt_row.fill = PatternFill('solid', fgColor='F8F9FA') # Light alternating color
# Status styles for different conditions
success_style = NamedStyle(name='success_style')
success_style.font = Font(size=11, bold=True, color='1D8348', name='Calibri') # Green
success_style.alignment = Alignment(horizontal='center', vertical='center')
success_style.border = Border(top=thin, left=thin, bottom=thin, right=thin)
success_style.fill = PatternFill('solid', fgColor='D5F4E6') # Light green
warning_style = NamedStyle(name='warning_style')
warning_style.font = Font(size=11, bold=True, color='D68910', name='Calibri') # Orange
warning_style.alignment = Alignment(horizontal='center', vertical='center')
warning_style.border = Border(top=thin, left=thin, bottom=thin, right=thin)
warning_style.fill = PatternFill('solid', fgColor='FCF3CF') # Light yellow
error_style = NamedStyle(name='error_style')
error_style.font = Font(size=11, bold=True, color='C0392B', name='Calibri') # Red
error_style.alignment = Alignment(horizontal='center', vertical='center')
error_style.border = Border(top=thin, left=thin, bottom=thin, right=thin)
error_style.fill = PatternFill('solid', fgColor='FADBD8') # Light red
# Number style for metrics
number_style = NamedStyle(name='number_style')
number_style.font = Font(size=11, name='Calibri', bold=True)
number_style.alignment = Alignment(horizontal='center', vertical='center')
number_style.border = Border(top=thin, left=thin, bottom=thin, right=thin)
number_style.number_format = '#,##0.00'
for s in (header, data, center, left, title, subtitle, period, alt_row,
success_style, warning_style, error_style, number_style):
_add_named_style_safe(wb, s)
def _remove_table_if_exists(ws, displayName):
try:
existing = [t for t in list(getattr(ws, '_tables', [])) if getattr(t, 'displayName', '') == displayName]
for t in existing:
try:
ws._tables.remove(t)
except Exception:
pass
except Exception:
pass
def workbook_create(path):
wb = Workbook()
# Buat Dashboard Summary sebagai sheet pertama (active)
ws_dashboard = wb.active
ws_dashboard.title = DASHBOARD_SHEET
# Set attractive tab color for Dashboard - Blue
ws_dashboard.sheet_properties.tabColor = "1F4E79"
ensure_styles(wb)
worksheet_dashboard_summary(ws_dashboard)
# Buat Main Sheet (Utilisasi FPC)
ws = wb.create_sheet(MAIN_SHEET)
# Set attractive tab color for Main Sheet - Green
ws.sheet_properties.tabColor = "27AE60"
ensure_styles(wb)
# Professional corporate header
ws.merge_cells('A1:J1')
ws['A1'] = 'NETWORK INFRASTRUCTURE MONITORING SYSTEM - FPC UTILIZATION'
ws['A1'].font = Font(bold=True, size=16, color='FFFFFF', name='Calibri')
ws['A1'].alignment = Alignment(horizontal='center', vertical='center')
ws['A1'].fill = PatternFill('solid', fgColor='2E4A6B') # Professional dark blue
ws.row_dimensions[1].height = 40
# Subtitle with report name
ws.merge_cells('A2:J2')
ws['A2'] = 'FPC Utilization Analysis Report'
ws['A2'].font = Font(bold=True, size=14, color='2E4A6B', name='Calibri')
ws['A2'].alignment = Alignment(horizontal='center', vertical='center')
ws['A2'].fill = PatternFill('solid', fgColor='F8F9FA')
ws.row_dimensions[2].height = 30
# Report period with full merge to prevent cut-off and timezone
ws.merge_cells('A3:J3')
timezone_str = get_indonesia_timezone()
ws['A3'] = f'Report Period: {capture_time_global.strftime("%d %B %Y, %H:%M")} {timezone_str}'
ws['A3'].font = Font(bold=True, size=12, color='2C3E50', name='Calibri')
ws['A3'].alignment = Alignment(horizontal='center', vertical='center')
ws['A3'].fill = PatternFill('solid', fgColor='ECF0F1')
ws.row_dimensions[3].height = 25
# Professional spacing
ws.row_dimensions[4].height = 8
# Professional headers - width akan di-auto-resize berdasarkan content
headers_main = [
('A5', 'No.'), ('B5', 'Node Name'), ('C5', 'Divre'),
('D5', 'Interface Description'), ('E5', 'Interface ID'), ('F5', 'Module Type'),
('G5', 'Port Capacity'), ('H5', 'Current Traffic'), ('I5', 'Utilization (%)'),
('J5', 'Status'),
]
for cell, text in headers_main:
ws[cell] = text
ws[cell].font = Font(bold=True, size=11, color='FFFFFF', name='Calibri')
ws[cell].alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
ws[cell].fill = PatternFill('solid', fgColor='2E4A6B') # Professional blue
ws[cell].border = Border(
top=Side(border_style='medium', color='2E4A6B'),
left=Side(border_style='thin', color='FFFFFF'),
bottom=Side(border_style='medium', color='2E4A6B'),
right=Side(border_style='thin', color='FFFFFF')
)
# Professional header styling
ws.row_dimensions[5].height = 35
try:
ws.freeze_panes = 'A6' # Freeze above data rows
except Exception:
pass
if UTIL_SHEET not in wb.sheetnames:
ws2 = wb.create_sheet(UTIL_SHEET)
# Set attractive tab color for Util Sheet - Orange
ws2.sheet_properties.tabColor = "E67E22"
worksheet_utilisasi_port(ws2)
if ALARM_SHEET not in wb.sheetnames:
ws3 = wb.create_sheet(ALARM_SHEET)
# Set attractive tab color for Alarm Sheet - Red
ws3.sheet_properties.tabColor = "E74C3C"
worksheet_alarm_status(ws3)
if HARDWARE_SHEET not in wb.sheetnames:
ws4 = wb.create_sheet(HARDWARE_SHEET)
# Set attractive tab color for Hardware Sheet - Purple
ws4.sheet_properties.tabColor = "8E44AD"
worksheet_hardware_inventory(ws4)
# Set Dashboard Summary sebagai sheet aktif saat file dibuka
wb.active = wb[DASHBOARD_SHEET]
wb.save(path)
wb.close()
def worksheet_utilisasi_port(ws):
# Professional corporate header
ws.merge_cells('A1:M1')
ws['A1'] = 'NETWORK INFRASTRUCTURE MONITORING SYSTEM - PORT UTILIZATION'
ws['A1'].font = Font(bold=True, size=16, color='FFFFFF', name='Calibri')
ws['A1'].alignment = Alignment(horizontal='center', vertical='center')
ws['A1'].fill = PatternFill('solid', fgColor='2E4A6B') # Professional blue
ws.row_dimensions[1].height = 40
# Subtitle with report name
ws.merge_cells('A2:M2')
ws['A2'] = 'Detailed Port Utilization Monitoring Report'
ws['A2'].font = Font(bold=True, size=14, color='2E4A6B', name='Calibri')
ws['A2'].alignment = Alignment(horizontal='center', vertical='center')
ws['A2'].fill = PatternFill('solid', fgColor='F8F9FA')
ws.row_dimensions[2].height = 30
# Report period with full merge to prevent cut-off and timezone
ws.merge_cells('A3:M3')
timezone_str = get_indonesia_timezone()
ws['A3'] = f'Report Period: {capture_time_global.strftime("%d %B %Y, %H:%M")} {timezone_str}'
ws['A3'].font = Font(bold=True, size=12, color='2C3E50', name='Calibri')
ws['A3'].alignment = Alignment(horizontal='center', vertical='center')
ws['A3'].fill = PatternFill('solid', fgColor='ECF0F1')
ws.row_dimensions[3].height = 25
# Professional spacing
ws.row_dimensions[4].height = 8
# Professional headers - width akan di-auto-resize berdasarkan content
headers_util = [
('A5', 'No.'), ('B5', 'Node Name'), ('C5', 'Divre'), ('D5', 'Interface ID'),
('E5', 'Module Description'), ('F5', 'Port Capacity'), ('G5', 'Last Flapped'),
('H5', 'SFP Status'), ('I5', 'Configuration'), ('J5', 'Interface Description'), ('K5', 'Status'), ('L5', 'Flap Alert'), ('M5', 'Alert Up/Down'),
]
for cell, text in headers_util:
ws[cell] = text
ws[cell].font = Font(bold=True, size=11, color='FFFFFF', name='Calibri')
ws[cell].alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
ws[cell].fill = PatternFill('solid', fgColor='2E4A6B') # Professional blue
ws[cell].border = Border(
top=Side(border_style='medium', color='2E4A6B'),
left=Side(border_style='thin', color='FFFFFF'),
bottom=Side(border_style='medium', color='2E4A6B'),
right=Side(border_style='thin', color='FFFFFF')
)
# Professional header styling
ws.row_dimensions[5].height = 35
try:
ws.freeze_panes = 'A6' # Freeze above data rows
except Exception:
pass
def worksheet_alarm_status(ws):
# Professional corporate header
ws.merge_cells('A1:H1')
ws['A1'] = 'NETWORK INFRASTRUCTURE MONITORING SYSTEM - ALARM STATUS'
ws['A1'].font = Font(bold=True, size=16, color='FFFFFF', name='Calibri')
ws['A1'].alignment = Alignment(horizontal='center', vertical='center')
ws['A1'].fill = PatternFill('solid', fgColor='2E4A6B') # Professional blue
ws.row_dimensions[1].height = 40
# Subtitle with report name
ws.merge_cells('A2:H2')
ws['A2'] = 'Network Alarm Status Monitoring Report'
ws['A2'].font = Font(bold=True, size=14, color='2E4A6B', name='Calibri')
ws['A2'].alignment = Alignment(horizontal='center', vertical='center')
ws['A2'].fill = PatternFill('solid', fgColor='F8F9FA')
ws.row_dimensions[2].height = 30
# Report period with full merge to prevent cut-off and timezone
ws.merge_cells('A3:H3')
timezone_str = get_indonesia_timezone()
ws['A3'] = f'Report Period: {capture_time_global.strftime("%d %B %Y, %H:%M")} {timezone_str}'
ws['A3'].font = Font(bold=True, size=12, color='2C3E50', name='Calibri')
ws['A3'].alignment = Alignment(horizontal='center', vertical='center')
ws['A3'].fill = PatternFill('solid', fgColor='ECF0F1')
ws.row_dimensions[3].height = 25
# Professional spacing
ws.row_dimensions[4].height = 8
# Professional headers - width akan di-auto-resize berdasarkan content
headers_alarm = [
('A5', 'No.'), ('B5', 'Node Name'), ('C5', 'Divre'),
('D5', 'Alarm Time'), ('E5', 'Alarm Type'),
('F5', 'Alarm Description'), ('G5', 'Severity Level'), ('H5', 'Current Status'),
]
for cell, text in headers_alarm:
ws[cell] = text
ws[cell].font = Font(bold=True, size=11, color='FFFFFF', name='Calibri')
ws[cell].alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
ws[cell].fill = PatternFill('solid', fgColor='2E4A6B') # Professional blue
ws[cell].border = Border(
top=Side(border_style='medium', color='2E4A6B'),
left=Side(border_style='thin', color='FFFFFF'),
bottom=Side(border_style='medium', color='2E4A6B'),
right=Side(border_style='thin', color='FFFFFF')
)
# Professional header styling
ws.row_dimensions[5].height = 35
try:
ws.freeze_panes = 'A6' # Freeze header row
except Exception:
pass
def worksheet_hardware_inventory(ws):
# Professional corporate header
ws.merge_cells('A1:K1')
ws['A1'] = 'NETWORK INFRASTRUCTURE MONITORING SYSTEM - HARDWARE INVENTORY'
ws['A1'].font = Font(bold=True, size=16, color='FFFFFF', name='Calibri')
ws['A1'].fill = PatternFill('solid', fgColor='2E4A6B') # Professional blue
ws['A1'].alignment = Alignment(horizontal='center', vertical='center')
ws.row_dimensions[1].height = 40
# Report title
ws.merge_cells('A2:K2')
ws['A2'] = 'Hardware Inventory Monitoring Report'
ws['A2'].font = Font(bold=True, size=14, color='2E4A6B', name='Calibri')
ws['A2'].fill = PatternFill('solid', fgColor='F8F9FA')
ws['A2'].alignment = Alignment(horizontal='center', vertical='center')
ws.row_dimensions[2].height = 30
# Period information - merged to prevent truncation and timezone
ws.merge_cells('A3:K3')
timezone_str = get_indonesia_timezone()
ws['A3'] = f'Report Period: {capture_time_global.strftime("%d %B %Y, %H:%M")} {timezone_str}'
ws['A3'].font = Font(bold=True, size=12, color='2C3E50', name='Calibri')
ws['A3'].fill = PatternFill('solid', fgColor='ECF0F1')
ws['A3'].alignment = Alignment(horizontal='center', vertical='center')
ws.row_dimensions[3].height = 25
# Professional spacing
ws.row_dimensions[4].height = 8
# Professional headers - width akan di-auto-resize berdasarkan content
headers_hardware = [
('A5', 'No.'), ('B5', 'Node Name'), ('C5', 'Divre'),
('D5', 'Component Type'), ('E5', 'Slot/Position'),
('F5', 'Part Number'), ('G5', 'Serial Number'),
('H5', 'Model/Description'), ('I5', 'Version'),
('J5', 'Operational Status'), ('K5', 'Remarks'),
]
for cell, text in headers_hardware:
ws[cell] = text
ws[cell].font = Font(bold=True, color='FFFFFF', name='Calibri')
ws[cell].fill = PatternFill('solid', fgColor='2E4A6B') # Professional blue
ws[cell].alignment = Alignment(horizontal='center', vertical='center')
ws[cell].border = Border(
top=Side(border_style='medium', color='2E4A6B'),
left=Side(border_style='thin', color='FFFFFF'),
bottom=Side(border_style='medium', color='2E4A6B'),
right=Side(border_style='thin', color='FFFFFF')
)
# Professional header row styling
ws.row_dimensions[5].height = 30
try:
ws.freeze_panes = 'A6' # Freeze header row
except Exception:
pass
def worksheet_system_performance(ws, system_data=None):
"""
System Performance Monitoring Sheet
"""
if system_data is None:
system_data = {}
# Professional column configuration
ws.column_dimensions['A'].width = 5 # No