PyELSSA 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyELSSA/__init__.py +28 -0
- PyELSSA/configuration.py +325 -0
- PyELSSA/data/TestFile.txt +0 -0
- PyELSSA/data/TestFile1.csv +0 -0
- PyELSSA/data/TestFile2.csv +0 -0
- PyELSSA/gui/gui_base_frame.py +221 -0
- PyELSSA/gui/gui_main_page.py +142 -0
- PyELSSA/gui/gui_start.py +424 -0
- PyELSSA/gui/gui_sub_frame.py +414 -0
- PyELSSA/licensing.py +224 -0
- PyELSSA/master.py +87 -0
- PyELSSA/release_update.py +59 -0
- PyELSSA/shared_contents.py +491 -0
- PyELSSA/shared_msazure_api.py +469 -0
- pyelssa-0.1.0.dist-info/METADATA +44 -0
- pyelssa-0.1.0.dist-info/RECORD +18 -0
- pyelssa-0.1.0.dist-info/WHEEL +4 -0
- pyelssa-0.1.0.dist-info/licenses/LICENSE +88 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#=================================================================
|
|
3
|
+
# Created by: Jieming Ye
|
|
4
|
+
# Created on: Feb 2024
|
|
5
|
+
# Last Modified: Feb 2024
|
|
6
|
+
#=================================================================
|
|
7
|
+
# Copyright (c) 2024 [Jieming Ye]
|
|
8
|
+
#
|
|
9
|
+
# This Python source code is licensed under the
|
|
10
|
+
# Open Source Non-Commercial License (OSNCL) v1.0
|
|
11
|
+
# See LICENSE for details.
|
|
12
|
+
#=================================================================
|
|
13
|
+
"""
|
|
14
|
+
This file defines action pages for the PyELSSA GUI.
|
|
15
|
+
"""
|
|
16
|
+
#=================================================================
|
|
17
|
+
# VERSION CONTROL
|
|
18
|
+
# V1.0 (Jieming Ye) - Initial Version
|
|
19
|
+
#=================================================================
|
|
20
|
+
|
|
21
|
+
import tkinter as tk
|
|
22
|
+
from tkinter import ttk
|
|
23
|
+
|
|
24
|
+
from PyELSSA.shared_contents import SharedVariables
|
|
25
|
+
from PyELSSA.gui.gui_base_frame import BasePage
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ActionPage(BasePage):
|
|
29
|
+
config = None
|
|
30
|
+
|
|
31
|
+
def __init__(self, parent, controller):
|
|
32
|
+
super().__init__(parent, controller)
|
|
33
|
+
self.field_vars = {}
|
|
34
|
+
self.selection_var = tk.StringVar(value='0')
|
|
35
|
+
self.build_page()
|
|
36
|
+
|
|
37
|
+
def build_page(self):
|
|
38
|
+
if not self.config:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
title = self.create_label(self.headframe, self.config['title'], style='PageTitle.TLabel')
|
|
42
|
+
title.pack(anchor='w', pady=(0, 8))
|
|
43
|
+
|
|
44
|
+
description = self.create_label(self.headframe, self.config.get('description', ''), style='PageText.TLabel', wraplength=860, justify='left')
|
|
45
|
+
description.pack(anchor='w', pady=(0, 8))
|
|
46
|
+
|
|
47
|
+
for note in self.config.get('notes', []):
|
|
48
|
+
note_label = self.create_label(self.headframe, note, style='Info.TLabel', wraplength=860, justify='left')
|
|
49
|
+
note_label.pack(anchor='w', pady=(0, 6))
|
|
50
|
+
|
|
51
|
+
if self.config.get('configs'):
|
|
52
|
+
config_section = self.create_section(self.contentframe,title='Configuration')
|
|
53
|
+
columns = 4
|
|
54
|
+
for idx, cfg in enumerate(self.config['configs']):
|
|
55
|
+
row = idx // columns
|
|
56
|
+
col = idx % columns
|
|
57
|
+
var = tk.BooleanVar(value=False)
|
|
58
|
+
self.config_vars[cfg['name']] = var
|
|
59
|
+
chk = ttk.Checkbutton(config_section,text=cfg['label'],variable=var)
|
|
60
|
+
chk.grid(row=row,column=col,sticky='w',padx=10,pady=4)
|
|
61
|
+
|
|
62
|
+
for col in range(columns):
|
|
63
|
+
config_section.columnconfigure(col, weight=1)
|
|
64
|
+
|
|
65
|
+
if self.config.get('options'):
|
|
66
|
+
options_section = self.create_section(self.contentframe, title='Options')
|
|
67
|
+
for row, option in enumerate(self.config['options']):
|
|
68
|
+
radio = self.create_radio(options_section, option['label'], self.selection_var, option['value'])
|
|
69
|
+
radio.grid(row=row, column=0, sticky='w', padx=(0, 12), pady=4, columnspan=2)
|
|
70
|
+
options_section.columnconfigure(0, weight=1)
|
|
71
|
+
|
|
72
|
+
fields_section = self.create_section(self.contentframe, title='Inputs')
|
|
73
|
+
for row, field in enumerate(self.config.get('fields', [])):
|
|
74
|
+
label = self.create_label(fields_section, field['label'], style='OptionText.TLabel')
|
|
75
|
+
label.grid(row=row, column=0, sticky='w', padx=(0, 12), pady=6)
|
|
76
|
+
|
|
77
|
+
var = tk.StringVar()
|
|
78
|
+
self.field_vars[field['name']] = var
|
|
79
|
+
if field['type'] == 'file':
|
|
80
|
+
entry = self.create_entry(fields_section, var, width=40)
|
|
81
|
+
entry.grid(row=row, column=1, sticky='w', padx=(0, 12), pady=6)
|
|
82
|
+
select_button = self.create_button(fields_section, field.get('button', 'Select'), lambda v=var: self.auto_file_select(v), style='Secondary.TButton')
|
|
83
|
+
select_button.grid(row=row, column=2, sticky='w', pady=6)
|
|
84
|
+
else:
|
|
85
|
+
entry = self.create_entry(fields_section, var, width=30)
|
|
86
|
+
entry.grid(row=row, column=1, sticky='w', padx=(0, 12), pady=6)
|
|
87
|
+
|
|
88
|
+
run_section = self.create_section(self.contentframe, title='Action')
|
|
89
|
+
run_button = self.create_button(run_section, 'RUN', self._run_action)
|
|
90
|
+
run_button.pack(side='left', pady=8)
|
|
91
|
+
|
|
92
|
+
footer = self.create_frame(padding=(0, 0))
|
|
93
|
+
self.create_button(footer, 'Back to Home', lambda: self.controller.show_frame('StartPage')).pack(side='left', padx=6, pady=8)
|
|
94
|
+
self.create_button(footer, 'Back to Processing', lambda: self.controller.show_frame(self.config.get('back_target', 'StartPage')), style='Secondary.TButton').pack(side='left', padx=6, pady=8)
|
|
95
|
+
|
|
96
|
+
def _run_action(self):
|
|
97
|
+
inputs = {}
|
|
98
|
+
if self.config.get('configs'):
|
|
99
|
+
inputs['configs'] = {
|
|
100
|
+
name: var.get()
|
|
101
|
+
for name, var in self.config_vars.items()
|
|
102
|
+
}
|
|
103
|
+
if self.config.get('options'):
|
|
104
|
+
inputs['option_select'] = self.selection_var
|
|
105
|
+
for field in self.config.get('fields', []):
|
|
106
|
+
inputs[field['name']] = self.field_vars[field['name']]
|
|
107
|
+
self.run_new_thread_or_process(self.config['script'], **inputs)
|
|
108
|
+
|
|
109
|
+
# data passing structure example
|
|
110
|
+
# {
|
|
111
|
+
# 'configs': {
|
|
112
|
+
# 'setup': True,
|
|
113
|
+
# 'config': False,
|
|
114
|
+
# 'vision': True,
|
|
115
|
+
# 'validate': False
|
|
116
|
+
# },
|
|
117
|
+
# 'option_select': 'rail',
|
|
118
|
+
# 'input_file': 'model.xlsx'
|
|
119
|
+
# }
|
|
120
|
+
|
|
121
|
+
ACTION_PAGE_CONFIGS = {
|
|
122
|
+
'P01': {
|
|
123
|
+
'title': 'Page 1: List OSLO Train Data',
|
|
124
|
+
'description': 'Generate step outputs for OSLO trains. Copy and paste results into Excel for further analysis.',
|
|
125
|
+
'notes': ['NOTE: Option 3 and Option 4 require a Branch List text file.'],
|
|
126
|
+
'options': [
|
|
127
|
+
{'label': 'Option 1: List all trains step output from the simulation', 'value': '1'},
|
|
128
|
+
{'label': 'Option 2: List all trains step output From Start to End', 'value': '2'},
|
|
129
|
+
{'label': 'Option 3: List selected branches for the whole simulation window', 'value': '3'},
|
|
130
|
+
{'label': 'Option 4: List selected branches From Start to End', 'value': '4'},
|
|
131
|
+
],
|
|
132
|
+
'fields': [
|
|
133
|
+
{'name': 'time_start', 'label': 'Extraction From (Format: DHHMMSS)', 'type': 'entry'},
|
|
134
|
+
{'name': 'time_end', 'label': 'Extraction To (Format: DHHMMSS)', 'type': 'entry'},
|
|
135
|
+
{'name': 'text_input', 'label': 'Customised Branch File Name', 'type': 'file', 'button': 'Select'},
|
|
136
|
+
],
|
|
137
|
+
'script': 'list_file_processing.py',
|
|
138
|
+
'back_target': 'PageFour',
|
|
139
|
+
},
|
|
140
|
+
'P02': {
|
|
141
|
+
'title': 'Page 2: Low Voltage Analysis',
|
|
142
|
+
'description': 'Produce voltage-based reports and summaries for trains and branches.',
|
|
143
|
+
'notes': ['NOTE: Option 3 and Option 4 require a Branch List text file.'],
|
|
144
|
+
'options': [
|
|
145
|
+
{'label': 'Option 1: Processing whole simulation', 'value': '1'},
|
|
146
|
+
{'label': 'Option 2: Processing customised time window', 'value': '2'},
|
|
147
|
+
{'label': 'Option 3: Processing customised branches', 'value': '3'},
|
|
148
|
+
{'label': 'Option 4: Processing customised branches during customised time window', 'value': '4'},
|
|
149
|
+
],
|
|
150
|
+
'fields': [
|
|
151
|
+
{'name': 'low_v', 'label': 'Low Voltage Threshold (max 5 digit)', 'type': 'entry'},
|
|
152
|
+
{'name': 'time_start', 'label': 'Output From (Format: DHHMMSS)', 'type': 'entry'},
|
|
153
|
+
{'name': 'time_end', 'label': 'Output To (Format: DHHMMSS)', 'type': 'entry'},
|
|
154
|
+
{'name': 'text_input', 'label': 'Customised Branch File Name', 'type': 'file', 'button': 'Select'},
|
|
155
|
+
],
|
|
156
|
+
'script': 'list_file_processing.py',
|
|
157
|
+
'back_target': 'PageFour',
|
|
158
|
+
},
|
|
159
|
+
'P03': {
|
|
160
|
+
'title': 'Page 3: AC - Supply Points Load Analysis',
|
|
161
|
+
'description': 'Requires an Excel spreadsheet with predefined information for average power analysis.',
|
|
162
|
+
'notes': ['NOTE: Excel spreadsheet in .xlsx format is required before clicking RUN.', 'NOTE: Import "Excel - For Average Power" if needed.'],
|
|
163
|
+
'options': [
|
|
164
|
+
{'label': 'Option 1: Full Auto Process (Require .oof + .lst.txt files)', 'value': '1'},
|
|
165
|
+
{'label': 'Option 2: Auto process with user defined node configuration (Only require .oof file)', 'value': '2'},
|
|
166
|
+
{'label': 'Option 3: Update spreadsheet only (Require .d4 + .mxn)', 'value': '3'},
|
|
167
|
+
{'label': 'Option 4: Extract d4 and mxn only (Only require .oof file)', 'value': '4'},
|
|
168
|
+
],
|
|
169
|
+
'fields': [
|
|
170
|
+
{'name': 'text_input', 'label': 'Excel Name', 'type': 'file', 'button': 'Select'},
|
|
171
|
+
{'name': 'time_start', 'label': 'Extraction From (Format: DHHMMSS)', 'type': 'entry'},
|
|
172
|
+
{'name': 'time_end', 'label': 'Extraction To (Format: DHHMMSS)', 'type': 'entry'},
|
|
173
|
+
],
|
|
174
|
+
'script': 'average_load.py',
|
|
175
|
+
'back_target': 'PageFive',
|
|
176
|
+
},
|
|
177
|
+
'P04': {
|
|
178
|
+
'title': 'Page 4: Umeanuseful Analysis',
|
|
179
|
+
'description': 'Produces Umean useful dashboards as per BS EN 50388. Choose the right train selection for the assessment.',
|
|
180
|
+
'notes': ['NOTE: Import UmeanSettingTemplate.csv for Option 2.', 'NOTE: Require *.lst.txt file within the current folder.'],
|
|
181
|
+
'options': [
|
|
182
|
+
{'label': 'Option 1: Auto Configuration (Hourly Window + Supply Point Zone)', 'value': '1'},
|
|
183
|
+
{'label': 'Option 2: Auto + Customised Settings', 'value': '2'},
|
|
184
|
+
],
|
|
185
|
+
'fields': [
|
|
186
|
+
{'name': 'text_input', 'label': 'Umeanuseful Settings (.csv)', 'type': 'file', 'button': 'Select'},
|
|
187
|
+
{'name': 'time_start', 'label': 'Peak time From (Format: DHHMMSS)', 'type': 'entry'},
|
|
188
|
+
{'name': 'time_end', 'label': 'Peak time To (Format: DHHMMSS)', 'type': 'entry'},
|
|
189
|
+
],
|
|
190
|
+
'script': 'umeanuseful.py',
|
|
191
|
+
'back_target': 'PageFive',
|
|
192
|
+
},
|
|
193
|
+
'P05': {
|
|
194
|
+
'title': 'Page 5: AC - Incoming Feeder Protection',
|
|
195
|
+
'description': 'Preliminary assessment of incoming feeder protection based on relay type.',
|
|
196
|
+
'notes': ['NOTE: Excel spreadsheet in .xlsx format is required before clicking RUN.', 'NOTE: Import "Excel - For IF Protection" if needed.'],
|
|
197
|
+
'options': [
|
|
198
|
+
{'label': 'Option 1: Full Auto Process (Require .oof files)', 'value': '1'},
|
|
199
|
+
{'label': 'Option 2: Update Spreadsheet only (Require .d4 files)', 'value': '2'},
|
|
200
|
+
],
|
|
201
|
+
'fields': [
|
|
202
|
+
{'name': 'text_input', 'label': 'Excel Name', 'type': 'file', 'button': 'Select'},
|
|
203
|
+
{'name': 'time_start', 'label': 'Extraction From (Format: DHHMMSS)', 'type': 'entry'},
|
|
204
|
+
{'name': 'time_end', 'label': 'Extraction To (Format: DHHMMSS)', 'type': 'entry'},
|
|
205
|
+
],
|
|
206
|
+
'script': 'protection_if.py',
|
|
207
|
+
'back_target': 'PageFive',
|
|
208
|
+
},
|
|
209
|
+
'P06': {
|
|
210
|
+
'title': 'Page 6: AC - New Supply Point Connection Assessment',
|
|
211
|
+
'description': 'Assessment for new supply points requiring a defined Excel template.',
|
|
212
|
+
'notes': ['NOTE: Excel spreadsheet in .xlsx format is required before clicking RUN.', 'NOTE: Import "Excel - For New SP Connect" if needed.'],
|
|
213
|
+
'options': [
|
|
214
|
+
{'label': 'Option 1: Full Auto Process (Require .oof files)', 'value': '1'},
|
|
215
|
+
{'label': 'Option 2: Update Spreadsheet only (Require extracted .d4 files)', 'value': '2'},
|
|
216
|
+
],
|
|
217
|
+
'fields': [
|
|
218
|
+
{'name': 'text_input', 'label': 'Excel Name', 'type': 'file', 'button': 'Select'},
|
|
219
|
+
{'name': 'time_start', 'label': 'Extraction From (Format: DHHMMSS)', 'type': 'entry'},
|
|
220
|
+
{'name': 'time_end', 'label': 'Extraction To (Format: DHHMMSS)', 'type': 'entry'},
|
|
221
|
+
],
|
|
222
|
+
'script': 'grid_connection.py',
|
|
223
|
+
'back_target': 'PageFive',
|
|
224
|
+
},
|
|
225
|
+
'P07': {
|
|
226
|
+
'title': 'Page 7: AC - OLE Current Rating Assessment',
|
|
227
|
+
'description': 'Assessment of OLE current ratings using the provided Excel spreadsheet.',
|
|
228
|
+
'notes': ['NOTE: Excel spreadsheet in .xlsx format is required before clicking RUN.', 'NOTE: Import "Excel - For OLE Rating" if needed.'],
|
|
229
|
+
'options': [
|
|
230
|
+
{'label': 'Option 1: Full Auto Process (Require .oof files)', 'value': '1'},
|
|
231
|
+
{'label': 'Option 2: Update Spreadsheet only (Require .d4 files)', 'value': '2'},
|
|
232
|
+
],
|
|
233
|
+
'fields': [
|
|
234
|
+
{'name': 'text_input', 'label': 'Excel Name', 'type': 'file', 'button': 'Select'},
|
|
235
|
+
{'name': 'time_start', 'label': 'Extraction From (Format: DHHMMSS)', 'type': 'entry'},
|
|
236
|
+
{'name': 'time_end', 'label': 'Extraction To (Format: DHHMMSS)', 'type': 'entry'},
|
|
237
|
+
],
|
|
238
|
+
'script': 'ole_processing.py',
|
|
239
|
+
'back_target': 'PageFive',
|
|
240
|
+
},
|
|
241
|
+
'P08': {
|
|
242
|
+
'title': 'Page 8: AC - Static Frequency Converter Assessment',
|
|
243
|
+
'description': 'SFC assessment using Excel input and optional file extraction.',
|
|
244
|
+
'notes': ['NOTE: Excel spreadsheet in .xlsx format is required before clicking RUN.', 'NOTE: Import "Excel - For SFC" if needed.'],
|
|
245
|
+
'options': [
|
|
246
|
+
{'label': 'Option 1: Full Auto Process', 'value': '1'},
|
|
247
|
+
{'label': 'Option 2: Auto process with node configuration', 'value': '2'},
|
|
248
|
+
{'label': 'Option 3: Update spreadsheet only', 'value': '3'},
|
|
249
|
+
{'label': 'Option 4: Extract d4/mxn only', 'value': '4'},
|
|
250
|
+
],
|
|
251
|
+
'fields': [
|
|
252
|
+
{'name': 'text_input', 'label': 'Excel Name', 'type': 'file', 'button': 'Select'},
|
|
253
|
+
{'name': 'time_start', 'label': 'Extraction From (Format: DHHMMSS)', 'type': 'entry'},
|
|
254
|
+
{'name': 'time_end', 'label': 'Extraction To (Format: DHHMMSS)', 'type': 'entry'},
|
|
255
|
+
],
|
|
256
|
+
'script': 'sfc_assess.py',
|
|
257
|
+
'back_target': 'PageFive',
|
|
258
|
+
},
|
|
259
|
+
'P09': {
|
|
260
|
+
'title': 'Page 9: Substation TRU Assessment Data Prepare',
|
|
261
|
+
'description': 'Create substation assessment files and load summaries for TRU and grid calculations.',
|
|
262
|
+
'notes': ['NOTE: "FeederList.txt" is required before clicking RUN.'],
|
|
263
|
+
'options': [
|
|
264
|
+
{'label': 'Option 1: Substation RMS and average power summary', 'value': '1'},
|
|
265
|
+
{'label': 'Option 2: Grid Calculation (requires GridAllocation.csv)', 'value': '2'},
|
|
266
|
+
],
|
|
267
|
+
'fields': [
|
|
268
|
+
{'name': 'time_start', 'label': 'Extraction From (Format: DHHMMSS)', 'type': 'entry'},
|
|
269
|
+
{'name': 'time_end', 'label': 'Extraction To (Format: DHHMMSS)', 'type': 'entry'},
|
|
270
|
+
],
|
|
271
|
+
'script': 'batch_processing.py',
|
|
272
|
+
'back_target': 'PageSix',
|
|
273
|
+
},
|
|
274
|
+
'P10': {
|
|
275
|
+
'title': 'Page 10: Substation Protection / Track CB & ETE Assessment',
|
|
276
|
+
'description': 'Generate branch assessment summaries and rolling RMS calculations.',
|
|
277
|
+
'notes': ['NOTE: Time window is compulsory.', 'NOTE: Option 4-6 require BranchNodeList.txt.'],
|
|
278
|
+
'options': [
|
|
279
|
+
{'label': 'Option 1: All branches step output summary', 'value': '1'},
|
|
280
|
+
{'label': 'Option 2: All branches rolling RMS current calculation', 'value': '2'},
|
|
281
|
+
{'label': 'Option 3: All branches maximum rolling RMS current summary', 'value': '3'},
|
|
282
|
+
{'label': 'Option 4: Customised branches step output summary', 'value': '4'},
|
|
283
|
+
{'label': 'Option 5: Customised branches rolling RMS calculation', 'value': '5'},
|
|
284
|
+
{'label': 'Option 6: Customised branches maximum rolling RMS summary', 'value': '6'},
|
|
285
|
+
],
|
|
286
|
+
'fields': [
|
|
287
|
+
{'name': 'time_start', 'label': 'Extraction From (Format: DHHMMSS)', 'type': 'entry'},
|
|
288
|
+
{'name': 'time_end', 'label': 'Extraction To (Format: DHHMMSS)', 'type': 'entry'},
|
|
289
|
+
{'name': 'time_step', 'label': 'Time Seconds (0-86400)', 'type': 'entry'},
|
|
290
|
+
],
|
|
291
|
+
'script': 'batch_processing.py',
|
|
292
|
+
'back_target': 'PageSix',
|
|
293
|
+
},
|
|
294
|
+
'P11': {
|
|
295
|
+
'title': 'Page 11: DC Substation Assessment Summary',
|
|
296
|
+
'description': 'Create summary reports for DC ratings and train voltage assessments.',
|
|
297
|
+
'notes': ['NOTE: Excel spreadsheet in .xlsx format is required before clicking RUN.'],
|
|
298
|
+
'options': [
|
|
299
|
+
{'label': 'Option 1: TRU Summary', 'value': '1'},
|
|
300
|
+
{'label': 'Option 2: Main DC Circuit Breaker Summary', 'value': '2'},
|
|
301
|
+
{'label': 'Option 3: DC Busbar Summary', 'value': '3'},
|
|
302
|
+
{'label': 'Option 4: Negative ETE Summary', 'value': '4'},
|
|
303
|
+
{'label': 'Option 5: Impedance Bond Summary', 'value': '5'},
|
|
304
|
+
{'label': 'Option 6: Track Circuit Breaker Summary', 'value': '6'},
|
|
305
|
+
{'label': 'Option 7: Positive ETE Summary', 'value': '7'},
|
|
306
|
+
{'label': 'Option 8: Train Min Voltage Summary', 'value': '8'},
|
|
307
|
+
],
|
|
308
|
+
'fields': [
|
|
309
|
+
{'name': 'text_input', 'label': 'Excel Name', 'type': 'file', 'button': 'Select'},
|
|
310
|
+
],
|
|
311
|
+
'script': 'dc_summary.py',
|
|
312
|
+
'back_target': 'PageSix',
|
|
313
|
+
},
|
|
314
|
+
'P12': {
|
|
315
|
+
'title': 'Page 12: DC Single End Feeding 1st Stage Processing',
|
|
316
|
+
'description': 'Estimate single end feeding loads for preliminary DC assessment.',
|
|
317
|
+
'notes': ['NOTE: Time window is compulsory.', 'NOTE: Excel spreadsheet in .xlsx format is required before clicking RUN.'],
|
|
318
|
+
'options': [
|
|
319
|
+
{'label': 'Option 1: TCB Assessment (15min RMS)', 'value': '1'},
|
|
320
|
+
{'label': 'Option 2: ETE Assessment (30min RMS)', 'value': '2'},
|
|
321
|
+
{'label': 'Option 3: TCB Assessment with CSV step outputs', 'value': '3'},
|
|
322
|
+
{'label': 'Option 4: ETE Assessment with CSV step outputs', 'value': '4'},
|
|
323
|
+
],
|
|
324
|
+
'fields': [
|
|
325
|
+
{'name': 'time_start', 'label': 'Extraction From (Format: DHHMMSS)', 'type': 'entry'},
|
|
326
|
+
{'name': 'time_end', 'label': 'Extraction To (Format: DHHMMSS)', 'type': 'entry'},
|
|
327
|
+
{'name': 'text_input', 'label': 'Excel Name', 'type': 'file', 'button': 'Select'},
|
|
328
|
+
],
|
|
329
|
+
'script': 'dc_single_end_feeding.py',
|
|
330
|
+
'back_target': 'PageSix',
|
|
331
|
+
},
|
|
332
|
+
'P13': {
|
|
333
|
+
'title': 'Page 13: Battery EMU Assessment',
|
|
334
|
+
'description': 'Battery EMU assessment workflows with simple and detailed modes.',
|
|
335
|
+
'notes': ['NOTE: Excel spreadsheet in .xlsx format is required before clicking RUN.', 'NOTE: Import the matching Excel template as required for your option.'],
|
|
336
|
+
'options': [
|
|
337
|
+
{'label': 'Option 1: Preliminary Assessment (Quick BEMU)', 'value': '1'},
|
|
338
|
+
{'label': 'Option 2: Update spreadsheet only (Quick BEMU)', 'value': '2'},
|
|
339
|
+
{'label': 'Option 3: Detailed Modelling Auto Assessment (<RN29)', 'value': '3'},
|
|
340
|
+
{'label': 'Option 4: Update spreadsheet only (<RN29)', 'value': '4'},
|
|
341
|
+
{'label': 'Option 5: New BEMU Assessment (>RN29)', 'value': '5'},
|
|
342
|
+
],
|
|
343
|
+
'fields': [
|
|
344
|
+
{'name': 'text_input', 'label': 'Excel Name', 'type': 'file', 'button': 'Select'},
|
|
345
|
+
{'name': 'time_start', 'label': 'Extraction From (Format: DHHMMSS)', 'type': 'entry'},
|
|
346
|
+
{'name': 'time_end', 'label': 'Extraction To (Format: DHHMMSS)', 'type': 'entry'},
|
|
347
|
+
],
|
|
348
|
+
'script': 'battery_processing.py',
|
|
349
|
+
'back_target': 'PageFour',
|
|
350
|
+
},
|
|
351
|
+
'P14': {
|
|
352
|
+
'title': 'Page 14: DC Falling Voltage Protection Processing',
|
|
353
|
+
'description': 'Assess falling voltage protection and optional step result plots.',
|
|
354
|
+
'notes': ['NOTE: Time window is compulsory.', 'NOTE: Excel spreadsheet in .xlsx format is required before clicking RUN.'],
|
|
355
|
+
'options': [
|
|
356
|
+
{'label': 'Option 1: FVP assessment with OSOP extraction', 'value': '1'},
|
|
357
|
+
{'label': 'Option 2: FVP assessment with OSOP extraction and plots', 'value': '2'},
|
|
358
|
+
{'label': 'Option 3: FVP assessment only', 'value': '3'},
|
|
359
|
+
{'label': 'Option 4: FVP assessment with plots only', 'value': '4'},
|
|
360
|
+
],
|
|
361
|
+
'fields': [
|
|
362
|
+
{'name': 'text_input', 'label': 'Excel Name', 'type': 'file', 'button': 'Select'},
|
|
363
|
+
{'name': 'time_start', 'label': 'Extraction From (Format: DHHMMSS)', 'type': 'entry'},
|
|
364
|
+
{'name': 'time_end', 'label': 'Extraction To (Format: DHHMMSS)', 'type': 'entry'},
|
|
365
|
+
],
|
|
366
|
+
'script': 'dc_falling_voltage_protection.py',
|
|
367
|
+
'back_target': 'PageSix',
|
|
368
|
+
},
|
|
369
|
+
'P15': {
|
|
370
|
+
'title': 'Page 15: Low Voltage Summary',
|
|
371
|
+
'description': 'Create a low voltage summary from train step results.',
|
|
372
|
+
'notes': ['NOTE: Excel spreadsheet in .xlsx format is required before clicking RUN.', 'NOTE: Train_list.csv within each simulation folder is required.'],
|
|
373
|
+
'fields': [
|
|
374
|
+
{'name': 'text_input', 'label': 'Excel Name', 'type': 'file', 'button': 'Select'},
|
|
375
|
+
],
|
|
376
|
+
'script': 'low_v_summary.py',
|
|
377
|
+
'back_target': 'PageFour',
|
|
378
|
+
},
|
|
379
|
+
'P16': {
|
|
380
|
+
'title': 'Page 16: Batch Operation Settings',
|
|
381
|
+
'description': 'Run multiple analysis options in an unattended batch sequence.',
|
|
382
|
+
'notes': ['NOTE: Excel spreadsheet in .xlsx format is required before clicking RUN.'],
|
|
383
|
+
'options': [
|
|
384
|
+
{'label': 'Option 1: Run under root folder (recommended)', 'value': '1'},
|
|
385
|
+
{'label': 'Option 2: Run from anywhere (beta)', 'value': '2'},
|
|
386
|
+
],
|
|
387
|
+
'fields': [
|
|
388
|
+
{'name': 'text_input', 'label': 'Excel Name', 'type': 'file', 'button': 'Select'},
|
|
389
|
+
],
|
|
390
|
+
'script': 'all_in_one_processing.py',
|
|
391
|
+
'back_target': 'PageFour',
|
|
392
|
+
},
|
|
393
|
+
'P17': {
|
|
394
|
+
'title': 'Page 17: Voltage Profile Assessment',
|
|
395
|
+
'description': 'Plot low pantograph voltage profiles for branches and filtered time windows.',
|
|
396
|
+
'notes': ['NOTE: Excel spreadsheet in .xlsx format is required before clicking RUN.', 'NOTE: Train_list.csv within each simulation folder is required.'],
|
|
397
|
+
'options': [
|
|
398
|
+
{'label': 'Option 1: Plot low voltage summary only', 'value': '1'},
|
|
399
|
+
{'label': 'Option 2: Plot all pantograph voltage', 'value': '2'},
|
|
400
|
+
],
|
|
401
|
+
'fields': [
|
|
402
|
+
{'name': 'text_input', 'label': 'Excel Name', 'type': 'file', 'button': 'Select'},
|
|
403
|
+
],
|
|
404
|
+
'script': 'low_v_plot.py',
|
|
405
|
+
'back_target': 'PageFour',
|
|
406
|
+
},
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def create_action_page_class(name, config):
|
|
411
|
+
return type(name, (ActionPage,), {'config': config})
|
|
412
|
+
|
|
413
|
+
for page_name, page_config in ACTION_PAGE_CONFIGS.items():
|
|
414
|
+
globals()[page_name] = create_action_page_class(page_name, page_config)
|
PyELSSA/licensing.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
#=================================================================
|
|
4
|
+
# Created by: Jieming Ye
|
|
5
|
+
# Created on: Aug 2026
|
|
6
|
+
# Last Modified: Aug 2026
|
|
7
|
+
#=================================================================
|
|
8
|
+
# Copyright (c) 2026 [Jieming Ye]
|
|
9
|
+
#
|
|
10
|
+
# This Python source code is licensed under the
|
|
11
|
+
# Open Source Non-Commercial License (OSNCL) v1.0
|
|
12
|
+
# See LICENSE for details.
|
|
13
|
+
#=================================================================
|
|
14
|
+
"""
|
|
15
|
+
Pre-requisite:
|
|
16
|
+
N/A
|
|
17
|
+
Used Input:
|
|
18
|
+
N/A
|
|
19
|
+
Expected Output:
|
|
20
|
+
True/Flase
|
|
21
|
+
Description:
|
|
22
|
+
This module is the license process
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
#=================================================================
|
|
26
|
+
# VERSION CONTROL
|
|
27
|
+
# V1.0 (Jieming Ye) - Initial Version
|
|
28
|
+
# V2.0 (Jieming Ye) - Configuration File Processing added
|
|
29
|
+
#=================================================================
|
|
30
|
+
# Set Information Variable
|
|
31
|
+
# N/A
|
|
32
|
+
#=================================================================
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
import uuid
|
|
36
|
+
import requests
|
|
37
|
+
import psutil
|
|
38
|
+
import json
|
|
39
|
+
from datetime import datetime, timedelta
|
|
40
|
+
|
|
41
|
+
from PyELSSA.shared_contents import SharedVariables
|
|
42
|
+
from PyELSSA.shared_contents import SharedMethods
|
|
43
|
+
|
|
44
|
+
# Constants for file path
|
|
45
|
+
LICENSE_FILE = SharedVariables.license_file
|
|
46
|
+
|
|
47
|
+
# Function to get the MAC address of the current machine
|
|
48
|
+
def get_mac_address():
|
|
49
|
+
mac_addresses = []
|
|
50
|
+
try:
|
|
51
|
+
# mac_code = uuid.UUID(int=uuid.getnode()).hex[-12:]
|
|
52
|
+
# mac = ":".join([mac_code[e:e+2] for e in range(0, 11, 2)])
|
|
53
|
+
for interface, addrs in psutil.net_if_addrs().items():
|
|
54
|
+
for addr in addrs:
|
|
55
|
+
if addr.family == psutil.AF_LINK: # AF_LINK corresponds to MAC addresses
|
|
56
|
+
# mac_addresses.append((interface, addr.address)) # Return interface name and MAC address
|
|
57
|
+
mac_addresses.append(addr.address)
|
|
58
|
+
|
|
59
|
+
except Exception as e:
|
|
60
|
+
SharedMethods.print_message(f"ERROR: Fail to read mac address: {e}","31")
|
|
61
|
+
|
|
62
|
+
return mac_addresses
|
|
63
|
+
|
|
64
|
+
# Function to fetch the allowed MAC addresses from an online file
|
|
65
|
+
def fetch_allowed_macs(url):
|
|
66
|
+
try:
|
|
67
|
+
response = requests.get(url, timeout=5)
|
|
68
|
+
response.raise_for_status() # Check for HTTP errors
|
|
69
|
+
lines = response.text.splitlines()
|
|
70
|
+
|
|
71
|
+
allowed_macs = []
|
|
72
|
+
|
|
73
|
+
# Process each line
|
|
74
|
+
for line in lines:
|
|
75
|
+
line = line.strip()
|
|
76
|
+
# Ignore lines that start with '//'
|
|
77
|
+
if line.startswith("//"):
|
|
78
|
+
continue
|
|
79
|
+
# If the line looks like a MAC address (e.g., xx-xx-xx-xx-xx-xx)
|
|
80
|
+
if len(line) == 17 and all(c in "0123456789ABCDEFabcdef-:" for c in line):
|
|
81
|
+
# # Convert MAC to lowercase and replace '-' with ':'
|
|
82
|
+
# formatted_mac = line.lower().replace('-', ':')
|
|
83
|
+
allowed_macs.append(line)
|
|
84
|
+
|
|
85
|
+
return allowed_macs
|
|
86
|
+
|
|
87
|
+
except requests.exceptions.Timeout:
|
|
88
|
+
SharedMethods.print_message("ERROR: Request timed out after 5 seconds. POOR INTERNET...", "31")
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
SharedMethods.print_message(f"ERROR: Error fetching allowed MAC addresses: {e}","31")
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
# Function to check if the MAC address is allowed
|
|
96
|
+
def is_mac_allowed(mac_address, allowed_macs):
|
|
97
|
+
# if any mac address in the list is allowed, return True
|
|
98
|
+
for mac in mac_address:
|
|
99
|
+
if mac in allowed_macs:
|
|
100
|
+
return True
|
|
101
|
+
# if no mac address in the list is allowed, return False
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
# Function to read from the license file
|
|
105
|
+
def read_license_file():
|
|
106
|
+
if os.path.exists(LICENSE_FILE):
|
|
107
|
+
try:
|
|
108
|
+
with open(LICENSE_FILE, 'r') as file:
|
|
109
|
+
license_data = json.load(file)
|
|
110
|
+
status = license_data.get('status')
|
|
111
|
+
expiry_date_str = license_data.get('expiry_date')
|
|
112
|
+
unique_key = license_data.get('unique_key')
|
|
113
|
+
return status, expiry_date_str, unique_key
|
|
114
|
+
except Exception as e:
|
|
115
|
+
SharedMethods.print_message(f"ERROR: Error reading license: {e}","31")
|
|
116
|
+
return None, None, None
|
|
117
|
+
else:
|
|
118
|
+
SharedMethods.print_message(f"WARNING: License key DOES NOT exist yet...","33")
|
|
119
|
+
|
|
120
|
+
return None, None, None
|
|
121
|
+
|
|
122
|
+
# Function to write to the license file
|
|
123
|
+
def write_license_file(status, expiry_date_str, unique_key):
|
|
124
|
+
try:
|
|
125
|
+
os.makedirs(SharedVariables.configuration_path, exist_ok=True)
|
|
126
|
+
license_data = {
|
|
127
|
+
'status': status,
|
|
128
|
+
'expiry_date': expiry_date_str,
|
|
129
|
+
'unique_key': unique_key,
|
|
130
|
+
}
|
|
131
|
+
with open(LICENSE_FILE, 'w') as file:
|
|
132
|
+
json.dump(license_data, file, indent=4)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
SharedMethods.print_message(f"ERROR: Error writing license: {e}","31")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
# Function to check if the license is still valid
|
|
138
|
+
def is_license_valid(expiry_date_str,days_valid):
|
|
139
|
+
try:
|
|
140
|
+
if expiry_date_str:
|
|
141
|
+
expiry_date = datetime.strptime(expiry_date_str, "%Y-%m-%d")
|
|
142
|
+
start_date = expiry_date - timedelta(days=days_valid)
|
|
143
|
+
return start_date <= datetime.now() <= expiry_date
|
|
144
|
+
except:
|
|
145
|
+
return False
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
# Function to set license expiry in the license file
|
|
149
|
+
def set_license_expiry(days_valid, unique_key):
|
|
150
|
+
expiry_date = datetime.now() + timedelta(days=days_valid)
|
|
151
|
+
expiry_date_str = expiry_date.strftime("%Y-%m-%d")
|
|
152
|
+
write_license_file("1", expiry_date_str, unique_key)
|
|
153
|
+
|
|
154
|
+
# Function to allow user try five times password
|
|
155
|
+
def admin_password_bypass():
|
|
156
|
+
max_attempts = 5
|
|
157
|
+
|
|
158
|
+
for attempt in range(1, max_attempts + 1):
|
|
159
|
+
SharedMethods.print_message(f"ATTENTION: Attempt: ({attempt}/{max_attempts})","33")
|
|
160
|
+
user_input = input(f"Enter Admin Password: ")
|
|
161
|
+
if user_input == SharedVariables.admin_password:
|
|
162
|
+
SharedMethods.print_message(f"INFO: Temporary access granted. Please connect to the open internet to renew the license when possible.", '32')
|
|
163
|
+
return True
|
|
164
|
+
else:
|
|
165
|
+
SharedMethods.print_message(f"ERROR: Incorrect password. Try Again.","31")
|
|
166
|
+
|
|
167
|
+
SharedMethods.print_message("ERROR: Access Denied. Too many failed attempts.","31")
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
# Main function to start the application
|
|
171
|
+
def valid_license_main():
|
|
172
|
+
# disable in debug mode (bypass the process in debug mode):
|
|
173
|
+
if SharedMethods.is_debug_mode():
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
# Read the license information from the file
|
|
177
|
+
license_status, expiry_date_str, unique_key = read_license_file()
|
|
178
|
+
|
|
179
|
+
if license_status == "1" and is_license_valid(expiry_date_str,30):
|
|
180
|
+
SharedMethods.print_message(f"INFO: Valid License. Due to expiry on {expiry_date_str}.", '32')
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
else:
|
|
184
|
+
SharedMethods.print_message(f"WARNING: License not valid. Online validation process started...","33")
|
|
185
|
+
SharedMethods.print_message(f"ATTENTION: Ensure you have a valid online connection...","33")
|
|
186
|
+
mac_address = get_mac_address()
|
|
187
|
+
allowed_macs_url = SharedVariables.license_online # Replace with your URL
|
|
188
|
+
allowed_macs = fetch_allowed_macs(allowed_macs_url)
|
|
189
|
+
|
|
190
|
+
if not allowed_macs or not is_mac_allowed(mac_address, allowed_macs):
|
|
191
|
+
SharedMethods.print_message(f"ERROR: Unauthorized machine detected. This attempt have been notified.","31")
|
|
192
|
+
# Set license file status to 0 (failed check)
|
|
193
|
+
write_license_file("0", "", unique_key)
|
|
194
|
+
SharedMethods.print_message(f"ATTENTION: To authorize your machine. Please send all your machines MAC address to {SharedVariables.contacts}.","33")
|
|
195
|
+
SharedMethods.print_message(f"ATTENTION: To fetch your mac address. Please refer to '{SharedVariables.support_online}' and follow instructions.","33")
|
|
196
|
+
SharedMethods.print_message(f"ATTENTION: If you believe there is any issue, please contact support via {SharedVariables.contacts}.","33")
|
|
197
|
+
|
|
198
|
+
# allow user to bypass the password
|
|
199
|
+
if admin_password_bypass():
|
|
200
|
+
return True
|
|
201
|
+
else:
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
# License check passed, generate a unique key if it doesn't exist and set expiry
|
|
205
|
+
# (TODO) This is to be updated to a more secure and encrpted way in the future.
|
|
206
|
+
# Due to the source code is able to be modified and licensing can be bypassed. No addtional encrption is considered.
|
|
207
|
+
if unique_key == None:
|
|
208
|
+
unique_key = str(uuid.uuid4())
|
|
209
|
+
|
|
210
|
+
set_license_expiry(days_valid=30, unique_key=unique_key)
|
|
211
|
+
SharedMethods.print_message(f"License created / updated successfully. Due to be reviewed in 30 days.", '32')
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
def main():
|
|
215
|
+
if not valid_license_main():
|
|
216
|
+
SharedMethods.print_message(f"ERROR: Validation Failed. The application cannot be started.","31")
|
|
217
|
+
return False
|
|
218
|
+
else:
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Programme running
|
|
223
|
+
if __name__ == '__main__':
|
|
224
|
+
main()
|