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.
@@ -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()