Saltar a contenido

Referencia de la API

⚠️ Nota / Note:
Esta página está disponible solo en Inglés temporalmente. This page is currently available in English only.

Proyecto compatible con Eventlet y GEvent.

business_logic

program_manager

AttendancesManager

Bases: AttendancesManagerBase

Source code in src\business_logic\program_manager.py
 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
class AttendancesManager(AttendancesManagerBase):
    def __init__(self):
        """
        Initializes the ProgramManager instance.

        This constructor sets up the initial state of the ProgramManager by
        creating a new instance of SharedState and passing it to the parent
        class initializer.

        Attributes:
            state (SharedState): The shared state object used to manage
            program-wide data and operations.
        """
        self.state = SharedState()
        super().__init__(self.state)

    def manage_devices_attendances(self, selected_ips: list[str], emit_progress: Callable = None):
        """
        Manages the attendance records for the specified devices.
        This method processes attendance data for the devices with the given IPs.
        It also handles configuration settings related to clearing attendance data
        and updates the configuration file if necessary.

        Args:
            selected_ips (list[str]): A list of IP addresses of the devices to manage.
            emit_progress (Callable, optional): A callable function to emit progress updates. Defaults to None.

        Returns:
            (int): The count of attendances processed.

        Side Effects:
            - Reads configuration settings from 'config.ini'.
            - Resets the internal state before processing.
            - Updates the 'force_clear_attendance' setting in 'config.ini' if it was set to True.
        """
        self.emit_progress: Callable = emit_progress
        config.read(os.path.join(find_root_directory(), 'config.ini'))
        self.clear_attendance: bool = config.getboolean('Device_config', 'clear_attendance')
        self.force_clear_attendance: bool = config.getboolean('Device_config', 'force_clear_attendance')
        logging.debug(f'force_clear_attendance: {self.force_clear_attendance}')
        self.state.reset()
        attendances_count = super().manage_devices_attendances(selected_ips)
        if self.force_clear_attendance:
            self.force_clear_attendance = False
            config['Device_config']['force_clear_attendance'] = 'False'

            with open('config.ini', 'w') as configfile:
                config.write(configfile)
        return attendances_count

    def manage_attendances_of_one_device(self, device: Device):
        """
        Manages the attendance data for a single device.
        This method handles the connection to a device, retrieves attendance data, processes it,
        and updates the device's state and attendance records. It also manages error handling
        and ensures proper cleanup of resources.

        Args:
            device (Device): The device object representing the attendance device to be managed.

        Workflow:
            1. Establishes a connection to the device using `ConnectionManager`.
            2. Retrieves attendance data from the device.
            3. Formats the attendance data and handles any errors during formatting.
            4. Clears attendance data on the device based on the `clear_attendance` flag.
            5. Updates the device's model name if possible.
            6. Processes individual and global attendance records.
            7. Synchronizes the device's time and handles time-related errors.
            8. Updates the attendance count for the device in a shared dictionary.
            9. Ensures proper disconnection from the device and updates progress tracking.

        Exceptions:
            - Handles `NetworkError` and `ObtainAttendancesError` during connection and data retrieval.
            - Raises `ConnectionFailedError` if the connection to the device fails.
            - Catches and re-raises other exceptions as `BaseError` with a specific error code.
            - Handles time synchronization errors such as `OutdatedTimeError` and updates the battery status.

        Logging:
            - Logs debug information for connection initiation, attendance processing, and cleanup.
            - Logs errors and warnings for failed connections and other issues.

        Returns:
            None
        """
        logging.debug(f"Iniciando {device.ip}")
        try:
            try:
                conn_manager = ConnectionManager(device.ip, 4370, device.communication)
                #import time
                #start_time = time.time()
                conn_manager.connect_with_retry()
                #end_time = time.time()
                #logging.debug(f'{device.ip} - Tiempo de conexión total: {(end_time - start_time):2f}')
                attendances: list[Attendance] = conn_manager.get_attendances()
                #logging.info(f'{device.ip} - PREFORMATEO - Longitud marcaciones: {len(attendances)} - Marcaciones: {attendances}')
                attendances, attendances_with_error = self.format_attendances(attendances, device.id)
                if len(attendances_with_error) > 0:
                    if not self.force_clear_attendance:
                        self.clear_attendance = False
                        logging.debug(f'No se eliminaran las marcaciones correspondientes al dispositivo {device.ip}')
                #logging.info(f'{device.ip} - POSTFORMATEO - Longitud marcaciones: {len(attendances)} - Marcaciones: {attendances}')
                logging.debug(f'clear_attendance: {self.clear_attendance}')
                conn_manager.clear_attendances(self.clear_attendance)
            except (NetworkError, ObtainAttendancesError) as e:
                with self.lock:
                    self.attendances_count_devices[device.ip] = {
                        "connection failed": True
                    }
                raise ConnectionFailedError(device.model_name, device.point, device.ip)
            except Exception as e:
                raise BaseError(3000, str(e)) from e

            try:
                device.model_name = conn_manager.update_device_name()
            except Exception as e:
                pass

            self.manage_individual_attendances(device, attendances)
            self.manage_global_attendances(attendances)

            try:
                conn_manager.update_time()
            except NetworkError as e:
                NetworkError(f'{device.model_name}, {device.point}, {device.ip}')
            except OutdatedTimeError as e:
                HourManager().update_battery_status(device.ip)
                BatteryFailingError(device.model_name, device.point, device.ip)

            with self.lock:
                self.attendances_count_devices[device.ip] = {
                    "attendance count": str(len(attendances))
                }
        except Exception as e:
            pass
        finally:
            if conn_manager.is_connected():
                conn_manager.disconnect()
            ProgressTracker(self.state, self.emit_progress).update(device)
            logging.debug(f"Finalizando {device.ip}")
        return
__init__()

Initializes the ProgramManager instance.

This constructor sets up the initial state of the ProgramManager by creating a new instance of SharedState and passing it to the parent class initializer.

Attributes:

Name Type Description
state SharedState

The shared state object used to manage

Source code in src\business_logic\program_manager.py
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def __init__(self):
    """
    Initializes the ProgramManager instance.

    This constructor sets up the initial state of the ProgramManager by
    creating a new instance of SharedState and passing it to the parent
    class initializer.

    Attributes:
        state (SharedState): The shared state object used to manage
        program-wide data and operations.
    """
    self.state = SharedState()
    super().__init__(self.state)
manage_attendances_of_one_device(device)

Manages the attendance data for a single device. This method handles the connection to a device, retrieves attendance data, processes it, and updates the device's state and attendance records. It also manages error handling and ensures proper cleanup of resources.

Parameters:

Name Type Description Default
device Device

The device object representing the attendance device to be managed.

required
Workflow
  1. Establishes a connection to the device using ConnectionManager.
  2. Retrieves attendance data from the device.
  3. Formats the attendance data and handles any errors during formatting.
  4. Clears attendance data on the device based on the clear_attendance flag.
  5. Updates the device's model name if possible.
  6. Processes individual and global attendance records.
  7. Synchronizes the device's time and handles time-related errors.
  8. Updates the attendance count for the device in a shared dictionary.
  9. Ensures proper disconnection from the device and updates progress tracking.
Logging
  • Logs debug information for connection initiation, attendance processing, and cleanup.
  • Logs errors and warnings for failed connections and other issues.

Returns:

Type Description

None

Source code in src\business_logic\program_manager.py
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
def manage_attendances_of_one_device(self, device: Device):
    """
    Manages the attendance data for a single device.
    This method handles the connection to a device, retrieves attendance data, processes it,
    and updates the device's state and attendance records. It also manages error handling
    and ensures proper cleanup of resources.

    Args:
        device (Device): The device object representing the attendance device to be managed.

    Workflow:
        1. Establishes a connection to the device using `ConnectionManager`.
        2. Retrieves attendance data from the device.
        3. Formats the attendance data and handles any errors during formatting.
        4. Clears attendance data on the device based on the `clear_attendance` flag.
        5. Updates the device's model name if possible.
        6. Processes individual and global attendance records.
        7. Synchronizes the device's time and handles time-related errors.
        8. Updates the attendance count for the device in a shared dictionary.
        9. Ensures proper disconnection from the device and updates progress tracking.

    Exceptions:
        - Handles `NetworkError` and `ObtainAttendancesError` during connection and data retrieval.
        - Raises `ConnectionFailedError` if the connection to the device fails.
        - Catches and re-raises other exceptions as `BaseError` with a specific error code.
        - Handles time synchronization errors such as `OutdatedTimeError` and updates the battery status.

    Logging:
        - Logs debug information for connection initiation, attendance processing, and cleanup.
        - Logs errors and warnings for failed connections and other issues.

    Returns:
        None
    """
    logging.debug(f"Iniciando {device.ip}")
    try:
        try:
            conn_manager = ConnectionManager(device.ip, 4370, device.communication)
            #import time
            #start_time = time.time()
            conn_manager.connect_with_retry()
            #end_time = time.time()
            #logging.debug(f'{device.ip} - Tiempo de conexión total: {(end_time - start_time):2f}')
            attendances: list[Attendance] = conn_manager.get_attendances()
            #logging.info(f'{device.ip} - PREFORMATEO - Longitud marcaciones: {len(attendances)} - Marcaciones: {attendances}')
            attendances, attendances_with_error = self.format_attendances(attendances, device.id)
            if len(attendances_with_error) > 0:
                if not self.force_clear_attendance:
                    self.clear_attendance = False
                    logging.debug(f'No se eliminaran las marcaciones correspondientes al dispositivo {device.ip}')
            #logging.info(f'{device.ip} - POSTFORMATEO - Longitud marcaciones: {len(attendances)} - Marcaciones: {attendances}')
            logging.debug(f'clear_attendance: {self.clear_attendance}')
            conn_manager.clear_attendances(self.clear_attendance)
        except (NetworkError, ObtainAttendancesError) as e:
            with self.lock:
                self.attendances_count_devices[device.ip] = {
                    "connection failed": True
                }
            raise ConnectionFailedError(device.model_name, device.point, device.ip)
        except Exception as e:
            raise BaseError(3000, str(e)) from e

        try:
            device.model_name = conn_manager.update_device_name()
        except Exception as e:
            pass

        self.manage_individual_attendances(device, attendances)
        self.manage_global_attendances(attendances)

        try:
            conn_manager.update_time()
        except NetworkError as e:
            NetworkError(f'{device.model_name}, {device.point}, {device.ip}')
        except OutdatedTimeError as e:
            HourManager().update_battery_status(device.ip)
            BatteryFailingError(device.model_name, device.point, device.ip)

        with self.lock:
            self.attendances_count_devices[device.ip] = {
                "attendance count": str(len(attendances))
            }
    except Exception as e:
        pass
    finally:
        if conn_manager.is_connected():
            conn_manager.disconnect()
        ProgressTracker(self.state, self.emit_progress).update(device)
        logging.debug(f"Finalizando {device.ip}")
    return
manage_devices_attendances(selected_ips, emit_progress=None)

Manages the attendance records for the specified devices. This method processes attendance data for the devices with the given IPs. It also handles configuration settings related to clearing attendance data and updates the configuration file if necessary.

Parameters:

Name Type Description Default
selected_ips list[str]

A list of IP addresses of the devices to manage.

required
emit_progress Callable

A callable function to emit progress updates. Defaults to None.

None

Returns:

Type Description
int

The count of attendances processed.

Side Effects
  • Reads configuration settings from 'config.ini'.
  • Resets the internal state before processing.
  • Updates the 'force_clear_attendance' setting in 'config.ini' if it was set to True.
Source code in src\business_logic\program_manager.py
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
def manage_devices_attendances(self, selected_ips: list[str], emit_progress: Callable = None):
    """
    Manages the attendance records for the specified devices.
    This method processes attendance data for the devices with the given IPs.
    It also handles configuration settings related to clearing attendance data
    and updates the configuration file if necessary.

    Args:
        selected_ips (list[str]): A list of IP addresses of the devices to manage.
        emit_progress (Callable, optional): A callable function to emit progress updates. Defaults to None.

    Returns:
        (int): The count of attendances processed.

    Side Effects:
        - Reads configuration settings from 'config.ini'.
        - Resets the internal state before processing.
        - Updates the 'force_clear_attendance' setting in 'config.ini' if it was set to True.
    """
    self.emit_progress: Callable = emit_progress
    config.read(os.path.join(find_root_directory(), 'config.ini'))
    self.clear_attendance: bool = config.getboolean('Device_config', 'clear_attendance')
    self.force_clear_attendance: bool = config.getboolean('Device_config', 'force_clear_attendance')
    logging.debug(f'force_clear_attendance: {self.force_clear_attendance}')
    self.state.reset()
    attendances_count = super().manage_devices_attendances(selected_ips)
    if self.force_clear_attendance:
        self.force_clear_attendance = False
        config['Device_config']['force_clear_attendance'] = 'False'

        with open('config.ini', 'w') as configfile:
            config.write(configfile)
    return attendances_count

ConnectionsInfo

Bases: OperationManager

Source code in src\business_logic\program_manager.py
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
class ConnectionsInfo(OperationManager):
    def __init__(self):
        """
        Initializes the ProgramManager instance.

        This constructor sets up the shared state and initializes a dictionary
        to store connection information. It also calls the superclass initializer
        with the shared state.

        Attributes:
            state (SharedState): An instance of SharedState to manage shared resources.
            connections_info (dict[str, ConnectionInfo]): A dictionary mapping connection
                identifiers to their respective ConnectionInfo objects.
        """
        self.state = SharedState()
        self.connections_info: dict[str, ConnectionInfo] = {}
        super().__init__(self.state)

    def obtain_connections_info(self, selected_ips: list[str], emit_progress: Callable = None):
        """
        Obtains connection information for a list of selected IPs and manages the threading process 
        to retrieve this information from devices.

        Args:
            selected_ips (list[str]): A list of IP addresses to connect to and retrieve information from.
            emit_progress (Callable, optional): A callable function to emit progress updates during 
                the operation. Defaults to None.

        Returns:
            (dict): A dictionary containing connection information for the devices if any connections 
                    were successfully established. Returns an empty dictionary if no connections were made.
        """
        self.connections_info.clear()
        self.emit_progress: Callable = emit_progress
        self.state.reset()

        super().manage_threads_to_devices(selected_ips=selected_ips, function=self.obtain_connection_info)

        if len(self.connections_info) > 0:
            return self.connections_info

    def obtain_connection_info(self, device: Device):
        """
        Establishes a connection to a device, retrieves its connection information, 
        and updates the connection status.

        Args:
            device (Device): The device object containing information such as IP, 
                             communication type, and model name.

        Raises:
            ConnectionFailedError: If the connection to the device fails due to a 
                                   network error.
            BaseError: If any other unexpected exception occurs during the process.

        Workflow:
            1. Initializes a connection manager for the device using its IP and 
               communication type.
            2. Attempts to connect to the device with retries.
            3. Pings the device to verify connectivity.
            4. If the ping is successful, retrieves device information and updates 
               the connection info.
            5. If the ping fails or a network error occurs, marks the connection 
               as failed and raises a ConnectionFailedError.
            6. Updates the shared connection information dictionary with the 
               device's connection status.
            7. Ensures the connection is properly disconnected in the `finally` block.
            8. Updates the progress tracker and logs the completion of the process.

        Note:
            This method uses a lock to ensure thread-safe updates to the shared 
            `connections_info` dictionary.
        """
        try:
            try:
                logging.debug(f"Iniciando {device.ip}")
                conn_manager: ConnectionManager = ConnectionManager(device.ip, 4370, device.communication)
                connection_info: ConnectionInfo = ConnectionInfo()
                conn_manager.connect_with_retry()
                test_ping_connection: bool = conn_manager.ping_device()
                if test_ping_connection:
                    device_info: DeviceInfo = conn_manager.obtain_device_info()
                    connection_info.update({
                        "connection_failed": False,
                        "device_info": device_info
                    })
                else:
                    connection_info.update({
                        "connection_failed": True,
                    })
                with self.lock:
                    self.connections_info[device.ip] = connection_info
            except NetworkError as e:
                connection_info.update({
                    "connection_failed": True
                })
                with self.lock:
                    self.connections_info[device.ip] = connection_info
                raise ConnectionFailedError(device.model_name, device.point, device.ip)
        except ConnectionFailedError:
            pass
        except Exception as e:
            BaseError(3000, str(e))
        finally:
            if conn_manager.is_connected():
                conn_manager.disconnect()
            ProgressTracker(self.state, self.emit_progress).update(device)
            logging.debug(f"Finalizando {device.ip}")
        return
__init__()

Initializes the ProgramManager instance.

This constructor sets up the shared state and initializes a dictionary to store connection information. It also calls the superclass initializer with the shared state.

Attributes:

Name Type Description
state SharedState

An instance of SharedState to manage shared resources.

connections_info dict[str, ConnectionInfo]

A dictionary mapping connection identifiers to their respective ConnectionInfo objects.

Source code in src\business_logic\program_manager.py
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
def __init__(self):
    """
    Initializes the ProgramManager instance.

    This constructor sets up the shared state and initializes a dictionary
    to store connection information. It also calls the superclass initializer
    with the shared state.

    Attributes:
        state (SharedState): An instance of SharedState to manage shared resources.
        connections_info (dict[str, ConnectionInfo]): A dictionary mapping connection
            identifiers to their respective ConnectionInfo objects.
    """
    self.state = SharedState()
    self.connections_info: dict[str, ConnectionInfo] = {}
    super().__init__(self.state)
obtain_connection_info(device)

Establishes a connection to a device, retrieves its connection information, and updates the connection status.

Parameters:

Name Type Description Default
device Device

The device object containing information such as IP, communication type, and model name.

required

Raises:

Type Description
ConnectionFailedError

If the connection to the device fails due to a network error.

BaseError

If any other unexpected exception occurs during the process.

Workflow
  1. Initializes a connection manager for the device using its IP and communication type.
  2. Attempts to connect to the device with retries.
  3. Pings the device to verify connectivity.
  4. If the ping is successful, retrieves device information and updates the connection info.
  5. If the ping fails or a network error occurs, marks the connection as failed and raises a ConnectionFailedError.
  6. Updates the shared connection information dictionary with the device's connection status.
  7. Ensures the connection is properly disconnected in the finally block.
  8. Updates the progress tracker and logs the completion of the process.
Note

This method uses a lock to ensure thread-safe updates to the shared connections_info dictionary.

Source code in src\business_logic\program_manager.py
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
def obtain_connection_info(self, device: Device):
    """
    Establishes a connection to a device, retrieves its connection information, 
    and updates the connection status.

    Args:
        device (Device): The device object containing information such as IP, 
                         communication type, and model name.

    Raises:
        ConnectionFailedError: If the connection to the device fails due to a 
                               network error.
        BaseError: If any other unexpected exception occurs during the process.

    Workflow:
        1. Initializes a connection manager for the device using its IP and 
           communication type.
        2. Attempts to connect to the device with retries.
        3. Pings the device to verify connectivity.
        4. If the ping is successful, retrieves device information and updates 
           the connection info.
        5. If the ping fails or a network error occurs, marks the connection 
           as failed and raises a ConnectionFailedError.
        6. Updates the shared connection information dictionary with the 
           device's connection status.
        7. Ensures the connection is properly disconnected in the `finally` block.
        8. Updates the progress tracker and logs the completion of the process.

    Note:
        This method uses a lock to ensure thread-safe updates to the shared 
        `connections_info` dictionary.
    """
    try:
        try:
            logging.debug(f"Iniciando {device.ip}")
            conn_manager: ConnectionManager = ConnectionManager(device.ip, 4370, device.communication)
            connection_info: ConnectionInfo = ConnectionInfo()
            conn_manager.connect_with_retry()
            test_ping_connection: bool = conn_manager.ping_device()
            if test_ping_connection:
                device_info: DeviceInfo = conn_manager.obtain_device_info()
                connection_info.update({
                    "connection_failed": False,
                    "device_info": device_info
                })
            else:
                connection_info.update({
                    "connection_failed": True,
                })
            with self.lock:
                self.connections_info[device.ip] = connection_info
        except NetworkError as e:
            connection_info.update({
                "connection_failed": True
            })
            with self.lock:
                self.connections_info[device.ip] = connection_info
            raise ConnectionFailedError(device.model_name, device.point, device.ip)
    except ConnectionFailedError:
        pass
    except Exception as e:
        BaseError(3000, str(e))
    finally:
        if conn_manager.is_connected():
            conn_manager.disconnect()
        ProgressTracker(self.state, self.emit_progress).update(device)
        logging.debug(f"Finalizando {device.ip}")
    return
obtain_connections_info(selected_ips, emit_progress=None)

Obtains connection information for a list of selected IPs and manages the threading process to retrieve this information from devices.

Parameters:

Name Type Description Default
selected_ips list[str]

A list of IP addresses to connect to and retrieve information from.

required
emit_progress Callable

A callable function to emit progress updates during the operation. Defaults to None.

None

Returns:

Type Description
dict

A dictionary containing connection information for the devices if any connections were successfully established. Returns an empty dictionary if no connections were made.

Source code in src\business_logic\program_manager.py
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
def obtain_connections_info(self, selected_ips: list[str], emit_progress: Callable = None):
    """
    Obtains connection information for a list of selected IPs and manages the threading process 
    to retrieve this information from devices.

    Args:
        selected_ips (list[str]): A list of IP addresses to connect to and retrieve information from.
        emit_progress (Callable, optional): A callable function to emit progress updates during 
            the operation. Defaults to None.

    Returns:
        (dict): A dictionary containing connection information for the devices if any connections 
                were successfully established. Returns an empty dictionary if no connections were made.
    """
    self.connections_info.clear()
    self.emit_progress: Callable = emit_progress
    self.state.reset()

    super().manage_threads_to_devices(selected_ips=selected_ips, function=self.obtain_connection_info)

    if len(self.connections_info) > 0:
        return self.connections_info

HourManager

Bases: HourManagerBase

Source code in src\business_logic\program_manager.py
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
class HourManager(HourManagerBase):
    def __init__(self):
        """
        Initializes the ProgramManager instance.

        This constructor sets up the initial state of the ProgramManager by
        creating a new instance of SharedState and passing it to the parent
        class initializer.

        Attributes:
            state (SharedState): The shared state object used to manage
            program-wide state and data.
        """
        self.state = SharedState()
        super().__init__(self.state)

    def manage_hour_devices(self, selected_ips: list[str], emit_progress: Callable = None):
        """
        Synchronizes the time on a list of devices identified by their IP addresses.

        Args:
            selected_ips (list[str]): A list of IP addresses of the devices to update.
            emit_progress (Callable, optional): A callback function to emit progress updates. Defaults to None.

        Returns:
            (Any): The result of the `update_devices_time` method from the superclass.
        """
        self.emit_progress: Callable = emit_progress
        self.state.reset()
        return super().update_devices_time(selected_ips)

    def update_device_time_of_one_device(self, device: Device):
        """
        Updates the device time for a single device.

        This method attempts to connect to the specified device, update its time, 
        and handle any errors that may occur during the process. It also updates 
        the device's error status and progress tracking.

        Args:
            device (Device): The device object containing information such as 
                             IP address, communication type, model name, and point.

        Raises:
            ConnectionFailedError: If the connection to the device fails.
            BatteryFailingError: If the device's battery is failing and its time 
                                 cannot be updated.
            BaseError: For any other unexpected errors, with error code 3000.

        Notes:
            - Uses a lock to ensure thread-safe updates to the `devices_errors` dictionary.
            - Updates the battery status if an outdated time error occurs.
            - Ensures the connection is properly closed in the `finally` block.
            - Tracks progress using the `ProgressTracker` class.
        """
        logging.debug(f"Iniciando {device.ip}")
        try:
            try:
                conn_manager: ConnectionManager = ConnectionManager(device.ip, 4370, device.communication)
                conn_manager.connect_with_retry()
                with self.lock:
                    self.devices_errors[device.ip] = { "connection failed": False }
                conn_manager.update_time()
                with self.lock:
                    self.devices_errors[device.ip] = { "battery failing": False }
            except NetworkError as e:
                with self.lock:
                    self.devices_errors[device.ip] = { "connection failed": True }
                raise ConnectionFailedError(device.model_name, device.point, device.ip)
            except OutdatedTimeError as e:
                with self.lock:
                    self.devices_errors[device.ip] = { "battery failing": True }
                HourManager().update_battery_status(device.ip)
                raise BatteryFailingError(device.model_name, device.point, device.ip)
        except ConnectionFailedError as e:
            pass
        except BatteryFailingError as e:
            pass
        except Exception as e:
            BaseError(3000, str(e))
        finally:
            if conn_manager.is_connected():
                conn_manager.disconnect()
            ProgressTracker(self.state, self.emit_progress).update(device)
            logging.debug(f"Finalizando {device.ip}")
        return
__init__()

Initializes the ProgramManager instance.

This constructor sets up the initial state of the ProgramManager by creating a new instance of SharedState and passing it to the parent class initializer.

Attributes:

Name Type Description
state SharedState

The shared state object used to manage

Source code in src\business_logic\program_manager.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def __init__(self):
    """
    Initializes the ProgramManager instance.

    This constructor sets up the initial state of the ProgramManager by
    creating a new instance of SharedState and passing it to the parent
    class initializer.

    Attributes:
        state (SharedState): The shared state object used to manage
        program-wide state and data.
    """
    self.state = SharedState()
    super().__init__(self.state)
manage_hour_devices(selected_ips, emit_progress=None)

Synchronizes the time on a list of devices identified by their IP addresses.

Parameters:

Name Type Description Default
selected_ips list[str]

A list of IP addresses of the devices to update.

required
emit_progress Callable

A callback function to emit progress updates. Defaults to None.

None

Returns:

Type Description
Any

The result of the update_devices_time method from the superclass.

Source code in src\business_logic\program_manager.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def manage_hour_devices(self, selected_ips: list[str], emit_progress: Callable = None):
    """
    Synchronizes the time on a list of devices identified by their IP addresses.

    Args:
        selected_ips (list[str]): A list of IP addresses of the devices to update.
        emit_progress (Callable, optional): A callback function to emit progress updates. Defaults to None.

    Returns:
        (Any): The result of the `update_devices_time` method from the superclass.
    """
    self.emit_progress: Callable = emit_progress
    self.state.reset()
    return super().update_devices_time(selected_ips)
update_device_time_of_one_device(device)

Updates the device time for a single device.

This method attempts to connect to the specified device, update its time, and handle any errors that may occur during the process. It also updates the device's error status and progress tracking.

Parameters:

Name Type Description Default
device Device

The device object containing information such as IP address, communication type, model name, and point.

required

Raises:

Type Description
ConnectionFailedError

If the connection to the device fails.

BatteryFailingError

If the device's battery is failing and its time cannot be updated.

BaseError

For any other unexpected errors, with error code 3000.

Notes
  • Uses a lock to ensure thread-safe updates to the devices_errors dictionary.
  • Updates the battery status if an outdated time error occurs.
  • Ensures the connection is properly closed in the finally block.
  • Tracks progress using the ProgressTracker class.
Source code in src\business_logic\program_manager.py
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
def update_device_time_of_one_device(self, device: Device):
    """
    Updates the device time for a single device.

    This method attempts to connect to the specified device, update its time, 
    and handle any errors that may occur during the process. It also updates 
    the device's error status and progress tracking.

    Args:
        device (Device): The device object containing information such as 
                         IP address, communication type, model name, and point.

    Raises:
        ConnectionFailedError: If the connection to the device fails.
        BatteryFailingError: If the device's battery is failing and its time 
                             cannot be updated.
        BaseError: For any other unexpected errors, with error code 3000.

    Notes:
        - Uses a lock to ensure thread-safe updates to the `devices_errors` dictionary.
        - Updates the battery status if an outdated time error occurs.
        - Ensures the connection is properly closed in the `finally` block.
        - Tracks progress using the `ProgressTracker` class.
    """
    logging.debug(f"Iniciando {device.ip}")
    try:
        try:
            conn_manager: ConnectionManager = ConnectionManager(device.ip, 4370, device.communication)
            conn_manager.connect_with_retry()
            with self.lock:
                self.devices_errors[device.ip] = { "connection failed": False }
            conn_manager.update_time()
            with self.lock:
                self.devices_errors[device.ip] = { "battery failing": False }
        except NetworkError as e:
            with self.lock:
                self.devices_errors[device.ip] = { "connection failed": True }
            raise ConnectionFailedError(device.model_name, device.point, device.ip)
        except OutdatedTimeError as e:
            with self.lock:
                self.devices_errors[device.ip] = { "battery failing": True }
            HourManager().update_battery_status(device.ip)
            raise BatteryFailingError(device.model_name, device.point, device.ip)
    except ConnectionFailedError as e:
        pass
    except BatteryFailingError as e:
        pass
    except Exception as e:
        BaseError(3000, str(e))
    finally:
        if conn_manager.is_connected():
            conn_manager.disconnect()
        ProgressTracker(self.state, self.emit_progress).update(device)
        logging.debug(f"Finalizando {device.ip}")
    return

ProgressTracker

Source code in src\business_logic\program_manager.py
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
class ProgressTracker:
    def __init__(self, state: SharedState, emit_progress: Callable):
        """
        Initializes the ProgramManager instance.

        Args:
            state (SharedState): The shared state object used to manage and share data across components.
            emit_progress (Callable): A callable function used to emit progress updates.
        """
        self.state: SharedState = state
        self.emit_progress: Callable = emit_progress

    def update(self, device: Device):
        """
        Updates the progress of processing a device and emits progress information if applicable.

        Args:
            device (Device): The device object being processed.

        Behavior:
            - Increments the count of processed devices in the current state.
            - Calculates the progress percentage based on the total devices.
            - Emits progress information including:
                - Percent progress.
                - IP address of the device being processed.
                - Number of processed devices.
                - Total number of devices.
            - Logs the progress details for debugging purposes.

        Exceptions:
            - Catches any exception that occurs during the update process and raises a `BaseError`
              with an error code of 3000 and a descriptive message.
        """
        try:
            if self.state:
                processed_devices: int = self.state.increment_processed_devices()
                if self.emit_progress:
                    progress: int = self.state.calculate_progress()
                    self.emit_progress(
                        percent_progress=progress,
                        device_progress=device.ip,
                        processed_devices=processed_devices,
                        total_devices=self.state.get_total_devices()
                    )
                    logging.debug(f"Processed: {processed_devices}/{self.state.get_total_devices()}, Progress: {progress}%")
        except Exception as e:
            BaseError(3000, f'Error actualizando el progreso: {str(e)}')
        return
__init__(state, emit_progress)

Initializes the ProgramManager instance.

Parameters:

Name Type Description Default
state SharedState

The shared state object used to manage and share data across components.

required
emit_progress Callable

A callable function used to emit progress updates.

required
Source code in src\business_logic\program_manager.py
37
38
39
40
41
42
43
44
45
46
def __init__(self, state: SharedState, emit_progress: Callable):
    """
    Initializes the ProgramManager instance.

    Args:
        state (SharedState): The shared state object used to manage and share data across components.
        emit_progress (Callable): A callable function used to emit progress updates.
    """
    self.state: SharedState = state
    self.emit_progress: Callable = emit_progress
update(device)

Updates the progress of processing a device and emits progress information if applicable.

Parameters:

Name Type Description Default
device Device

The device object being processed.

required
Behavior
  • Increments the count of processed devices in the current state.
  • Calculates the progress percentage based on the total devices.
  • Emits progress information including:
    • Percent progress.
    • IP address of the device being processed.
    • Number of processed devices.
    • Total number of devices.
  • Logs the progress details for debugging purposes.
Source code in src\business_logic\program_manager.py
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
def update(self, device: Device):
    """
    Updates the progress of processing a device and emits progress information if applicable.

    Args:
        device (Device): The device object being processed.

    Behavior:
        - Increments the count of processed devices in the current state.
        - Calculates the progress percentage based on the total devices.
        - Emits progress information including:
            - Percent progress.
            - IP address of the device being processed.
            - Number of processed devices.
            - Total number of devices.
        - Logs the progress details for debugging purposes.

    Exceptions:
        - Catches any exception that occurs during the update process and raises a `BaseError`
          with an error code of 3000 and a descriptive message.
    """
    try:
        if self.state:
            processed_devices: int = self.state.increment_processed_devices()
            if self.emit_progress:
                progress: int = self.state.calculate_progress()
                self.emit_progress(
                    percent_progress=progress,
                    device_progress=device.ip,
                    processed_devices=processed_devices,
                    total_devices=self.state.get_total_devices()
                )
                logging.debug(f"Processed: {processed_devices}/{self.state.get_total_devices()}, Progress: {progress}%")
    except Exception as e:
        BaseError(3000, f'Error actualizando el progreso: {str(e)}')
    return

RestartManager

Bases: OperationManager

Source code in src\business_logic\program_manager.py
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
class RestartManager(OperationManager):
    def __init__(self):
        """
        Initializes the ProgramManager instance.

        This constructor sets up the shared state and initializes a dictionary
        to track device errors. It also calls the superclass initializer with
        the shared state.

        Attributes:
            state (SharedState): An instance of SharedState to manage shared data.
            devices_errors (dict[str, dict[str, bool]]): A dictionary to store
                error states for devices, where the keys are device identifiers
                and the values are dictionaries mapping error types to their
                boolean statuses.
        """
        self.state = SharedState()
        self.devices_errors: dict[str, dict[str, bool]] = {}
        super().__init__(self.state)

    def restart_devices(self, selected_ips: list[str], emit_progress: Callable = None):
        """
        Restarts the devices specified by their IP addresses.
        This method clears any existing device errors, resets the state, and manages
        threads to restart the specified devices. If any errors occur during the 
        restart process, they are collected and returned.

        Args:
            selected_ips (list[str]): A list of IP addresses of the devices to restart.
            emit_progress (Callable, optional): A callable function to emit progress updates. 
                Defaults to None.

        Returns:
            (dict): A dictionary containing errors encountered during the restart process, 
                    if any. If no errors occur, an empty dictionary is returned.
        """
        self.devices_errors.clear()
        self.emit_progress: Callable = emit_progress
        self.state.reset()
        super().manage_threads_to_devices(selected_ips=selected_ips, function=self.restart_device)

        if len(self.devices_errors) > 0:
            return self.devices_errors

    def restart_device(self, device: Device):
        """
        Restart the specified device by establishing a connection, sending a restart command, 
        and handling any potential errors during the process.

        Args:
            device (Device): The device object containing details such as IP address, 
                             communication type, and model information.

        Raises:
            ConnectionFailedError: If the connection to the device fails.
            BaseError: For any other unexpected errors during the restart process.

        Notes:
            - Uses a connection manager to handle the device connection and restart operation.
            - Updates the device error state in a thread-safe manner using a lock.
            - Ensures the connection is properly closed in the `finally` block.
            - Tracks progress using the `ProgressTracker` class.
        """
        try:
            try:
                conn_manager: ConnectionManager = ConnectionManager(device.ip, 4370, device.communication)
                conn_manager.connect_with_retry()
                with self.lock:
                    self.devices_errors[device.ip] = { "connection failed": False }
                conn_manager.restart_device()
            except NetworkError as e:
                with self.lock:
                    self.devices_errors[device.ip] = { "connection failed": True }
                raise ConnectionFailedError(device.model_name, device.point, device.ip)
        except ConnectionFailedError as e:
            pass
        except Exception as e:
            BaseError(3000, str(e))
        finally:
            if conn_manager.is_connected():
                conn_manager.disconnect()
            ProgressTracker(self.state, self.emit_progress).update(device)
        return
__init__()

Initializes the ProgramManager instance.

This constructor sets up the shared state and initializes a dictionary to track device errors. It also calls the superclass initializer with the shared state.

Attributes:

Name Type Description
state SharedState

An instance of SharedState to manage shared data.

devices_errors dict[str, dict[str, bool]]

A dictionary to store error states for devices, where the keys are device identifiers and the values are dictionaries mapping error types to their boolean statuses.

Source code in src\business_logic\program_manager.py
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
def __init__(self):
    """
    Initializes the ProgramManager instance.

    This constructor sets up the shared state and initializes a dictionary
    to track device errors. It also calls the superclass initializer with
    the shared state.

    Attributes:
        state (SharedState): An instance of SharedState to manage shared data.
        devices_errors (dict[str, dict[str, bool]]): A dictionary to store
            error states for devices, where the keys are device identifiers
            and the values are dictionaries mapping error types to their
            boolean statuses.
    """
    self.state = SharedState()
    self.devices_errors: dict[str, dict[str, bool]] = {}
    super().__init__(self.state)
restart_device(device)

Restart the specified device by establishing a connection, sending a restart command, and handling any potential errors during the process.

Parameters:

Name Type Description Default
device Device

The device object containing details such as IP address, communication type, and model information.

required

Raises:

Type Description
ConnectionFailedError

If the connection to the device fails.

BaseError

For any other unexpected errors during the restart process.

Notes
  • Uses a connection manager to handle the device connection and restart operation.
  • Updates the device error state in a thread-safe manner using a lock.
  • Ensures the connection is properly closed in the finally block.
  • Tracks progress using the ProgressTracker class.
Source code in src\business_logic\program_manager.py
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
def restart_device(self, device: Device):
    """
    Restart the specified device by establishing a connection, sending a restart command, 
    and handling any potential errors during the process.

    Args:
        device (Device): The device object containing details such as IP address, 
                         communication type, and model information.

    Raises:
        ConnectionFailedError: If the connection to the device fails.
        BaseError: For any other unexpected errors during the restart process.

    Notes:
        - Uses a connection manager to handle the device connection and restart operation.
        - Updates the device error state in a thread-safe manner using a lock.
        - Ensures the connection is properly closed in the `finally` block.
        - Tracks progress using the `ProgressTracker` class.
    """
    try:
        try:
            conn_manager: ConnectionManager = ConnectionManager(device.ip, 4370, device.communication)
            conn_manager.connect_with_retry()
            with self.lock:
                self.devices_errors[device.ip] = { "connection failed": False }
            conn_manager.restart_device()
        except NetworkError as e:
            with self.lock:
                self.devices_errors[device.ip] = { "connection failed": True }
            raise ConnectionFailedError(device.model_name, device.point, device.ip)
    except ConnectionFailedError as e:
        pass
    except Exception as e:
        BaseError(3000, str(e))
    finally:
        if conn_manager.is_connected():
            conn_manager.disconnect()
        ProgressTracker(self.state, self.emit_progress).update(device)
    return
restart_devices(selected_ips, emit_progress=None)

Restarts the devices specified by their IP addresses. This method clears any existing device errors, resets the state, and manages threads to restart the specified devices. If any errors occur during the restart process, they are collected and returned.

Parameters:

Name Type Description Default
selected_ips list[str]

A list of IP addresses of the devices to restart.

required
emit_progress Callable

A callable function to emit progress updates. Defaults to None.

None

Returns:

Type Description
dict

A dictionary containing errors encountered during the restart process, if any. If no errors occur, an empty dictionary is returned.

Source code in src\business_logic\program_manager.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
def restart_devices(self, selected_ips: list[str], emit_progress: Callable = None):
    """
    Restarts the devices specified by their IP addresses.
    This method clears any existing device errors, resets the state, and manages
    threads to restart the specified devices. If any errors occur during the 
    restart process, they are collected and returned.

    Args:
        selected_ips (list[str]): A list of IP addresses of the devices to restart.
        emit_progress (Callable, optional): A callable function to emit progress updates. 
            Defaults to None.

    Returns:
        (dict): A dictionary containing errors encountered during the restart process, 
                if any. If no errors occur, an empty dictionary is returned.
    """
    self.devices_errors.clear()
    self.emit_progress: Callable = emit_progress
    self.state.reset()
    super().manage_threads_to_devices(selected_ips=selected_ips, function=self.restart_device)

    if len(self.devices_errors) > 0:
        return self.devices_errors

ui

base_dialog

BaseDialog

Bases: QDialog

Source code in src\ui\base_dialog.py
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
class BaseDialog(QDialog):
    def __init__(self, parent=None, window_title=""):
        """
        Initializes the base dialog window with the specified parent and window title.

        Args:
            parent (QWidget, optional): The parent widget for the dialog. Defaults to None.
            window_title (str, optional): The title of the dialog window. Defaults to an empty string.

        Raises:
            BaseError: If an exception occurs during initialization, it raises a BaseError with code 3501
                       and the exception message.
        """
        try:
            super().__init__(parent)
            self.setWindowTitle(window_title)

            # Set window icon
            self.file_path_resources = os.path.join(find_marker_directory("resources"), "resources")
            self.file_path_icon = os.path.join(self.file_path_resources, "fingerprint.ico")
            self.setWindowIcon(QIcon(self.file_path_icon))

            # Allow minimizing the window
            self.setWindowFlag(Qt.WindowMinimizeButtonHint, True)
            # Allow maximizing the window
            self.setWindowFlag(Qt.WindowMaximizeButtonHint, True)

            self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
        except Exception as e:
            raise BaseError(3501, str(e))

    def init_ui(self):
        pass

    def adjust_size_to_table(self):
        """
        Adjusts the size of the dialog window to fit the content of the table widget.
        This method resizes the columns of the table widget to fit their contents and calculates
        the required width and height of the table based on its content. It ensures that the
        dialog window does not exceed the available screen height, applying a margin if necessary.
        Finally, it resizes the dialog window to accommodate the adjusted table dimensions.

        Notes:
            - An extra width adjustment is added to account for margins and the button bar.
            - The height is capped to fit within the available screen height minus a margin.
        """
        # Adjust columns based on the content
        self.table_widget.resizeColumnsToContents()

        # Get the content size of the table (width and height)
        table_width = self.table_widget.horizontalHeader().length()
        table_height = self.table_widget.verticalHeader().length() + self.table_widget.rowCount() * self.table_widget.rowHeight(0)

        max_height = self.screen().availableGeometry().height()
        if table_height > max_height:
            table_height = max_height-50

        # Adjust the main window size
        self.resize(table_width + 120, table_height)  # Extra adjustment for margins and button bar

    def center_window(self):
        """
        Centers the window on the current screen.

        This method calculates the available geometry of the current screen
        and moves the window to the center of the screen.

        Returns:
            None
        """
        screen = self.screen()  # Get the current screen
        screen_rect = screen.availableGeometry()  # Get screen available geometry
        self.move(screen_rect.center() - self.rect().center())  # Move to center
__init__(parent=None, window_title='')

Initializes the base dialog window with the specified parent and window title.

Parameters:

Name Type Description Default
parent QWidget

The parent widget for the dialog. Defaults to None.

None
window_title str

The title of the dialog window. Defaults to an empty string.

''

Raises:

Type Description
BaseError

If an exception occurs during initialization, it raises a BaseError with code 3501 and the exception message.

Source code in src\ui\base_dialog.py
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
def __init__(self, parent=None, window_title=""):
    """
    Initializes the base dialog window with the specified parent and window title.

    Args:
        parent (QWidget, optional): The parent widget for the dialog. Defaults to None.
        window_title (str, optional): The title of the dialog window. Defaults to an empty string.

    Raises:
        BaseError: If an exception occurs during initialization, it raises a BaseError with code 3501
                   and the exception message.
    """
    try:
        super().__init__(parent)
        self.setWindowTitle(window_title)

        # Set window icon
        self.file_path_resources = os.path.join(find_marker_directory("resources"), "resources")
        self.file_path_icon = os.path.join(self.file_path_resources, "fingerprint.ico")
        self.setWindowIcon(QIcon(self.file_path_icon))

        # Allow minimizing the window
        self.setWindowFlag(Qt.WindowMinimizeButtonHint, True)
        # Allow maximizing the window
        self.setWindowFlag(Qt.WindowMaximizeButtonHint, True)

        self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)
    except Exception as e:
        raise BaseError(3501, str(e))
adjust_size_to_table()

Adjusts the size of the dialog window to fit the content of the table widget. This method resizes the columns of the table widget to fit their contents and calculates the required width and height of the table based on its content. It ensures that the dialog window does not exceed the available screen height, applying a margin if necessary. Finally, it resizes the dialog window to accommodate the adjusted table dimensions.

Notes
  • An extra width adjustment is added to account for margins and the button bar.
  • The height is capped to fit within the available screen height minus a margin.
Source code in src\ui\base_dialog.py
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
def adjust_size_to_table(self):
    """
    Adjusts the size of the dialog window to fit the content of the table widget.
    This method resizes the columns of the table widget to fit their contents and calculates
    the required width and height of the table based on its content. It ensures that the
    dialog window does not exceed the available screen height, applying a margin if necessary.
    Finally, it resizes the dialog window to accommodate the adjusted table dimensions.

    Notes:
        - An extra width adjustment is added to account for margins and the button bar.
        - The height is capped to fit within the available screen height minus a margin.
    """
    # Adjust columns based on the content
    self.table_widget.resizeColumnsToContents()

    # Get the content size of the table (width and height)
    table_width = self.table_widget.horizontalHeader().length()
    table_height = self.table_widget.verticalHeader().length() + self.table_widget.rowCount() * self.table_widget.rowHeight(0)

    max_height = self.screen().availableGeometry().height()
    if table_height > max_height:
        table_height = max_height-50

    # Adjust the main window size
    self.resize(table_width + 120, table_height)  # Extra adjustment for margins and button bar
center_window()

Centers the window on the current screen.

This method calculates the available geometry of the current screen and moves the window to the center of the screen.

Returns:

Type Description

None

Source code in src\ui\base_dialog.py
85
86
87
88
89
90
91
92
93
94
95
96
97
def center_window(self):
    """
    Centers the window on the current screen.

    This method calculates the available geometry of the current screen
    and moves the window to the center of the screen.

    Returns:
        None
    """
    screen = self.screen()  # Get the current screen
    screen_rect = screen.availableGeometry()  # Get screen available geometry
    self.move(screen_rect.center() - self.rect().center())  # Move to center

base_select_devices_dialog

SelectDevicesDialog

Bases: BaseDialog

Source code in src\ui\base_select_devices_dialog.py
 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
class SelectDevicesDialog(BaseDialog):
    def __init__(self, parent=None, op_function=None, window_title=""):
        """
        Initializes the BaseSelectDevicesDialog class.

        Args:
            parent (QWidget, optional): The parent widget for this dialog. Defaults to None.
            op_function (callable, optional): A callable operation function to be executed. Defaults to None.
            window_title (str, optional): The title of the dialog window. Defaults to an empty string.

        Attributes:
            op_function (callable): Stores the operation function passed as an argument.
            file_path (str): The file path to the "info_devices.txt" file, located in the current working directory.
            data (list): A list to store device-related data.

        Raises:
            BaseError: If an exception occurs during initialization, it raises a BaseError with code 3501 and the error message.
        """
        try:
            super().__init__(parent, window_title=window_title)
            self.op_function = op_function
            # File path containing device information
            self.file_path = os.path.join(os.getcwd(), "info_devices.txt")
            self.data = []
        except Exception as e:
            raise BaseError(3501, str(e))

    def init_ui(self, header_labels):
        """
        Initializes the user interface for the device selection dialog.

        Args:
            header_labels (list): A list of strings representing the column headers for the table.

        UI Components:
            - QVBoxLayout: Main layout for the dialog.
            - QTableWidget: Table widget to display devices with configurable columns and sorting enabled.
            - QPushButton: Buttons for updating data, selecting all rows, and deselecting all rows.
            - QLabel: Label to display a message when data is being updated.
            - QProgressBar: Progress bar to indicate the progress of data updates.
            - ComboBoxDelegate: Delegate for the communication column to provide a combo box UI.

        Features:
            - Multi-selection enabled for the table with full row selection.
            - Buttons to select all rows, deselect all rows, and perform operations on selected devices.
            - Progress bar and label for visual feedback during data updates.
            - Automatic loading of initial device data into the table.

        Raises:
            BaseError: If an exception occurs during the initialization process, it raises a BaseError with code 3501.
        """
        try:
            layout = QVBoxLayout(self)

            from PyQt5.QtWidgets import QHBoxLayout

            self.inputs_widget = QWidget()
            self.inputs_layout = QHBoxLayout(self.inputs_widget)

            self.label_timeout = QLabel("Tiempo de Espera:", self)
            self.spin_timeout = QSpinBox(self)
            self.spin_timeout.setMinimum(0)
            self.spin_timeout.setMaximum(999)
            config.read(os.path.join(find_root_directory(), 'config.ini'))
            self.timeout = config.getint('Network_config', 'timeout')
            if self.timeout:
                self.spin_timeout.setValue(self.timeout)
            else:
                self.spin_timeout.setValue(15)
            self.spin_timeout.valueChanged.connect(self.on_change_timeout)

            self.label_retries = QLabel("Reintentos:", self)
            self.spin_retries = QSpinBox(self)
            self.spin_retries.setMinimum(0)
            self.spin_retries.setMaximum(10)
            self.retries = config.getint('Network_config', 'retry_connection')
            if self.retries:
                self.spin_retries.setValue(self.retries)
            else:
                self.spin_retries.setValue(3)
            self.spin_retries.valueChanged.connect(self.on_change_retries)

            self.inputs_layout.addWidget(self.label_timeout)
            self.inputs_layout.addWidget(self.spin_timeout)
            self.inputs_layout.addWidget(self.label_retries)
            self.inputs_layout.addWidget(self.spin_retries)

            layout.addWidget(self.inputs_widget)

            # Table for show devices
            self.table_widget = QTableWidget()
            self.table_widget.setColumnCount(len(header_labels))
            self.table_widget.setHorizontalHeaderLabels(header_labels)
            self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
            self.table_widget.horizontalHeader().setStretchLastSection(True)
            self.table_widget.setSortingEnabled(True)

            # Enable full row selection with multi-selection (each click toggles selection)
            self.table_widget.setSelectionBehavior(QTableWidget.SelectRows)
            self.table_widget.setSelectionMode(QTableWidget.MultiSelection)
            layout.addWidget(self.table_widget)

            self.button_layout = QHBoxLayout()

            # Button for update data (operate for selected devices)
            self.btn_update = QPushButton("Actualizar datos")
            self.btn_update.clicked.connect(self.operation_with_selected_ips)
            self.button_layout.addWidget(self.btn_update)

            self.btn_activate_all = QPushButton("Seleccionar todo", self)
            self.btn_activate_all.clicked.connect(self.select_all_rows)
            self.button_layout.addWidget(self.btn_activate_all)

            self.btn_deactivate_all = QPushButton("Deseleccionar todo", self)
            self.btn_deactivate_all.clicked.connect(self.deselect_all_rows)
            self.button_layout.addWidget(self.btn_deactivate_all)

            layout.addLayout(self.button_layout)

            self.label_updating = QLabel("Actualizando datos...", self)
            self.label_updating.setAlignment(Qt.AlignCenter)
            self.label_updating.setVisible(False)
            layout.addWidget(self.label_updating)

            # Progress bar
            self.progress_bar = QProgressBar(self)
            self.progress_bar.setMinimum(0)
            self.progress_bar.setMaximum(100)
            self.progress_bar.setValue(0)
            self.progress_bar.setVisible(False)
            layout.addWidget(self.progress_bar)

            self.setLayout(layout)

            # Set delegate for communication column (UI only)
            combo_box_delegate = ComboBoxDelegate(self.table_widget)
            self.table_widget.setItemDelegateForColumn(5, combo_box_delegate)

            # Load initial device data
            self.load_data()
        except Exception as e:
            raise BaseError(3501, str(e))

    def on_change_timeout(self, value):
        if value != self.timeout:
            self.timeout = value
            config.set('Network_config', 'timeout', str(self.spin_timeout.value()))
            with open(os.path.join(find_root_directory(), 'config.ini'), 'w') as configfile:
                config.write(configfile)

    def on_change_retries(self, value):
        if value != self.retries:
            self.retries = value
            config.set('Network_config', 'retry_connection', str(self.spin_retries.value()))
            with open(os.path.join(find_root_directory(), 'config.ini'), 'w') as configfile:
                config.write(configfile)

    def load_data(self):
        """
        Loads data from a file specified by `self.file_path` and filters it based on 
        specific criteria. The method reads each line of the file, splits it into 
        parts, and checks if the last part (active status) indicates a truthy value 
        (e.g., 'true', '1', 'yes', 'verdadero', 'si'). If the criteria are met, 
        selected fields are extracted and appended to `self.data`.

        After processing the file, the data is loaded into a table using 
        `self.load_data_into_table()`.

        Raises:
            BaseError: If an exception occurs during file reading or processing, 
                       it raises a `BaseError` with code 3001 and the exception message.
        """
        try:
            self.data = []
            with open(self.file_path, "r") as file:
                for line in file:
                    parts = line.strip().split(" - ")
                    if len(parts) == 8 and parts[7].lower() in ['true', '1', 'yes', 'verdadero', 'si']:
                        # Unpack only the required fields for display
                        district, model, point, ip, id_val, communication, battery, active = parts
                        self.data.append((district, model, point, ip, id_val, communication))
            self.load_data_into_table()
        except Exception as e:
            raise BaseError(3001, str(e))

    def load_data_into_table(self):
        """
        Populates the table widget with data from the `self.data` attribute.

        This method clears any existing rows in the table widget and inserts new rows
        based on the records in `self.data`. Each cell is populated with non-editable
        items, and the background color of the cells is set to light gray. After
        populating the table, the method adjusts the table's size to fit its content.

        Steps:

        1. Clears all rows in the table widget.
        2. Iterates through the records in `self.data` and inserts rows into the table.
        3. Creates non-editable table items for each cell and sets their background color.
        4. Adjusts the table size to fit the content.

        Note:
            - The `self.data` attribute is expected to be an iterable of records, where
              each record is an iterable of values corresponding to table columns.
            - The table widget is assumed to be an instance of QTableWidget.

        """
        self.table_widget.setRowCount(0)
        for row_index, record in enumerate(self.data):
            self.table_widget.insertRow(row_index)
            # Create non-editable items for each column
            for col_index, value in enumerate(record):
                item = QTableWidgetItem(str(value))
                item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                item.setBackground(QColor(Qt.lightGray))
                self.table_widget.setItem(row_index, col_index, item)
        self.adjust_size_to_table()

    def operation_with_selected_ips(self):
        """
        Handles operations with the selected IP addresses from the table widget.
        This method retrieves the selected rows from the table widget, extracts the IP addresses
        from a specific column, and performs an operation on the selected IPs using a separate thread.
        It also updates the UI to reflect the ongoing operation and handles progress updates.

        Raises:
            Exception: If no devices are selected, a message box is displayed, and an exception is raised.
            BaseError: If any other exception occurs during the execution of the method.

        Attributes:
            selected_ips (list[str]): A list to store the IP addresses of the selected devices.

        UI Updates:
            - Hides the table widget.
            - Sorts the table widget by the IP column in descending order.
            - Updates labels and buttons to indicate the operation is in progress.
            - Displays a progress bar to show the operation's progress.

        Threads:
            - Creates and starts an `OperationThread` to perform the operation on the selected IPs.
            - Connects thread signals to appropriate methods for progress updates, termination, and cleanup.
        """
        try:        
            self.selected_ips: list[str] = []
            # Retrieve selected rows via the selection model
            for index in self.table_widget.selectionModel().selectedRows():
                row = index.row()
                ip = self.table_widget.item(row, 3).text()  # Column 3 holds the IP
                self.selected_ips.append(ip)

            if not self.selected_ips:
                QMessageBox.information(self, "Sin selección", "No se seleccionaron dispositivos")
                raise Exception("No se seleccionaron dispositivos")
            else:
                self.inputs_widget.setVisible(False)
                self.table_widget.setVisible(False)
                self.table_widget.sortByColumn(3, Qt.DescendingOrder)
                self.label_updating.setText("Actualizando datos...")
                self.btn_update.setVisible(False)
                self.btn_activate_all.setVisible(False)
                self.btn_deactivate_all.setVisible(False)
                self.label_updating.setVisible(True)
                self.progress_bar.setVisible(True)
                self.progress_bar.setValue(0)
                #logging.debug(f"Dispositivos seleccionados: {self.selected_ips}")
                self.op_thread = OperationThread(self.op_function, self.selected_ips)
                self.op_thread.progress_updated.connect(self.update_progress)
                self.op_thread.op_terminate.connect(self.op_terminate)
                self.op_thread.finished.connect(self.cleanup_thread)
                self.op_thread.start()
        except Exception as e:
            raise BaseError(3000, str(e))

    def cleanup_thread(self):
        """
        Cleans up the operation thread by scheduling its deletion.

        This method ensures that the `op_thread` object is properly deleted
        using the `deleteLater()` method, which schedules the object for
        deletion once it is safe to do so, typically after all pending events
        have been processed.
        """
        self.op_thread.deleteLater()

    def op_terminate(self, devices=None):
        """
        Resets the UI components to their default visible states after an operation.

        Args:
            devices (optional): A parameter for devices, currently unused in the method.
        """
        self.inputs_widget.setVisible(True)
        self.btn_update.setVisible(True)
        self.btn_activate_all.setVisible(True)
        self.btn_deactivate_all.setVisible(True)
        self.table_widget.setVisible(True)
        self.label_updating.setVisible(False)
        self.progress_bar.setVisible(False)

    def column_exists(self, column_name):
        """
        Checks if a column with the specified name exists in the table widget.

        Args:
            column_name (str): The name of the column to check for existence.

        Returns:
            (bool): True if the column exists, False otherwise.
        """
        headers = [self.table_widget.horizontalHeaderItem(i).text() for i in range(self.table_widget.columnCount())]
        return column_name in headers

    def get_column_number(self, column_name):
        """
        Retrieves the index of a column in the table widget based on the column's name.

        Args:
            column_name (str): The name of the column to search for.

        Returns:
            (int): The index of the column if found, otherwise -1.
        """
        for i in range(self.table_widget.columnCount()):
            if self.table_widget.horizontalHeaderItem(i).text() == column_name:
                return i
        return -1

    def update_progress(self, percent_progress, device_progress, processed_devices, total_devices):
        """
        Updates the progress bar and status label with the current progress of device processing.

        Args:
            percent_progress (int): The overall progress percentage to be displayed on the progress bar.
            device_progress (str): A message indicating the status of the last connection attempt.
            processed_devices (int): The number of devices that have been processed so far.
            total_devices (int): The total number of devices to be processed.

        Returns:
            None
        """
        if percent_progress and device_progress:
            self.progress_bar.setValue(percent_progress)
            self.label_updating.setText(f"Último intento de conexión: {device_progress}\n{processed_devices}/{total_devices} dispositivos")

    def select_all_rows(self):
        """
        Selects all rows in the table widget that are not already selected.
        This method retrieves the list of currently selected rows in the table widget
        and iterates through all rows in the table. If a row is not already selected,
        it selects the row.

        Note:
            - The method assumes that `self.table_widget` is a valid QTableWidget or
              similar widget with `selectionModel`, `selectedRows`, `rowCount`, and
              `selectRow` methods.
        """
        selected_rows = [index.row() for index in self.table_widget.selectionModel().selectedRows()]

        row_count = self.table_widget.rowCount()
        for row in range(row_count):
            if row not in selected_rows:
                self.table_widget.selectRow(row)

    def deselect_all_rows(self):
        """
        Deselects all rows in the table widget.

        This method clears the current selection in the table widget,
        ensuring that no rows remain selected.
        """
        self.table_widget.clearSelection()

    def ensure_column_exists(self, column_name):
        """
        Ensures that a column with the specified name exists in the table widget. 
        If the column does not exist, it creates a new column with the given name.
        If the column already exists, it retrieves its index.

        Args:
            column_name (str): The name of the column to ensure exists.

        Returns:
            (int): The index of the column in the table widget.
        """
        if not self.column_exists(column_name):
            # Add a new column to the table
            actual_column = self.table_widget.columnCount()
            self.table_widget.setColumnCount(actual_column + 1)
            self.table_widget.setHorizontalHeaderItem(actual_column, QTableWidgetItem(column_name))
        else:
            # Get the column number
            actual_column = self.get_column_number(column_name)
        return actual_column
__init__(parent=None, op_function=None, window_title='')

Initializes the BaseSelectDevicesDialog class.

Parameters:

Name Type Description Default
parent QWidget

The parent widget for this dialog. Defaults to None.

None
op_function callable

A callable operation function to be executed. Defaults to None.

None
window_title str

The title of the dialog window. Defaults to an empty string.

''

Attributes:

Name Type Description
op_function callable

Stores the operation function passed as an argument.

file_path str

The file path to the "info_devices.txt" file, located in the current working directory.

data list

A list to store device-related data.

Raises:

Type Description
BaseError

If an exception occurs during initialization, it raises a BaseError with code 3501 and the error message.

Source code in src\ui\base_select_devices_dialog.py
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
def __init__(self, parent=None, op_function=None, window_title=""):
    """
    Initializes the BaseSelectDevicesDialog class.

    Args:
        parent (QWidget, optional): The parent widget for this dialog. Defaults to None.
        op_function (callable, optional): A callable operation function to be executed. Defaults to None.
        window_title (str, optional): The title of the dialog window. Defaults to an empty string.

    Attributes:
        op_function (callable): Stores the operation function passed as an argument.
        file_path (str): The file path to the "info_devices.txt" file, located in the current working directory.
        data (list): A list to store device-related data.

    Raises:
        BaseError: If an exception occurs during initialization, it raises a BaseError with code 3501 and the error message.
    """
    try:
        super().__init__(parent, window_title=window_title)
        self.op_function = op_function
        # File path containing device information
        self.file_path = os.path.join(os.getcwd(), "info_devices.txt")
        self.data = []
    except Exception as e:
        raise BaseError(3501, str(e))
cleanup_thread()

Cleans up the operation thread by scheduling its deletion.

This method ensures that the op_thread object is properly deleted using the deleteLater() method, which schedules the object for deletion once it is safe to do so, typically after all pending events have been processed.

Source code in src\ui\base_select_devices_dialog.py
289
290
291
292
293
294
295
296
297
298
def cleanup_thread(self):
    """
    Cleans up the operation thread by scheduling its deletion.

    This method ensures that the `op_thread` object is properly deleted
    using the `deleteLater()` method, which schedules the object for
    deletion once it is safe to do so, typically after all pending events
    have been processed.
    """
    self.op_thread.deleteLater()
column_exists(column_name)

Checks if a column with the specified name exists in the table widget.

Parameters:

Name Type Description Default
column_name str

The name of the column to check for existence.

required

Returns:

Type Description
bool

True if the column exists, False otherwise.

Source code in src\ui\base_select_devices_dialog.py
315
316
317
318
319
320
321
322
323
324
325
326
def column_exists(self, column_name):
    """
    Checks if a column with the specified name exists in the table widget.

    Args:
        column_name (str): The name of the column to check for existence.

    Returns:
        (bool): True if the column exists, False otherwise.
    """
    headers = [self.table_widget.horizontalHeaderItem(i).text() for i in range(self.table_widget.columnCount())]
    return column_name in headers
deselect_all_rows()

Deselects all rows in the table widget.

This method clears the current selection in the table widget, ensuring that no rows remain selected.

Source code in src\ui\base_select_devices_dialog.py
379
380
381
382
383
384
385
386
def deselect_all_rows(self):
    """
    Deselects all rows in the table widget.

    This method clears the current selection in the table widget,
    ensuring that no rows remain selected.
    """
    self.table_widget.clearSelection()
ensure_column_exists(column_name)

Ensures that a column with the specified name exists in the table widget. If the column does not exist, it creates a new column with the given name. If the column already exists, it retrieves its index.

Parameters:

Name Type Description Default
column_name str

The name of the column to ensure exists.

required

Returns:

Type Description
int

The index of the column in the table widget.

Source code in src\ui\base_select_devices_dialog.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
def ensure_column_exists(self, column_name):
    """
    Ensures that a column with the specified name exists in the table widget. 
    If the column does not exist, it creates a new column with the given name.
    If the column already exists, it retrieves its index.

    Args:
        column_name (str): The name of the column to ensure exists.

    Returns:
        (int): The index of the column in the table widget.
    """
    if not self.column_exists(column_name):
        # Add a new column to the table
        actual_column = self.table_widget.columnCount()
        self.table_widget.setColumnCount(actual_column + 1)
        self.table_widget.setHorizontalHeaderItem(actual_column, QTableWidgetItem(column_name))
    else:
        # Get the column number
        actual_column = self.get_column_number(column_name)
    return actual_column
get_column_number(column_name)

Retrieves the index of a column in the table widget based on the column's name.

Parameters:

Name Type Description Default
column_name str

The name of the column to search for.

required

Returns:

Type Description
int

The index of the column if found, otherwise -1.

Source code in src\ui\base_select_devices_dialog.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def get_column_number(self, column_name):
    """
    Retrieves the index of a column in the table widget based on the column's name.

    Args:
        column_name (str): The name of the column to search for.

    Returns:
        (int): The index of the column if found, otherwise -1.
    """
    for i in range(self.table_widget.columnCount()):
        if self.table_widget.horizontalHeaderItem(i).text() == column_name:
            return i
    return -1
init_ui(header_labels)

Initializes the user interface for the device selection dialog.

Parameters:

Name Type Description Default
header_labels list

A list of strings representing the column headers for the table.

required
UI Components
  • QVBoxLayout: Main layout for the dialog.
  • QTableWidget: Table widget to display devices with configurable columns and sorting enabled.
  • QPushButton: Buttons for updating data, selecting all rows, and deselecting all rows.
  • QLabel: Label to display a message when data is being updated.
  • QProgressBar: Progress bar to indicate the progress of data updates.
  • ComboBoxDelegate: Delegate for the communication column to provide a combo box UI.
Features
  • Multi-selection enabled for the table with full row selection.
  • Buttons to select all rows, deselect all rows, and perform operations on selected devices.
  • Progress bar and label for visual feedback during data updates.
  • Automatic loading of initial device data into the table.

Raises:

Type Description
BaseError

If an exception occurs during the initialization process, it raises a BaseError with code 3501.

Source code in src\ui\base_select_devices_dialog.py
 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
def init_ui(self, header_labels):
    """
    Initializes the user interface for the device selection dialog.

    Args:
        header_labels (list): A list of strings representing the column headers for the table.

    UI Components:
        - QVBoxLayout: Main layout for the dialog.
        - QTableWidget: Table widget to display devices with configurable columns and sorting enabled.
        - QPushButton: Buttons for updating data, selecting all rows, and deselecting all rows.
        - QLabel: Label to display a message when data is being updated.
        - QProgressBar: Progress bar to indicate the progress of data updates.
        - ComboBoxDelegate: Delegate for the communication column to provide a combo box UI.

    Features:
        - Multi-selection enabled for the table with full row selection.
        - Buttons to select all rows, deselect all rows, and perform operations on selected devices.
        - Progress bar and label for visual feedback during data updates.
        - Automatic loading of initial device data into the table.

    Raises:
        BaseError: If an exception occurs during the initialization process, it raises a BaseError with code 3501.
    """
    try:
        layout = QVBoxLayout(self)

        from PyQt5.QtWidgets import QHBoxLayout

        self.inputs_widget = QWidget()
        self.inputs_layout = QHBoxLayout(self.inputs_widget)

        self.label_timeout = QLabel("Tiempo de Espera:", self)
        self.spin_timeout = QSpinBox(self)
        self.spin_timeout.setMinimum(0)
        self.spin_timeout.setMaximum(999)
        config.read(os.path.join(find_root_directory(), 'config.ini'))
        self.timeout = config.getint('Network_config', 'timeout')
        if self.timeout:
            self.spin_timeout.setValue(self.timeout)
        else:
            self.spin_timeout.setValue(15)
        self.spin_timeout.valueChanged.connect(self.on_change_timeout)

        self.label_retries = QLabel("Reintentos:", self)
        self.spin_retries = QSpinBox(self)
        self.spin_retries.setMinimum(0)
        self.spin_retries.setMaximum(10)
        self.retries = config.getint('Network_config', 'retry_connection')
        if self.retries:
            self.spin_retries.setValue(self.retries)
        else:
            self.spin_retries.setValue(3)
        self.spin_retries.valueChanged.connect(self.on_change_retries)

        self.inputs_layout.addWidget(self.label_timeout)
        self.inputs_layout.addWidget(self.spin_timeout)
        self.inputs_layout.addWidget(self.label_retries)
        self.inputs_layout.addWidget(self.spin_retries)

        layout.addWidget(self.inputs_widget)

        # Table for show devices
        self.table_widget = QTableWidget()
        self.table_widget.setColumnCount(len(header_labels))
        self.table_widget.setHorizontalHeaderLabels(header_labels)
        self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.table_widget.horizontalHeader().setStretchLastSection(True)
        self.table_widget.setSortingEnabled(True)

        # Enable full row selection with multi-selection (each click toggles selection)
        self.table_widget.setSelectionBehavior(QTableWidget.SelectRows)
        self.table_widget.setSelectionMode(QTableWidget.MultiSelection)
        layout.addWidget(self.table_widget)

        self.button_layout = QHBoxLayout()

        # Button for update data (operate for selected devices)
        self.btn_update = QPushButton("Actualizar datos")
        self.btn_update.clicked.connect(self.operation_with_selected_ips)
        self.button_layout.addWidget(self.btn_update)

        self.btn_activate_all = QPushButton("Seleccionar todo", self)
        self.btn_activate_all.clicked.connect(self.select_all_rows)
        self.button_layout.addWidget(self.btn_activate_all)

        self.btn_deactivate_all = QPushButton("Deseleccionar todo", self)
        self.btn_deactivate_all.clicked.connect(self.deselect_all_rows)
        self.button_layout.addWidget(self.btn_deactivate_all)

        layout.addLayout(self.button_layout)

        self.label_updating = QLabel("Actualizando datos...", self)
        self.label_updating.setAlignment(Qt.AlignCenter)
        self.label_updating.setVisible(False)
        layout.addWidget(self.label_updating)

        # Progress bar
        self.progress_bar = QProgressBar(self)
        self.progress_bar.setMinimum(0)
        self.progress_bar.setMaximum(100)
        self.progress_bar.setValue(0)
        self.progress_bar.setVisible(False)
        layout.addWidget(self.progress_bar)

        self.setLayout(layout)

        # Set delegate for communication column (UI only)
        combo_box_delegate = ComboBoxDelegate(self.table_widget)
        self.table_widget.setItemDelegateForColumn(5, combo_box_delegate)

        # Load initial device data
        self.load_data()
    except Exception as e:
        raise BaseError(3501, str(e))
load_data()

Loads data from a file specified by self.file_path and filters it based on specific criteria. The method reads each line of the file, splits it into parts, and checks if the last part (active status) indicates a truthy value (e.g., 'true', '1', 'yes', 'verdadero', 'si'). If the criteria are met, selected fields are extracted and appended to self.data.

After processing the file, the data is loaded into a table using self.load_data_into_table().

Raises:

Type Description
BaseError

If an exception occurs during file reading or processing, it raises a BaseError with code 3001 and the exception message.

Source code in src\ui\base_select_devices_dialog.py
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
def load_data(self):
    """
    Loads data from a file specified by `self.file_path` and filters it based on 
    specific criteria. The method reads each line of the file, splits it into 
    parts, and checks if the last part (active status) indicates a truthy value 
    (e.g., 'true', '1', 'yes', 'verdadero', 'si'). If the criteria are met, 
    selected fields are extracted and appended to `self.data`.

    After processing the file, the data is loaded into a table using 
    `self.load_data_into_table()`.

    Raises:
        BaseError: If an exception occurs during file reading or processing, 
                   it raises a `BaseError` with code 3001 and the exception message.
    """
    try:
        self.data = []
        with open(self.file_path, "r") as file:
            for line in file:
                parts = line.strip().split(" - ")
                if len(parts) == 8 and parts[7].lower() in ['true', '1', 'yes', 'verdadero', 'si']:
                    # Unpack only the required fields for display
                    district, model, point, ip, id_val, communication, battery, active = parts
                    self.data.append((district, model, point, ip, id_val, communication))
        self.load_data_into_table()
    except Exception as e:
        raise BaseError(3001, str(e))
load_data_into_table()

Populates the table widget with data from the self.data attribute.

This method clears any existing rows in the table widget and inserts new rows based on the records in self.data. Each cell is populated with non-editable items, and the background color of the cells is set to light gray. After populating the table, the method adjusts the table's size to fit its content.

Steps:

  1. Clears all rows in the table widget.
  2. Iterates through the records in self.data and inserts rows into the table.
  3. Creates non-editable table items for each cell and sets their background color.
  4. Adjusts the table size to fit the content.
Note
  • The self.data attribute is expected to be an iterable of records, where each record is an iterable of values corresponding to table columns.
  • The table widget is assumed to be an instance of QTableWidget.
Source code in src\ui\base_select_devices_dialog.py
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
def load_data_into_table(self):
    """
    Populates the table widget with data from the `self.data` attribute.

    This method clears any existing rows in the table widget and inserts new rows
    based on the records in `self.data`. Each cell is populated with non-editable
    items, and the background color of the cells is set to light gray. After
    populating the table, the method adjusts the table's size to fit its content.

    Steps:

    1. Clears all rows in the table widget.
    2. Iterates through the records in `self.data` and inserts rows into the table.
    3. Creates non-editable table items for each cell and sets their background color.
    4. Adjusts the table size to fit the content.

    Note:
        - The `self.data` attribute is expected to be an iterable of records, where
          each record is an iterable of values corresponding to table columns.
        - The table widget is assumed to be an instance of QTableWidget.

    """
    self.table_widget.setRowCount(0)
    for row_index, record in enumerate(self.data):
        self.table_widget.insertRow(row_index)
        # Create non-editable items for each column
        for col_index, value in enumerate(record):
            item = QTableWidgetItem(str(value))
            item.setFlags(item.flags() & ~Qt.ItemIsEditable)
            item.setBackground(QColor(Qt.lightGray))
            self.table_widget.setItem(row_index, col_index, item)
    self.adjust_size_to_table()
op_terminate(devices=None)

Resets the UI components to their default visible states after an operation.

Parameters:

Name Type Description Default
devices optional

A parameter for devices, currently unused in the method.

None
Source code in src\ui\base_select_devices_dialog.py
300
301
302
303
304
305
306
307
308
309
310
311
312
313
def op_terminate(self, devices=None):
    """
    Resets the UI components to their default visible states after an operation.

    Args:
        devices (optional): A parameter for devices, currently unused in the method.
    """
    self.inputs_widget.setVisible(True)
    self.btn_update.setVisible(True)
    self.btn_activate_all.setVisible(True)
    self.btn_deactivate_all.setVisible(True)
    self.table_widget.setVisible(True)
    self.label_updating.setVisible(False)
    self.progress_bar.setVisible(False)
operation_with_selected_ips()

Handles operations with the selected IP addresses from the table widget. This method retrieves the selected rows from the table widget, extracts the IP addresses from a specific column, and performs an operation on the selected IPs using a separate thread. It also updates the UI to reflect the ongoing operation and handles progress updates.

Raises:

Type Description
Exception

If no devices are selected, a message box is displayed, and an exception is raised.

BaseError

If any other exception occurs during the execution of the method.

Attributes:

Name Type Description
selected_ips list[str]

A list to store the IP addresses of the selected devices.

UI Updates
  • Hides the table widget.
  • Sorts the table widget by the IP column in descending order.
  • Updates labels and buttons to indicate the operation is in progress.
  • Displays a progress bar to show the operation's progress.
Threads
  • Creates and starts an OperationThread to perform the operation on the selected IPs.
  • Connects thread signals to appropriate methods for progress updates, termination, and cleanup.
Source code in src\ui\base_select_devices_dialog.py
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
def operation_with_selected_ips(self):
    """
    Handles operations with the selected IP addresses from the table widget.
    This method retrieves the selected rows from the table widget, extracts the IP addresses
    from a specific column, and performs an operation on the selected IPs using a separate thread.
    It also updates the UI to reflect the ongoing operation and handles progress updates.

    Raises:
        Exception: If no devices are selected, a message box is displayed, and an exception is raised.
        BaseError: If any other exception occurs during the execution of the method.

    Attributes:
        selected_ips (list[str]): A list to store the IP addresses of the selected devices.

    UI Updates:
        - Hides the table widget.
        - Sorts the table widget by the IP column in descending order.
        - Updates labels and buttons to indicate the operation is in progress.
        - Displays a progress bar to show the operation's progress.

    Threads:
        - Creates and starts an `OperationThread` to perform the operation on the selected IPs.
        - Connects thread signals to appropriate methods for progress updates, termination, and cleanup.
    """
    try:        
        self.selected_ips: list[str] = []
        # Retrieve selected rows via the selection model
        for index in self.table_widget.selectionModel().selectedRows():
            row = index.row()
            ip = self.table_widget.item(row, 3).text()  # Column 3 holds the IP
            self.selected_ips.append(ip)

        if not self.selected_ips:
            QMessageBox.information(self, "Sin selección", "No se seleccionaron dispositivos")
            raise Exception("No se seleccionaron dispositivos")
        else:
            self.inputs_widget.setVisible(False)
            self.table_widget.setVisible(False)
            self.table_widget.sortByColumn(3, Qt.DescendingOrder)
            self.label_updating.setText("Actualizando datos...")
            self.btn_update.setVisible(False)
            self.btn_activate_all.setVisible(False)
            self.btn_deactivate_all.setVisible(False)
            self.label_updating.setVisible(True)
            self.progress_bar.setVisible(True)
            self.progress_bar.setValue(0)
            #logging.debug(f"Dispositivos seleccionados: {self.selected_ips}")
            self.op_thread = OperationThread(self.op_function, self.selected_ips)
            self.op_thread.progress_updated.connect(self.update_progress)
            self.op_thread.op_terminate.connect(self.op_terminate)
            self.op_thread.finished.connect(self.cleanup_thread)
            self.op_thread.start()
    except Exception as e:
        raise BaseError(3000, str(e))
select_all_rows()

Selects all rows in the table widget that are not already selected. This method retrieves the list of currently selected rows in the table widget and iterates through all rows in the table. If a row is not already selected, it selects the row.

Note
  • The method assumes that self.table_widget is a valid QTableWidget or similar widget with selectionModel, selectedRows, rowCount, and selectRow methods.
Source code in src\ui\base_select_devices_dialog.py
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
def select_all_rows(self):
    """
    Selects all rows in the table widget that are not already selected.
    This method retrieves the list of currently selected rows in the table widget
    and iterates through all rows in the table. If a row is not already selected,
    it selects the row.

    Note:
        - The method assumes that `self.table_widget` is a valid QTableWidget or
          similar widget with `selectionModel`, `selectedRows`, `rowCount`, and
          `selectRow` methods.
    """
    selected_rows = [index.row() for index in self.table_widget.selectionModel().selectedRows()]

    row_count = self.table_widget.rowCount()
    for row in range(row_count):
        if row not in selected_rows:
            self.table_widget.selectRow(row)
update_progress(percent_progress, device_progress, processed_devices, total_devices)

Updates the progress bar and status label with the current progress of device processing.

Parameters:

Name Type Description Default
percent_progress int

The overall progress percentage to be displayed on the progress bar.

required
device_progress str

A message indicating the status of the last connection attempt.

required
processed_devices int

The number of devices that have been processed so far.

required
total_devices int

The total number of devices to be processed.

required

Returns:

Type Description

None

Source code in src\ui\base_select_devices_dialog.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
def update_progress(self, percent_progress, device_progress, processed_devices, total_devices):
    """
    Updates the progress bar and status label with the current progress of device processing.

    Args:
        percent_progress (int): The overall progress percentage to be displayed on the progress bar.
        device_progress (str): A message indicating the status of the last connection attempt.
        processed_devices (int): The number of devices that have been processed so far.
        total_devices (int): The total number of devices to be processed.

    Returns:
        None
    """
    if percent_progress and device_progress:
        self.progress_bar.setValue(percent_progress)
        self.label_updating.setText(f"Último intento de conexión: {device_progress}\n{processed_devices}/{total_devices} dispositivos")

icon_manager

MainWindow

Bases: QMainWindow

Source code in src\ui\icon_manager.py
 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
class MainWindow(QMainWindow):
    def __init__(self):
        """
        Initializes the IconManager class.
        This constructor sets up the initial state of the application, including
        the system tray icon, configuration settings, and application startup behavior.
        It also handles potential duplicate instances of the application.

        Attributes:
            is_running (bool): Indicates if the application is currently running.
            checked_clear_attendance (bool): State of the "clear attendance" checkbox,
                retrieved from the configuration file.
            checked_automatic_init (bool): Indicates if the application is set to start
                automatically on system startup.
            tray_icon (QSystemTrayIcon): The system tray icon for the application.

        Raises:
            BaseError: If an exception occurs during initialization, it raises a
                BaseError with an error code, message, and severity level.
        """
        try:
            super().__init__()
            self.is_running = False  # Variable to indicate if the application is running
            self.checked_clear_attendance = eval(config['Device_config']['clear_attendance'])  # State of the clear attendance checkbox
            self.checked_automatic_init = is_startup_entry_exists("Programa Reloj de Asistencias")

            self.tray_icon: QSystemTrayIcon = None  # Variable to store the QSystemTrayIcon
            self.__init_ui()  # Initialize the user interface

            # Set the initial tray icon to loading.png
            file_path = os.path.join(find_marker_directory("resources"), "resources", "system_tray", "loading.png")  # Icon file path
            # logging.debug(file_path)
            self.tray_icon.setIcon(QIcon(file_path))

            """ if not is_user_admin():
                run_as_admin()
            """

            #if verify_duplicated_instance(sys.argv[0]):
            #    exit_duplicated_instance()

            # Change the tray icon to program-icon.png after all initializations
            file_path = os.path.join(find_marker_directory("resources"), "resources", "system_tray", "program-icon.png")  # Icon file path
            # logging.debug(file_path)
            self.tray_icon.setIcon(QIcon(file_path))
        except Exception as e:
            raise BaseError(3501, str(e), "critical")

    def __init_ui(self):
        """
        Initializes the user interface components for the application.

        This method is responsible for setting up and configuring the system tray icon
        by invoking the necessary helper methods. It ensures that the system tray icon
        is properly created and ready for use.
        """
        # Create and configure the system tray icon
        self.__create_tray_icon()  # Create the system tray icon        

    def __create_tray_icon(self):
        """
        Creates and configures the system tray icon for the application.
        This method initializes a QSystemTrayIcon with a tooltip and a custom context menu
        containing various actions for interacting with the application. The context menu
        includes options for modifying devices, restarting devices, testing connections,
        updating device time, fetching attendances, and toggling specific settings. It also
        provides options to view logs and exit the application.
        The tray icon displays a notification message upon initialization and is shown in
        the system tray.

        Raises:
            BaseError: If an exception occurs during the creation or configuration of the
                       system tray icon, it raises a BaseError with an error code and message.
        """
        try:
            file_path = os.path.join(find_marker_directory("resources"), "resources", "system_tray", "loading.png")  # Icon file path
            # logging.debug(file_path)
            self.tray_icon = QSystemTrayIcon(QIcon(file_path), self)  # Create QSystemTrayIcon with the icon and associated main window
            self.tray_icon.showMessage("Notificación", 'Iniciando la aplicación', QSystemTrayIcon.Information)
            self.tray_icon.setToolTip("Programa Reloj de Asistencias")  # Tooltip text

            # Create a custom context menu
            menu = QMenu()
            menu.addAction(self.__create_action("Modificar dispositivos...", lambda: self.__opt_modify_devices()))  # Action to modify devices
            menu.addAction(self.__create_action("Reiniciar dispositivos...", lambda: self.__opt_restart_devices()))  # Action to restart devices    
            menu.addAction(self.__create_action("Probar conexiones...", lambda: self.__opt_test_connections()))  # Action to test connections
            menu.addAction(self.__create_action("Actualizar hora...", lambda: self.__opt_update_devices_time()))  # Action to update device time
            menu.addAction(self.__create_action("Obtener marcaciones...", lambda: self.__opt_fetch_devices_attendances()))  # Action to fetch device attendances
            menu.addSeparator()  # Context menu separator
            # Checkbox as QAction with checkable state
            clear_attendance_action = QAction("Eliminar marcaciones", menu)
            clear_attendance_action.setCheckable(True)  # Make the QAction checkable
            clear_attendance_action.setChecked(self.checked_clear_attendance)  # Set initial checkbox state
            clear_attendance_action.triggered.connect(self.__opt_toggle_checkbox_clear_attendance)  # Connect action to toggle checkbox state
            menu.addAction(clear_attendance_action)  # Add action to the menu
            # logging.debug(f'checked_automatic_init: {self.checked_automatic_init}')
            # Action to toggle the checkbox state
            automatic_init_action = QAction('Iniciar automáticamente', menu)
            automatic_init_action.setCheckable(True)
            automatic_init_action.setChecked(self.checked_automatic_init)
            automatic_init_action.triggered.connect(self.__opt_toggle_checkbox_automatic_init)
            menu.addAction(automatic_init_action)
            menu.addSeparator()  # Context menu separator
            menu.addAction(self.__create_action("Ver errores...", lambda: self.__opt_show_logs()))  # Action to show logs
            menu.addAction(self.__create_action("Salir", lambda: self.__opt_exit_icon()))  # Action to exit the application
            self.tray_icon.setContextMenu(menu)  # Assign context menu to the icon

            self.tray_icon.show()  # Show the system tray icon
        except Exception as e:
            raise BaseError(3500, str(e), "critical")

    def __create_action(self, text, function):
        """
        Creates a QAction with the specified text and associates it with a function.

        Args:
            text (str): The display text for the action.
            function (callable): The function to be executed when the action is triggered.

        Returns:
            (QAction): The created action with the specified text and connected function.
        """
        action = QAction(text, self)  # Create QAction with the text and associated main window
        action.triggered.connect(function)  # Connect the action to the provided function
        return action  # Return the created action

    def start_timer(self):
        """
        Starts a timer by returning the current time in seconds.

        Returns:
            (float): The current time in seconds since the epoch.
        """
        return time.time()  # Return the current time in seconds

    def stop_timer(self, start_time):
        """
        Stops the timer and calculates the elapsed time since the provided start time.

        Args:
            start_time (float): The starting time in seconds since the epoch.

        Returns:
            None

        Logs:
            Logs the elapsed time in seconds to the application log.

        Side Effects:
            Displays a system tray notification with the elapsed time.
        """
        end_time = self.start_timer()  # Get the end time
        elapsed_time = end_time - start_time  # Calculate the elapsed time
        logging.info(f'La tarea finalizo en {elapsed_time:.2f} segundos')
        self.tray_icon.showMessage("Notificacion", f'La tarea finalizo en {elapsed_time:.2f} segundos', QSystemTrayIcon.Information)  # Show notification with the elapsed time

    def __show_message_information(self, title, text):
        """
        Displays an informational message dialog with a specified title and text.
        This method creates a QMessageBox instance to show an informational message
        to the user. It sets the title, text, and icon of the dialog box. Additionally,
        it customizes the window icon using a specified `.ico` file. After the dialog
        box is closed, it ensures that the tray icon's context menu is made visible again.

        Args:
            title (str): The title of the message dialog box.
            text (str): The informational text to display in the dialog box.

        Side Effects:
            - Displays a QMessageBox with the specified title and text.
            - Sets the window icon of the QMessageBox using a custom `.ico` file.
            - Ensures the tray icon's context menu is visible after the dialog box is closed.
        """
        msg_box = QMessageBox()  # Create QMessageBox instance
        msg_box.setWindowTitle(title)  # Set the dialog box title
        msg_box.setText(text)  # Set the message text
        msg_box.setIcon(QMessageBox.Information)  # Set the dialog box icon (information)
        file_path = os.path.join(find_marker_directory("resources"), "resources", "fingerprint.ico")
        msg_box.setWindowIcon(QIcon(file_path))
        msg_box.exec_()  # Show the dialog box

        # Once the QMessageBox is closed, show the context menu again
        if self.tray_icon:
            self.tray_icon.contextMenu().setVisible(True)

    @pyqtSlot()
    def __opt_modify_devices(self):
        """
        Handles the modification of devices through a dialog interface.

        This method creates and displays a `ModifyDevicesDialog` for modifying device settings.
        Once the dialog is closed, it ensures that the tray icon's context menu is visible again.
        Any exceptions encountered during the process are logged using the `BaseError` class.

        Raises:
            BaseError: If an exception occurs during the execution of the method, it is wrapped
                       and logged with an error code of 3500.
        """
        try:
            device_dialog = ModifyDevicesDialog()
            device_dialog.exec_()
            # Once the QDialog is closed, show the context menu again
            if self.tray_icon:
                self.tray_icon.contextMenu().setVisible(True)
        except Exception as e:
            BaseError(3500, str(e))

    @pyqtSlot()
    def __opt_show_logs(self):
        """
        Displays the logs dialog and ensures the system tray context menu is visible after the dialog is closed.

        This method attempts to create and display a `LogsDialog` instance. Once the dialog is closed, 
        it ensures that the system tray icon's context menu is made visible again. If an exception occurs 
        during this process, it is handled by logging the error using the `BaseError` class.

        Raises:
            Exception: If an error occurs while creating or displaying the `LogsDialog`.
        """
        try:
            error_log_dialog = LogsDialog()
            error_log_dialog.exec_()
            # Once the QDialog is closed, show the context menu again
            if self.tray_icon:
                self.tray_icon.contextMenu().setVisible(True)
        except Exception as e:
            BaseError(3500, str(e))

    @pyqtSlot()
    def __opt_restart_devices(self):
        """
        Handles the restart devices operation by displaying a dialog to the user.

        This method creates and executes a `RestartDevicesDialog` to allow the user
        to restart devices. Once the dialog is closed, it ensures that the tray icon's
        context menu is made visible again. If an exception occurs during the process,
        it logs the error using the `BaseError` class.

        Raises:
            Exception: If an error occurs during the execution of the dialog or
                       while handling the tray icon's context menu.
        """
        try:
            restart_devices_dialog = RestartDevicesDialog()
            restart_devices_dialog.exec_()
            # Once the QDialog is closed, show the context menu again
            if self.tray_icon:
                self.tray_icon.contextMenu().setVisible(True)
        except Exception as e:
            BaseError(3500, str(e))

    @pyqtSlot()
    def __opt_test_connections(self):
        """
        Handles the testing of device connections by opening a dialog to ping devices.

        This method creates and displays a `PingDevicesDialog` to check the status of devices.
        Once the dialog is closed, it ensures that the tray icon's context menu is visible again.
        If an exception occurs during the process, it logs the error using the `BaseError` class.

        Raises:
            Exception: If an error occurs during the execution of the method.
        """
        try:
            device_status_dialog = PingDevicesDialog()  # Get device status
            device_status_dialog.exec_()
            # Once the QDialog is closed, show the context menu again
            if self.tray_icon:
                self.tray_icon.contextMenu().setVisible(True)
        except Exception as e:
            BaseError(3500, str(e))

    @pyqtSlot()
    def __opt_update_devices_time(self):
        """
        Handles the process of updating the time on devices.

        This method creates and displays a dialog for updating the time on devices.
        Once the dialog is closed, it ensures that the context menu of the tray icon
        is made visible again if it exists. Any exceptions raised during the process
        are caught and logged using the BaseError class.

        Exceptions:
            Exception: Catches any exception that occurs during the execution and
                       logs it with an error code and message.
        """
        try:
            update_time_device_dialog = UpdateTimeDeviceDialog()
            update_time_device_dialog.exec_()
            # Once the QDialog is closed, show the context menu again
            if self.tray_icon:
                self.tray_icon.contextMenu().setVisible(True)
        except Exception as e:
            BaseError(3500, str(e))

    @pyqtSlot()
    def __opt_fetch_devices_attendances(self):
        """
        Handles the process of fetching attendance data from devices.

        This method creates and displays a dialog for obtaining attendance data
        from devices. Once the dialog is closed, it ensures that the tray icon's
        context menu is made visible again. Any exceptions encountered during the
        process are captured and logged using the BaseError class.

        Raises:
            BaseError: If an exception occurs during the execution of the method.
        """
        try:
            device_attendances_dialog = ObtainAttendancesDevicesDialog()
            #device_attendances_dialog.op_terminated.connect(self.stop_timer)
            device_attendances_dialog.exec_()
            # Once the QDialog is closed, show the context menu again
            if self.tray_icon:
                self.tray_icon.contextMenu().setVisible(True)
        except Exception as e:
            BaseError(3500, str(e))

    @pyqtSlot()
    def __opt_toggle_checkbox_clear_attendance(self):
        """
        Toggles the state of the 'clear attendance' checkbox and updates the configuration file accordingly.

        This method inverts the current state of the `checked_clear_attendance` attribute, updates the 
        corresponding value in the configuration file under the 'Device_config' section, and writes the 
        changes back to the file. If an error occurs during the file write operation, it raises a 
        `BaseError` with an appropriate error code and message.

        Raises:
            BaseError: If an exception occurs while writing to the configuration file.
        """
        self.checked_clear_attendance = not self.checked_clear_attendance  # Invert the current checkbox state
        # logging.debug(f"Status checkbox: {self.checked_clear_attendance}")  # Debug log: current checkbox state
        # Modify the value of the desired field in the configuration file
        config['Device_config']['clear_attendance'] = str(self.checked_clear_attendance)
        # Write the changes back to the configuration file
        try:
            with open('config.ini', 'w') as config_file:
                config.write(config_file)
        except Exception as e:
            BaseError(3001, str(e))

    @pyqtSlot()
    def __opt_toggle_checkbox_automatic_init(self):
        """
        Toggles the state of the "automatic initialization" checkbox and updates the system's startup configuration
        accordingly. This method is intended to be used in a frozen Python application (e.g., packaged with PyInstaller).
        When the checkbox is toggled:

        - If enabled, the application is added to the system's startup programs.
        - If disabled, the application is removed from the system's startup programs.

        Exceptions are caught and logged using the `BaseError` class.

        Raises:
            BaseError: If an exception occurs during the process, it is wrapped and raised with an error code (3000).

        Notes:
            - The `add_to_startup` and `remove_from_startup` functions are assumed to handle the actual system-level
              operations for managing startup programs.
            - This method only functions correctly in a frozen Python environment (e.g., when `sys.frozen` is True).
        """
        import sys
        try:
            if getattr(sys, 'frozen', False):
                self.checked_automatic_init = not self.checked_automatic_init  # Invert the current checkbox state
                # logging.debug(f"Status checkbox: {self.checked_automatic_init}")  # Debug log: current checkbox state

                if self.checked_automatic_init:
                    # logging.debug('add_to_startup')
                    add_to_startup("Programa Reloj de Asistencias")
                else:
                    # logging.debug('remove_from_startup')
                    remove_from_startup("Programa Reloj de Asistencias")
        except Exception as e:
            BaseError(3000, str(e))

    @pyqtSlot()
    def __opt_exit_icon(self):
        """
        Handles the exit operation for the application.

        This method hides the system tray icon, if it exists, and then quits the application.

        Returns:
            None
        """
        if self.tray_icon:
            self.tray_icon.hide()  # Hide the system tray icon
            QApplication.quit()  # Exit the application
__create_action(text, function)

Creates a QAction with the specified text and associates it with a function.

Parameters:

Name Type Description Default
text str

The display text for the action.

required
function callable

The function to be executed when the action is triggered.

required

Returns:

Type Description
QAction

The created action with the specified text and connected function.

Source code in src\ui\icon_manager.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def __create_action(self, text, function):
    """
    Creates a QAction with the specified text and associates it with a function.

    Args:
        text (str): The display text for the action.
        function (callable): The function to be executed when the action is triggered.

    Returns:
        (QAction): The created action with the specified text and connected function.
    """
    action = QAction(text, self)  # Create QAction with the text and associated main window
    action.triggered.connect(function)  # Connect the action to the provided function
    return action  # Return the created action
__create_tray_icon()

Creates and configures the system tray icon for the application. This method initializes a QSystemTrayIcon with a tooltip and a custom context menu containing various actions for interacting with the application. The context menu includes options for modifying devices, restarting devices, testing connections, updating device time, fetching attendances, and toggling specific settings. It also provides options to view logs and exit the application. The tray icon displays a notification message upon initialization and is shown in the system tray.

Raises:

Type Description
BaseError

If an exception occurs during the creation or configuration of the system tray icon, it raises a BaseError with an error code and message.

Source code in src\ui\icon_manager.py
 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
def __create_tray_icon(self):
    """
    Creates and configures the system tray icon for the application.
    This method initializes a QSystemTrayIcon with a tooltip and a custom context menu
    containing various actions for interacting with the application. The context menu
    includes options for modifying devices, restarting devices, testing connections,
    updating device time, fetching attendances, and toggling specific settings. It also
    provides options to view logs and exit the application.
    The tray icon displays a notification message upon initialization and is shown in
    the system tray.

    Raises:
        BaseError: If an exception occurs during the creation or configuration of the
                   system tray icon, it raises a BaseError with an error code and message.
    """
    try:
        file_path = os.path.join(find_marker_directory("resources"), "resources", "system_tray", "loading.png")  # Icon file path
        # logging.debug(file_path)
        self.tray_icon = QSystemTrayIcon(QIcon(file_path), self)  # Create QSystemTrayIcon with the icon and associated main window
        self.tray_icon.showMessage("Notificación", 'Iniciando la aplicación', QSystemTrayIcon.Information)
        self.tray_icon.setToolTip("Programa Reloj de Asistencias")  # Tooltip text

        # Create a custom context menu
        menu = QMenu()
        menu.addAction(self.__create_action("Modificar dispositivos...", lambda: self.__opt_modify_devices()))  # Action to modify devices
        menu.addAction(self.__create_action("Reiniciar dispositivos...", lambda: self.__opt_restart_devices()))  # Action to restart devices    
        menu.addAction(self.__create_action("Probar conexiones...", lambda: self.__opt_test_connections()))  # Action to test connections
        menu.addAction(self.__create_action("Actualizar hora...", lambda: self.__opt_update_devices_time()))  # Action to update device time
        menu.addAction(self.__create_action("Obtener marcaciones...", lambda: self.__opt_fetch_devices_attendances()))  # Action to fetch device attendances
        menu.addSeparator()  # Context menu separator
        # Checkbox as QAction with checkable state
        clear_attendance_action = QAction("Eliminar marcaciones", menu)
        clear_attendance_action.setCheckable(True)  # Make the QAction checkable
        clear_attendance_action.setChecked(self.checked_clear_attendance)  # Set initial checkbox state
        clear_attendance_action.triggered.connect(self.__opt_toggle_checkbox_clear_attendance)  # Connect action to toggle checkbox state
        menu.addAction(clear_attendance_action)  # Add action to the menu
        # logging.debug(f'checked_automatic_init: {self.checked_automatic_init}')
        # Action to toggle the checkbox state
        automatic_init_action = QAction('Iniciar automáticamente', menu)
        automatic_init_action.setCheckable(True)
        automatic_init_action.setChecked(self.checked_automatic_init)
        automatic_init_action.triggered.connect(self.__opt_toggle_checkbox_automatic_init)
        menu.addAction(automatic_init_action)
        menu.addSeparator()  # Context menu separator
        menu.addAction(self.__create_action("Ver errores...", lambda: self.__opt_show_logs()))  # Action to show logs
        menu.addAction(self.__create_action("Salir", lambda: self.__opt_exit_icon()))  # Action to exit the application
        self.tray_icon.setContextMenu(menu)  # Assign context menu to the icon

        self.tray_icon.show()  # Show the system tray icon
    except Exception as e:
        raise BaseError(3500, str(e), "critical")
__init__()

Initializes the IconManager class. This constructor sets up the initial state of the application, including the system tray icon, configuration settings, and application startup behavior. It also handles potential duplicate instances of the application.

Attributes:

Name Type Description
is_running bool

Indicates if the application is currently running.

checked_clear_attendance bool

State of the "clear attendance" checkbox, retrieved from the configuration file.

checked_automatic_init bool

Indicates if the application is set to start automatically on system startup.

tray_icon QSystemTrayIcon

The system tray icon for the application.

Raises:

Type Description
BaseError

If an exception occurs during initialization, it raises a BaseError with an error code, message, and severity level.

Source code in src\ui\icon_manager.py
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
def __init__(self):
    """
    Initializes the IconManager class.
    This constructor sets up the initial state of the application, including
    the system tray icon, configuration settings, and application startup behavior.
    It also handles potential duplicate instances of the application.

    Attributes:
        is_running (bool): Indicates if the application is currently running.
        checked_clear_attendance (bool): State of the "clear attendance" checkbox,
            retrieved from the configuration file.
        checked_automatic_init (bool): Indicates if the application is set to start
            automatically on system startup.
        tray_icon (QSystemTrayIcon): The system tray icon for the application.

    Raises:
        BaseError: If an exception occurs during initialization, it raises a
            BaseError with an error code, message, and severity level.
    """
    try:
        super().__init__()
        self.is_running = False  # Variable to indicate if the application is running
        self.checked_clear_attendance = eval(config['Device_config']['clear_attendance'])  # State of the clear attendance checkbox
        self.checked_automatic_init = is_startup_entry_exists("Programa Reloj de Asistencias")

        self.tray_icon: QSystemTrayIcon = None  # Variable to store the QSystemTrayIcon
        self.__init_ui()  # Initialize the user interface

        # Set the initial tray icon to loading.png
        file_path = os.path.join(find_marker_directory("resources"), "resources", "system_tray", "loading.png")  # Icon file path
        # logging.debug(file_path)
        self.tray_icon.setIcon(QIcon(file_path))

        """ if not is_user_admin():
            run_as_admin()
        """

        #if verify_duplicated_instance(sys.argv[0]):
        #    exit_duplicated_instance()

        # Change the tray icon to program-icon.png after all initializations
        file_path = os.path.join(find_marker_directory("resources"), "resources", "system_tray", "program-icon.png")  # Icon file path
        # logging.debug(file_path)
        self.tray_icon.setIcon(QIcon(file_path))
    except Exception as e:
        raise BaseError(3501, str(e), "critical")
__init_ui()

Initializes the user interface components for the application.

This method is responsible for setting up and configuring the system tray icon by invoking the necessary helper methods. It ensures that the system tray icon is properly created and ready for use.

Source code in src\ui\icon_manager.py
88
89
90
91
92
93
94
95
96
97
def __init_ui(self):
    """
    Initializes the user interface components for the application.

    This method is responsible for setting up and configuring the system tray icon
    by invoking the necessary helper methods. It ensures that the system tray icon
    is properly created and ready for use.
    """
    # Create and configure the system tray icon
    self.__create_tray_icon()  # Create the system tray icon        
__opt_exit_icon()

Handles the exit operation for the application.

This method hides the system tray icon, if it exists, and then quits the application.

Returns:

Type Description

None

Source code in src\ui\icon_manager.py
417
418
419
420
421
422
423
424
425
426
427
428
429
@pyqtSlot()
def __opt_exit_icon(self):
    """
    Handles the exit operation for the application.

    This method hides the system tray icon, if it exists, and then quits the application.

    Returns:
        None
    """
    if self.tray_icon:
        self.tray_icon.hide()  # Hide the system tray icon
        QApplication.quit()  # Exit the application
__opt_fetch_devices_attendances()

Handles the process of fetching attendance data from devices.

This method creates and displays a dialog for obtaining attendance data from devices. Once the dialog is closed, it ensures that the tray icon's context menu is made visible again. Any exceptions encountered during the process are captured and logged using the BaseError class.

Raises:

Type Description
BaseError

If an exception occurs during the execution of the method.

Source code in src\ui\icon_manager.py
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
@pyqtSlot()
def __opt_fetch_devices_attendances(self):
    """
    Handles the process of fetching attendance data from devices.

    This method creates and displays a dialog for obtaining attendance data
    from devices. Once the dialog is closed, it ensures that the tray icon's
    context menu is made visible again. Any exceptions encountered during the
    process are captured and logged using the BaseError class.

    Raises:
        BaseError: If an exception occurs during the execution of the method.
    """
    try:
        device_attendances_dialog = ObtainAttendancesDevicesDialog()
        #device_attendances_dialog.op_terminated.connect(self.stop_timer)
        device_attendances_dialog.exec_()
        # Once the QDialog is closed, show the context menu again
        if self.tray_icon:
            self.tray_icon.contextMenu().setVisible(True)
    except Exception as e:
        BaseError(3500, str(e))
__opt_modify_devices()

Handles the modification of devices through a dialog interface.

This method creates and displays a ModifyDevicesDialog for modifying device settings. Once the dialog is closed, it ensures that the tray icon's context menu is visible again. Any exceptions encountered during the process are logged using the BaseError class.

Raises:

Type Description
BaseError

If an exception occurs during the execution of the method, it is wrapped and logged with an error code of 3500.

Source code in src\ui\icon_manager.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
@pyqtSlot()
def __opt_modify_devices(self):
    """
    Handles the modification of devices through a dialog interface.

    This method creates and displays a `ModifyDevicesDialog` for modifying device settings.
    Once the dialog is closed, it ensures that the tray icon's context menu is visible again.
    Any exceptions encountered during the process are logged using the `BaseError` class.

    Raises:
        BaseError: If an exception occurs during the execution of the method, it is wrapped
                   and logged with an error code of 3500.
    """
    try:
        device_dialog = ModifyDevicesDialog()
        device_dialog.exec_()
        # Once the QDialog is closed, show the context menu again
        if self.tray_icon:
            self.tray_icon.contextMenu().setVisible(True)
    except Exception as e:
        BaseError(3500, str(e))
__opt_restart_devices()

Handles the restart devices operation by displaying a dialog to the user.

This method creates and executes a RestartDevicesDialog to allow the user to restart devices. Once the dialog is closed, it ensures that the tray icon's context menu is made visible again. If an exception occurs during the process, it logs the error using the BaseError class.

Raises:

Type Description
Exception

If an error occurs during the execution of the dialog or while handling the tray icon's context menu.

Source code in src\ui\icon_manager.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
@pyqtSlot()
def __opt_restart_devices(self):
    """
    Handles the restart devices operation by displaying a dialog to the user.

    This method creates and executes a `RestartDevicesDialog` to allow the user
    to restart devices. Once the dialog is closed, it ensures that the tray icon's
    context menu is made visible again. If an exception occurs during the process,
    it logs the error using the `BaseError` class.

    Raises:
        Exception: If an error occurs during the execution of the dialog or
                   while handling the tray icon's context menu.
    """
    try:
        restart_devices_dialog = RestartDevicesDialog()
        restart_devices_dialog.exec_()
        # Once the QDialog is closed, show the context menu again
        if self.tray_icon:
            self.tray_icon.contextMenu().setVisible(True)
    except Exception as e:
        BaseError(3500, str(e))
__opt_show_logs()

Displays the logs dialog and ensures the system tray context menu is visible after the dialog is closed.

This method attempts to create and display a LogsDialog instance. Once the dialog is closed, it ensures that the system tray icon's context menu is made visible again. If an exception occurs during this process, it is handled by logging the error using the BaseError class.

Raises:

Type Description
Exception

If an error occurs while creating or displaying the LogsDialog.

Source code in src\ui\icon_manager.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
@pyqtSlot()
def __opt_show_logs(self):
    """
    Displays the logs dialog and ensures the system tray context menu is visible after the dialog is closed.

    This method attempts to create and display a `LogsDialog` instance. Once the dialog is closed, 
    it ensures that the system tray icon's context menu is made visible again. If an exception occurs 
    during this process, it is handled by logging the error using the `BaseError` class.

    Raises:
        Exception: If an error occurs while creating or displaying the `LogsDialog`.
    """
    try:
        error_log_dialog = LogsDialog()
        error_log_dialog.exec_()
        # Once the QDialog is closed, show the context menu again
        if self.tray_icon:
            self.tray_icon.contextMenu().setVisible(True)
    except Exception as e:
        BaseError(3500, str(e))
__opt_test_connections()

Handles the testing of device connections by opening a dialog to ping devices.

This method creates and displays a PingDevicesDialog to check the status of devices. Once the dialog is closed, it ensures that the tray icon's context menu is visible again. If an exception occurs during the process, it logs the error using the BaseError class.

Raises:

Type Description
Exception

If an error occurs during the execution of the method.

Source code in src\ui\icon_manager.py
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
@pyqtSlot()
def __opt_test_connections(self):
    """
    Handles the testing of device connections by opening a dialog to ping devices.

    This method creates and displays a `PingDevicesDialog` to check the status of devices.
    Once the dialog is closed, it ensures that the tray icon's context menu is visible again.
    If an exception occurs during the process, it logs the error using the `BaseError` class.

    Raises:
        Exception: If an error occurs during the execution of the method.
    """
    try:
        device_status_dialog = PingDevicesDialog()  # Get device status
        device_status_dialog.exec_()
        # Once the QDialog is closed, show the context menu again
        if self.tray_icon:
            self.tray_icon.contextMenu().setVisible(True)
    except Exception as e:
        BaseError(3500, str(e))
__opt_toggle_checkbox_automatic_init()

Toggles the state of the "automatic initialization" checkbox and updates the system's startup configuration accordingly. This method is intended to be used in a frozen Python application (e.g., packaged with PyInstaller). When the checkbox is toggled:

  • If enabled, the application is added to the system's startup programs.
  • If disabled, the application is removed from the system's startup programs.

Exceptions are caught and logged using the BaseError class.

Raises:

Type Description
BaseError

If an exception occurs during the process, it is wrapped and raised with an error code (3000).

Notes
  • The add_to_startup and remove_from_startup functions are assumed to handle the actual system-level operations for managing startup programs.
  • This method only functions correctly in a frozen Python environment (e.g., when sys.frozen is True).
Source code in src\ui\icon_manager.py
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
@pyqtSlot()
def __opt_toggle_checkbox_automatic_init(self):
    """
    Toggles the state of the "automatic initialization" checkbox and updates the system's startup configuration
    accordingly. This method is intended to be used in a frozen Python application (e.g., packaged with PyInstaller).
    When the checkbox is toggled:

    - If enabled, the application is added to the system's startup programs.
    - If disabled, the application is removed from the system's startup programs.

    Exceptions are caught and logged using the `BaseError` class.

    Raises:
        BaseError: If an exception occurs during the process, it is wrapped and raised with an error code (3000).

    Notes:
        - The `add_to_startup` and `remove_from_startup` functions are assumed to handle the actual system-level
          operations for managing startup programs.
        - This method only functions correctly in a frozen Python environment (e.g., when `sys.frozen` is True).
    """
    import sys
    try:
        if getattr(sys, 'frozen', False):
            self.checked_automatic_init = not self.checked_automatic_init  # Invert the current checkbox state
            # logging.debug(f"Status checkbox: {self.checked_automatic_init}")  # Debug log: current checkbox state

            if self.checked_automatic_init:
                # logging.debug('add_to_startup')
                add_to_startup("Programa Reloj de Asistencias")
            else:
                # logging.debug('remove_from_startup')
                remove_from_startup("Programa Reloj de Asistencias")
    except Exception as e:
        BaseError(3000, str(e))
__opt_toggle_checkbox_clear_attendance()

Toggles the state of the 'clear attendance' checkbox and updates the configuration file accordingly.

This method inverts the current state of the checked_clear_attendance attribute, updates the corresponding value in the configuration file under the 'Device_config' section, and writes the changes back to the file. If an error occurs during the file write operation, it raises a BaseError with an appropriate error code and message.

Raises:

Type Description
BaseError

If an exception occurs while writing to the configuration file.

Source code in src\ui\icon_manager.py
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
@pyqtSlot()
def __opt_toggle_checkbox_clear_attendance(self):
    """
    Toggles the state of the 'clear attendance' checkbox and updates the configuration file accordingly.

    This method inverts the current state of the `checked_clear_attendance` attribute, updates the 
    corresponding value in the configuration file under the 'Device_config' section, and writes the 
    changes back to the file. If an error occurs during the file write operation, it raises a 
    `BaseError` with an appropriate error code and message.

    Raises:
        BaseError: If an exception occurs while writing to the configuration file.
    """
    self.checked_clear_attendance = not self.checked_clear_attendance  # Invert the current checkbox state
    # logging.debug(f"Status checkbox: {self.checked_clear_attendance}")  # Debug log: current checkbox state
    # Modify the value of the desired field in the configuration file
    config['Device_config']['clear_attendance'] = str(self.checked_clear_attendance)
    # Write the changes back to the configuration file
    try:
        with open('config.ini', 'w') as config_file:
            config.write(config_file)
    except Exception as e:
        BaseError(3001, str(e))
__opt_update_devices_time()

Handles the process of updating the time on devices.

This method creates and displays a dialog for updating the time on devices. Once the dialog is closed, it ensures that the context menu of the tray icon is made visible again if it exists. Any exceptions raised during the process are caught and logged using the BaseError class.

Raises:

Type Description
Exception

Catches any exception that occurs during the execution and logs it with an error code and message.

Source code in src\ui\icon_manager.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
@pyqtSlot()
def __opt_update_devices_time(self):
    """
    Handles the process of updating the time on devices.

    This method creates and displays a dialog for updating the time on devices.
    Once the dialog is closed, it ensures that the context menu of the tray icon
    is made visible again if it exists. Any exceptions raised during the process
    are caught and logged using the BaseError class.

    Exceptions:
        Exception: Catches any exception that occurs during the execution and
                   logs it with an error code and message.
    """
    try:
        update_time_device_dialog = UpdateTimeDeviceDialog()
        update_time_device_dialog.exec_()
        # Once the QDialog is closed, show the context menu again
        if self.tray_icon:
            self.tray_icon.contextMenu().setVisible(True)
    except Exception as e:
        BaseError(3500, str(e))
__show_message_information(title, text)

Displays an informational message dialog with a specified title and text. This method creates a QMessageBox instance to show an informational message to the user. It sets the title, text, and icon of the dialog box. Additionally, it customizes the window icon using a specified .ico file. After the dialog box is closed, it ensures that the tray icon's context menu is made visible again.

Parameters:

Name Type Description Default
title str

The title of the message dialog box.

required
text str

The informational text to display in the dialog box.

required
Side Effects
  • Displays a QMessageBox with the specified title and text.
  • Sets the window icon of the QMessageBox using a custom .ico file.
  • Ensures the tray icon's context menu is visible after the dialog box is closed.
Source code in src\ui\icon_manager.py
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
def __show_message_information(self, title, text):
    """
    Displays an informational message dialog with a specified title and text.
    This method creates a QMessageBox instance to show an informational message
    to the user. It sets the title, text, and icon of the dialog box. Additionally,
    it customizes the window icon using a specified `.ico` file. After the dialog
    box is closed, it ensures that the tray icon's context menu is made visible again.

    Args:
        title (str): The title of the message dialog box.
        text (str): The informational text to display in the dialog box.

    Side Effects:
        - Displays a QMessageBox with the specified title and text.
        - Sets the window icon of the QMessageBox using a custom `.ico` file.
        - Ensures the tray icon's context menu is visible after the dialog box is closed.
    """
    msg_box = QMessageBox()  # Create QMessageBox instance
    msg_box.setWindowTitle(title)  # Set the dialog box title
    msg_box.setText(text)  # Set the message text
    msg_box.setIcon(QMessageBox.Information)  # Set the dialog box icon (information)
    file_path = os.path.join(find_marker_directory("resources"), "resources", "fingerprint.ico")
    msg_box.setWindowIcon(QIcon(file_path))
    msg_box.exec_()  # Show the dialog box

    # Once the QMessageBox is closed, show the context menu again
    if self.tray_icon:
        self.tray_icon.contextMenu().setVisible(True)
start_timer()

Starts a timer by returning the current time in seconds.

Returns:

Type Description
float

The current time in seconds since the epoch.

Source code in src\ui\icon_manager.py
166
167
168
169
170
171
172
173
def start_timer(self):
    """
    Starts a timer by returning the current time in seconds.

    Returns:
        (float): The current time in seconds since the epoch.
    """
    return time.time()  # Return the current time in seconds
stop_timer(start_time)

Stops the timer and calculates the elapsed time since the provided start time.

Parameters:

Name Type Description Default
start_time float

The starting time in seconds since the epoch.

required

Returns:

Type Description

None

Logs

Logs the elapsed time in seconds to the application log.

Side Effects

Displays a system tray notification with the elapsed time.

Source code in src\ui\icon_manager.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def stop_timer(self, start_time):
    """
    Stops the timer and calculates the elapsed time since the provided start time.

    Args:
        start_time (float): The starting time in seconds since the epoch.

    Returns:
        None

    Logs:
        Logs the elapsed time in seconds to the application log.

    Side Effects:
        Displays a system tray notification with the elapsed time.
    """
    end_time = self.start_timer()  # Get the end time
    elapsed_time = end_time - start_time  # Calculate the elapsed time
    logging.info(f'La tarea finalizo en {elapsed_time:.2f} segundos')
    self.tray_icon.showMessage("Notificacion", f'La tarea finalizo en {elapsed_time:.2f} segundos', QSystemTrayIcon.Information)  # Show notification with the elapsed time

logs_dialog

LogsDialog

Bases: BaseDialog

Source code in src\ui\logs_dialog.py
 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
class LogsDialog(BaseDialog):
    def __init__(self):
        """
        Initializes the LogsDialog class.

        This constructor sets up the LogsDialog instance by calling the parent
        class initializer with a specific window title, initializing the user
        interface, and handling any exceptions that may occur during the process.

        Raises:
            BaseError: If an exception occurs during initialization, it is wrapped
                       in a BaseError with code 3501 and the exception message.
        """
        try:
            super().__init__(window_title="VISOR DE LOGS")
            self.init_ui()
            super().init_ui()
        except Exception as e:
            raise BaseError(3501, str(e))

    def init_ui(self):
        """
        Initializes the user interface for the logs dialog.
        This method sets up the UI components, including date selection widgets, 
        text search filter, error and source selection lists, and a text display 
        area for logs. It also configures the layout and connects signals to 
        dynamically filter logs based on user input.

        UI Components:
            - Date Selection Widgets:
                * `start_date_edit`: QDateEdit for selecting the start date.
                * `end_date_edit`: QDateEdit for selecting the end date.
            - Text Search Filter:
                * `text_search_edit`: QLineEdit for searching text in logs.
            - Error Selection List:
                * `error_list`: QListWidget for selecting error codes to filter logs.
            - Source Selection List:
                * `source_list`: QListWidget for selecting log sources to filter logs.
            - Toggle Filter Button:
                * `toggle_filter_button`: QPushButton to toggle the visibility of 
                  error and source filter lists.
            - Labels:
                * `select_errors_label`: QLabel for error filter instructions.
                * `select_sources_label`: QLabel for source filter instructions.
            - Logs Display:
                * `text_edit`: QTextEdit for displaying filtered logs.

        Layout:
            - `filter_layout`: Horizontal layout for date selection, text search, 
              and toggle filter button.
            - `error_list_layout`: Vertical layout for error filter label and list.
            - `source_list_layout`: Vertical layout for source filter label and list.
            - `filter_lists_layout`: Horizontal layout combining error and source 
              filter layouts.
            - `layout`: Main vertical layout combining all components.

        Behavior:
            - Connects signals to dynamically filter logs when:
                * Dates are changed.
                * Text is entered in the search field.
                * Error or source selections are modified.
            - Loads logs at startup.

        Raises:
            BaseError: If an exception occurs during UI initialization.
        """
        try:
            # Date selection widgets
            self.start_date_edit = QDateEdit(self)
            self.start_date_edit.setCalendarPopup(True)
            self.start_date_edit.setDate(QDate.currentDate().addMonths(-1))
            self.start_date_edit.editingFinished.connect(self.load_logs)  # Dynamic filtering on date change

            self.end_date_edit = QDateEdit(self)
            self.end_date_edit.setCalendarPopup(True)
            self.end_date_edit.setDate(QDate.currentDate())
            self.end_date_edit.editingFinished.connect(self.load_logs)  # Dynamic filtering on date change

            # Text search filter
            self.text_search_edit = QLineEdit(self)
            self.text_search_edit.setPlaceholderText("Buscar texto en los logs...")
            self.text_search_edit.textChanged.connect(self.load_logs)  # Dynamic filtering on text change

            # Error selection list (initially hidden)
            self.error_list = QListWidget(self)
            self.error_list.setSelectionMode(QListWidget.MultiSelection)
            self.error_list.setVisible(False)  # Initially hide the error list
            self.error_list.itemSelectionChanged.connect(self.load_logs)  # Dynamic filtering on selection change

            for code, description in ERROR_CODES_DICT.items():
                item = QListWidgetItem(f"[{code}] {description}")
                item.setData(1, code)  # Store only the error code as data
                self.error_list.addItem(item)

            # Source selection list
            self.source_list = QListWidget(self)
            self.source_list.setSelectionMode(QListWidget.MultiSelection)
            self.source_list.setVisible(False)  # Initially hide the source list
            self.source_list.itemSelectionChanged.connect(self.load_logs)  # Dynamic filtering on selection change

            sources = ["programa", "icono", "servicio"]
            for source in sources:
                item = QListWidgetItem(source)
                item.setData(1, source)  # Store the source as data
                self.source_list.addItem(item)

            # Button to toggle the filter lists visibility with an SVG icon
            self.toggle_filter_button = QPushButton(self)
            self.file_path_filter = os.path.join(self.file_path_resources, "window", "filter-right.svg")
            self.toggle_filter_button.setIcon(QIcon(self.file_path_filter))  # Set SVG icon
            self.toggle_filter_button.clicked.connect(self.toggle_filter_visibility)

            self.select_errors_label = QLabel("Selecciona los errores a filtrar (vacío = todos):")
            self.select_errors_label.setVisible(False)

            self.select_sources_label = QLabel("Selecciona las fuentes a filtrar (vacío = todas):")
            self.select_sources_label.setVisible(False)

            # Layout for filters
            filter_layout = QHBoxLayout()
            filter_layout.addWidget(QLabel("Desde:"))
            filter_layout.addWidget(self.start_date_edit)
            filter_layout.addWidget(QLabel("Hasta:"))
            filter_layout.addWidget(self.end_date_edit)
            filter_layout.addWidget(QLabel("Buscar:"))
            filter_layout.addWidget(self.text_search_edit)
            filter_layout.addWidget(self.toggle_filter_button)  # Button to toggle the filter lists

            # Layout for error list
            error_list_layout = QVBoxLayout()
            error_list_layout.addWidget(self.select_errors_label)
            error_list_layout.addWidget(self.error_list)
            error_list_layout.setStretch(0, 0)
            error_list_layout.setStretch(1, 1)

            # Layout for source list
            source_list_layout = QVBoxLayout()
            source_list_layout.addWidget(self.select_sources_label)
            source_list_layout.addWidget(self.source_list)
            source_list_layout.setStretch(0, 0)
            source_list_layout.setStretch(1, 1)

            # Combine error and source list layouts
            filter_lists_layout = QHBoxLayout()
            filter_lists_layout.addLayout(error_list_layout)
            filter_lists_layout.addLayout(source_list_layout)

            # Text widget to display logs
            self.text_edit = QTextEdit(self)
            self.text_edit.setReadOnly(True)

            # Main layout
            layout = QVBoxLayout()
            layout.addLayout(filter_layout)
            layout.addLayout(filter_lists_layout)
            layout.addWidget(self.text_edit)
            self.setLayout(layout)
            layout.setStretch(0, 0)  # filter_layout takes only the space it needs
            layout.setStretch(1, 0)  # filter_lists_layout takes only the space it needs
            layout.setStretch(2, 1)  # text_edit (logs) expands to fill the remaining space

            # Load logs at startup
            self.load_logs()
        except Exception as e:
            raise BaseError(3501, str(e), parent=self)

    def toggle_filter_visibility(self):
        """
        Toggles the visibility of filter-related UI elements in the logs dialog.

        This method switches the visibility state of the following elements:
            - `select_errors_label`
            - `error_list`
            - `select_sources_label`
            - `source_list`

        If an exception occurs during the process, it raises a `BaseError` with
        an error code of 3500 and the exception message.

        Raises:
            BaseError: If an exception occurs while toggling visibility.
        """
        try:
            self.select_errors_label.setVisible(not self.select_errors_label.isVisible())
            self.error_list.setVisible(not self.error_list.isVisible())
            self.select_sources_label.setVisible(not self.select_sources_label.isVisible())
            self.source_list.setVisible(not self.source_list.isVisible())
        except Exception as e:
            raise BaseError(3500, str(e))

    def load_logs(self):
        """
        Loads and displays error logs based on the specified filters.

        This method retrieves error logs within a specified date range, 
        filtered by selected error types, sources, and a search text. 

        The logs are then displayed in a text editor.

        Raises:
            BaseError: If an exception occurs during the log retrieval process.

        Filters:
            - Date range: Defined by `start_date_edit` and `end_date_edit`.
            - Error types: Selected items in `error_list`.
            - Sources: Selected items in `source_list`.
            - Search text: Text entered in `text_search_edit`.

        Steps:
            1. Retrieve the start and end dates from the date edit widgets.
            2. Get the search text and convert it to lowercase.
            3. Collect selected error types and sources from their respective lists.
            4. Fetch the filtered error logs using `get_error_logs`.
            5. Display the logs in the `text_edit` widget.

        Note:
            Ensure that the `get_error_logs` method is implemented to handle 
            the filtering logic and return the appropriate logs.
        """
        try:
            start_date = self.start_date_edit.date().toString("yyyy-MM-dd")
            end_date = self.end_date_edit.date().toString("yyyy-MM-dd")
            search_text = self.text_search_edit.text().lower()

            selected_errors = {item.data(1) for item in self.error_list.selectedItems()}
            selected_sources = {item.data(1) for item in self.source_list.selectedItems()}

            error_logs = self.get_error_logs(start_date, end_date, selected_errors, selected_sources, search_text)
            self.text_edit.setPlainText("\n".join(error_logs))
        except Exception as e:
            raise BaseError(3500, str(e))

    def get_error_logs(self, start_date, end_date, selected_errors, selected_sources, search_text):
        """
        Retrieves error log entries from log files within a specified date range, filtered by error codes, sources, 
        and optional search text.

        Args:
            start_date (str): The start date in the format 'YYYY-MM-DD' to filter log entries.
            end_date (str): The end date in the format 'YYYY-MM-DD' to filter log entries.
            selected_errors (list): A list of error codes to filter log entries. If empty, all error codes are included.
            selected_sources (list): A list of sources to filter log entries. If empty, all sources are included.
            search_text (str): Optional text to search within log entries. If empty, no search filtering is applied.

        Returns:
            (list): A list of formatted error log entries that match the specified filters, sorted by date and time.

        Raises:
            BaseError: If an exception occurs during log processing, it raises a BaseError with code 3500 and the error message.
        """
        try:
            error_entries = []

            pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) - \w+ - \[(\d{4})\]")  # Capture date, time, and error code

            log_files = {
                "programa": "programa_reloj_de_asistencias_" + PROGRAM_VERSION + "_error.log",
                "icono": "icono_reloj_de_asistencias_" + SERVICE_VERSION + "_error.log",
                "servicio": "servicio_reloj_de_asistencias_" + SERVICE_VERSION + "_error.log"
            }

            for folder in os.listdir(LOGS_DIR):
                folder_path = os.path.join(LOGS_DIR, folder)
                if os.path.isdir(folder_path):
                    for source, log_file in log_files.items():
                        if selected_sources and source not in selected_sources:
                            continue
                        log_path = os.path.join(folder_path, log_file)
                        if os.path.exists(log_path):
                            with open(log_path, "r", encoding="utf-8", errors="replace") as log_file:
                                for line in log_file:
                                    match = pattern.search(line)
                                    if match:
                                        log_datetime, error_code = match.groups()
                                        log_date = log_datetime.split()[0]
                                        if start_date <= log_date <= end_date:
                                            # Show all errors if none are selected, otherwise filter
                                            if (not selected_errors or error_code in selected_errors) and (not search_text or search_text in line.lower()):
                                                error_entries.append(f"{source}: {line.strip()}")

            # Sort the entries by date and time
            error_entries.sort(key=lambda x: re.search(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})", x).group(1))

            return error_entries
        except Exception as e:
            raise BaseError(3500, str(e))
__init__()

Initializes the LogsDialog class.

This constructor sets up the LogsDialog instance by calling the parent class initializer with a specific window title, initializing the user interface, and handling any exceptions that may occur during the process.

Raises:

Type Description
BaseError

If an exception occurs during initialization, it is wrapped in a BaseError with code 3501 and the exception message.

Source code in src\ui\logs_dialog.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def __init__(self):
    """
    Initializes the LogsDialog class.

    This constructor sets up the LogsDialog instance by calling the parent
    class initializer with a specific window title, initializing the user
    interface, and handling any exceptions that may occur during the process.

    Raises:
        BaseError: If an exception occurs during initialization, it is wrapped
                   in a BaseError with code 3501 and the exception message.
    """
    try:
        super().__init__(window_title="VISOR DE LOGS")
        self.init_ui()
        super().init_ui()
    except Exception as e:
        raise BaseError(3501, str(e))
get_error_logs(start_date, end_date, selected_errors, selected_sources, search_text)

Retrieves error log entries from log files within a specified date range, filtered by error codes, sources, and optional search text.

Parameters:

Name Type Description Default
start_date str

The start date in the format 'YYYY-MM-DD' to filter log entries.

required
end_date str

The end date in the format 'YYYY-MM-DD' to filter log entries.

required
selected_errors list

A list of error codes to filter log entries. If empty, all error codes are included.

required
selected_sources list

A list of sources to filter log entries. If empty, all sources are included.

required
search_text str

Optional text to search within log entries. If empty, no search filtering is applied.

required

Returns:

Type Description
list

A list of formatted error log entries that match the specified filters, sorted by date and time.

Raises:

Type Description
BaseError

If an exception occurs during log processing, it raises a BaseError with code 3500 and the error message.

Source code in src\ui\logs_dialog.py
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
def get_error_logs(self, start_date, end_date, selected_errors, selected_sources, search_text):
    """
    Retrieves error log entries from log files within a specified date range, filtered by error codes, sources, 
    and optional search text.

    Args:
        start_date (str): The start date in the format 'YYYY-MM-DD' to filter log entries.
        end_date (str): The end date in the format 'YYYY-MM-DD' to filter log entries.
        selected_errors (list): A list of error codes to filter log entries. If empty, all error codes are included.
        selected_sources (list): A list of sources to filter log entries. If empty, all sources are included.
        search_text (str): Optional text to search within log entries. If empty, no search filtering is applied.

    Returns:
        (list): A list of formatted error log entries that match the specified filters, sorted by date and time.

    Raises:
        BaseError: If an exception occurs during log processing, it raises a BaseError with code 3500 and the error message.
    """
    try:
        error_entries = []

        pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}) - \w+ - \[(\d{4})\]")  # Capture date, time, and error code

        log_files = {
            "programa": "programa_reloj_de_asistencias_" + PROGRAM_VERSION + "_error.log",
            "icono": "icono_reloj_de_asistencias_" + SERVICE_VERSION + "_error.log",
            "servicio": "servicio_reloj_de_asistencias_" + SERVICE_VERSION + "_error.log"
        }

        for folder in os.listdir(LOGS_DIR):
            folder_path = os.path.join(LOGS_DIR, folder)
            if os.path.isdir(folder_path):
                for source, log_file in log_files.items():
                    if selected_sources and source not in selected_sources:
                        continue
                    log_path = os.path.join(folder_path, log_file)
                    if os.path.exists(log_path):
                        with open(log_path, "r", encoding="utf-8", errors="replace") as log_file:
                            for line in log_file:
                                match = pattern.search(line)
                                if match:
                                    log_datetime, error_code = match.groups()
                                    log_date = log_datetime.split()[0]
                                    if start_date <= log_date <= end_date:
                                        # Show all errors if none are selected, otherwise filter
                                        if (not selected_errors or error_code in selected_errors) and (not search_text or search_text in line.lower()):
                                            error_entries.append(f"{source}: {line.strip()}")

        # Sort the entries by date and time
        error_entries.sort(key=lambda x: re.search(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})", x).group(1))

        return error_entries
    except Exception as e:
        raise BaseError(3500, str(e))
init_ui()

Initializes the user interface for the logs dialog. This method sets up the UI components, including date selection widgets, text search filter, error and source selection lists, and a text display area for logs. It also configures the layout and connects signals to dynamically filter logs based on user input.

UI Components
  • Date Selection Widgets:
    • start_date_edit: QDateEdit for selecting the start date.
    • end_date_edit: QDateEdit for selecting the end date.
  • Text Search Filter:
    • text_search_edit: QLineEdit for searching text in logs.
  • Error Selection List:
    • error_list: QListWidget for selecting error codes to filter logs.
  • Source Selection List:
    • source_list: QListWidget for selecting log sources to filter logs.
  • Toggle Filter Button:
    • toggle_filter_button: QPushButton to toggle the visibility of error and source filter lists.
  • Labels:
    • select_errors_label: QLabel for error filter instructions.
    • select_sources_label: QLabel for source filter instructions.
  • Logs Display:
    • text_edit: QTextEdit for displaying filtered logs.
Layout
  • filter_layout: Horizontal layout for date selection, text search, and toggle filter button.
  • error_list_layout: Vertical layout for error filter label and list.
  • source_list_layout: Vertical layout for source filter label and list.
  • filter_lists_layout: Horizontal layout combining error and source filter layouts.
  • layout: Main vertical layout combining all components.
Behavior
  • Connects signals to dynamically filter logs when:
    • Dates are changed.
    • Text is entered in the search field.
    • Error or source selections are modified.
  • Loads logs at startup.

Raises:

Type Description
BaseError

If an exception occurs during UI initialization.

Source code in src\ui\logs_dialog.py
 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
def init_ui(self):
    """
    Initializes the user interface for the logs dialog.
    This method sets up the UI components, including date selection widgets, 
    text search filter, error and source selection lists, and a text display 
    area for logs. It also configures the layout and connects signals to 
    dynamically filter logs based on user input.

    UI Components:
        - Date Selection Widgets:
            * `start_date_edit`: QDateEdit for selecting the start date.
            * `end_date_edit`: QDateEdit for selecting the end date.
        - Text Search Filter:
            * `text_search_edit`: QLineEdit for searching text in logs.
        - Error Selection List:
            * `error_list`: QListWidget for selecting error codes to filter logs.
        - Source Selection List:
            * `source_list`: QListWidget for selecting log sources to filter logs.
        - Toggle Filter Button:
            * `toggle_filter_button`: QPushButton to toggle the visibility of 
              error and source filter lists.
        - Labels:
            * `select_errors_label`: QLabel for error filter instructions.
            * `select_sources_label`: QLabel for source filter instructions.
        - Logs Display:
            * `text_edit`: QTextEdit for displaying filtered logs.

    Layout:
        - `filter_layout`: Horizontal layout for date selection, text search, 
          and toggle filter button.
        - `error_list_layout`: Vertical layout for error filter label and list.
        - `source_list_layout`: Vertical layout for source filter label and list.
        - `filter_lists_layout`: Horizontal layout combining error and source 
          filter layouts.
        - `layout`: Main vertical layout combining all components.

    Behavior:
        - Connects signals to dynamically filter logs when:
            * Dates are changed.
            * Text is entered in the search field.
            * Error or source selections are modified.
        - Loads logs at startup.

    Raises:
        BaseError: If an exception occurs during UI initialization.
    """
    try:
        # Date selection widgets
        self.start_date_edit = QDateEdit(self)
        self.start_date_edit.setCalendarPopup(True)
        self.start_date_edit.setDate(QDate.currentDate().addMonths(-1))
        self.start_date_edit.editingFinished.connect(self.load_logs)  # Dynamic filtering on date change

        self.end_date_edit = QDateEdit(self)
        self.end_date_edit.setCalendarPopup(True)
        self.end_date_edit.setDate(QDate.currentDate())
        self.end_date_edit.editingFinished.connect(self.load_logs)  # Dynamic filtering on date change

        # Text search filter
        self.text_search_edit = QLineEdit(self)
        self.text_search_edit.setPlaceholderText("Buscar texto en los logs...")
        self.text_search_edit.textChanged.connect(self.load_logs)  # Dynamic filtering on text change

        # Error selection list (initially hidden)
        self.error_list = QListWidget(self)
        self.error_list.setSelectionMode(QListWidget.MultiSelection)
        self.error_list.setVisible(False)  # Initially hide the error list
        self.error_list.itemSelectionChanged.connect(self.load_logs)  # Dynamic filtering on selection change

        for code, description in ERROR_CODES_DICT.items():
            item = QListWidgetItem(f"[{code}] {description}")
            item.setData(1, code)  # Store only the error code as data
            self.error_list.addItem(item)

        # Source selection list
        self.source_list = QListWidget(self)
        self.source_list.setSelectionMode(QListWidget.MultiSelection)
        self.source_list.setVisible(False)  # Initially hide the source list
        self.source_list.itemSelectionChanged.connect(self.load_logs)  # Dynamic filtering on selection change

        sources = ["programa", "icono", "servicio"]
        for source in sources:
            item = QListWidgetItem(source)
            item.setData(1, source)  # Store the source as data
            self.source_list.addItem(item)

        # Button to toggle the filter lists visibility with an SVG icon
        self.toggle_filter_button = QPushButton(self)
        self.file_path_filter = os.path.join(self.file_path_resources, "window", "filter-right.svg")
        self.toggle_filter_button.setIcon(QIcon(self.file_path_filter))  # Set SVG icon
        self.toggle_filter_button.clicked.connect(self.toggle_filter_visibility)

        self.select_errors_label = QLabel("Selecciona los errores a filtrar (vacío = todos):")
        self.select_errors_label.setVisible(False)

        self.select_sources_label = QLabel("Selecciona las fuentes a filtrar (vacío = todas):")
        self.select_sources_label.setVisible(False)

        # Layout for filters
        filter_layout = QHBoxLayout()
        filter_layout.addWidget(QLabel("Desde:"))
        filter_layout.addWidget(self.start_date_edit)
        filter_layout.addWidget(QLabel("Hasta:"))
        filter_layout.addWidget(self.end_date_edit)
        filter_layout.addWidget(QLabel("Buscar:"))
        filter_layout.addWidget(self.text_search_edit)
        filter_layout.addWidget(self.toggle_filter_button)  # Button to toggle the filter lists

        # Layout for error list
        error_list_layout = QVBoxLayout()
        error_list_layout.addWidget(self.select_errors_label)
        error_list_layout.addWidget(self.error_list)
        error_list_layout.setStretch(0, 0)
        error_list_layout.setStretch(1, 1)

        # Layout for source list
        source_list_layout = QVBoxLayout()
        source_list_layout.addWidget(self.select_sources_label)
        source_list_layout.addWidget(self.source_list)
        source_list_layout.setStretch(0, 0)
        source_list_layout.setStretch(1, 1)

        # Combine error and source list layouts
        filter_lists_layout = QHBoxLayout()
        filter_lists_layout.addLayout(error_list_layout)
        filter_lists_layout.addLayout(source_list_layout)

        # Text widget to display logs
        self.text_edit = QTextEdit(self)
        self.text_edit.setReadOnly(True)

        # Main layout
        layout = QVBoxLayout()
        layout.addLayout(filter_layout)
        layout.addLayout(filter_lists_layout)
        layout.addWidget(self.text_edit)
        self.setLayout(layout)
        layout.setStretch(0, 0)  # filter_layout takes only the space it needs
        layout.setStretch(1, 0)  # filter_lists_layout takes only the space it needs
        layout.setStretch(2, 1)  # text_edit (logs) expands to fill the remaining space

        # Load logs at startup
        self.load_logs()
    except Exception as e:
        raise BaseError(3501, str(e), parent=self)
load_logs()

Loads and displays error logs based on the specified filters.

This method retrieves error logs within a specified date range, filtered by selected error types, sources, and a search text.

The logs are then displayed in a text editor.

Raises:

Type Description
BaseError

If an exception occurs during the log retrieval process.

Filters
  • Date range: Defined by start_date_edit and end_date_edit.
  • Error types: Selected items in error_list.
  • Sources: Selected items in source_list.
  • Search text: Text entered in text_search_edit.
Steps
  1. Retrieve the start and end dates from the date edit widgets.
  2. Get the search text and convert it to lowercase.
  3. Collect selected error types and sources from their respective lists.
  4. Fetch the filtered error logs using get_error_logs.
  5. Display the logs in the text_edit widget.
Note

Ensure that the get_error_logs method is implemented to handle the filtering logic and return the appropriate logs.

Source code in src\ui\logs_dialog.py
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
def load_logs(self):
    """
    Loads and displays error logs based on the specified filters.

    This method retrieves error logs within a specified date range, 
    filtered by selected error types, sources, and a search text. 

    The logs are then displayed in a text editor.

    Raises:
        BaseError: If an exception occurs during the log retrieval process.

    Filters:
        - Date range: Defined by `start_date_edit` and `end_date_edit`.
        - Error types: Selected items in `error_list`.
        - Sources: Selected items in `source_list`.
        - Search text: Text entered in `text_search_edit`.

    Steps:
        1. Retrieve the start and end dates from the date edit widgets.
        2. Get the search text and convert it to lowercase.
        3. Collect selected error types and sources from their respective lists.
        4. Fetch the filtered error logs using `get_error_logs`.
        5. Display the logs in the `text_edit` widget.

    Note:
        Ensure that the `get_error_logs` method is implemented to handle 
        the filtering logic and return the appropriate logs.
    """
    try:
        start_date = self.start_date_edit.date().toString("yyyy-MM-dd")
        end_date = self.end_date_edit.date().toString("yyyy-MM-dd")
        search_text = self.text_search_edit.text().lower()

        selected_errors = {item.data(1) for item in self.error_list.selectedItems()}
        selected_sources = {item.data(1) for item in self.source_list.selectedItems()}

        error_logs = self.get_error_logs(start_date, end_date, selected_errors, selected_sources, search_text)
        self.text_edit.setPlainText("\n".join(error_logs))
    except Exception as e:
        raise BaseError(3500, str(e))
toggle_filter_visibility()

Toggles the visibility of filter-related UI elements in the logs dialog.

This method switches the visibility state of the following elements
  • select_errors_label
  • error_list
  • select_sources_label
  • source_list

If an exception occurs during the process, it raises a BaseError with an error code of 3500 and the exception message.

Raises:

Type Description
BaseError

If an exception occurs while toggling visibility.

Source code in src\ui\logs_dialog.py
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def toggle_filter_visibility(self):
    """
    Toggles the visibility of filter-related UI elements in the logs dialog.

    This method switches the visibility state of the following elements:
        - `select_errors_label`
        - `error_list`
        - `select_sources_label`
        - `source_list`

    If an exception occurs during the process, it raises a `BaseError` with
    an error code of 3500 and the exception message.

    Raises:
        BaseError: If an exception occurs while toggling visibility.
    """
    try:
        self.select_errors_label.setVisible(not self.select_errors_label.isVisible())
        self.error_list.setVisible(not self.error_list.isVisible())
        self.select_sources_label.setVisible(not self.select_sources_label.isVisible())
        self.source_list.setVisible(not self.source_list.isVisible())
    except Exception as e:
        raise BaseError(3500, str(e))

message_box

MessageBox

Bases: QMessageBox

Source code in src\ui\message_box.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class MessageBox(QMessageBox):
    def __init__(self, icon, text, parent=None):
        """
        Initializes the custom message box with the specified icon, text, and optional parent widget.

        Args:
            icon (QMessageBox.Icon): The icon to display in the message box.
            text (str): The text message to display in the message box.
            parent (QWidget, optional): The parent widget of the message box. Defaults to None.

        Raises:
            BaseError: If an error occurs during initialization, a BaseError with code 3501 is raised.
        """
        try:
            super().__init__(icon, 'Programa Reloj de Asistencias', text, parent)

            file_path = os.path.join(find_marker_directory("resources"), "resources", "fingerprint.ico")
            self.setWindowIcon(QIcon(file_path))
        except Exception as e:
            raise BaseError(3501, str(e))
__init__(icon, text, parent=None)

Initializes the custom message box with the specified icon, text, and optional parent widget.

Parameters:

Name Type Description Default
icon Icon

The icon to display in the message box.

required
text str

The text message to display in the message box.

required
parent QWidget

The parent widget of the message box. Defaults to None.

None

Raises:

Type Description
BaseError

If an error occurs during initialization, a BaseError with code 3501 is raised.

Source code in src\ui\message_box.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def __init__(self, icon, text, parent=None):
    """
    Initializes the custom message box with the specified icon, text, and optional parent widget.

    Args:
        icon (QMessageBox.Icon): The icon to display in the message box.
        text (str): The text message to display in the message box.
        parent (QWidget, optional): The parent widget of the message box. Defaults to None.

    Raises:
        BaseError: If an error occurs during initialization, a BaseError with code 3501 is raised.
    """
    try:
        super().__init__(icon, 'Programa Reloj de Asistencias', text, parent)

        file_path = os.path.join(find_marker_directory("resources"), "resources", "fingerprint.ico")
        self.setWindowIcon(QIcon(file_path))
    except Exception as e:
        raise BaseError(3501, str(e))

modify_device_dialog

AddDevicesDialog

Bases: QDialog

Source code in src\ui\modify_device_dialog.py
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
class AddDevicesDialog(QDialog):
    def __init__(self, parent=None, id=0):
        """
        Initializes the ModifyDeviceDialog class.

        Args:
            parent (QWidget, optional): The parent widget for this dialog. Defaults to None.
            id (int, optional): The identifier for the device. Defaults to 0.

        Raises:
            BaseError: Custom error raised with code 3501 if an exception occurs during initialization.
        """
        try:
            super().__init__(parent)
            self.setWindowTitle("Agregar nuevo dispositivo")
            self.setMinimumSize(400, 300)
            self.id = id
            self.init_ui()
        except Exception as e:
            raise BaseError(3501, str(e))

    def init_ui(self):
        """
        Initializes the user interface for the Modify Device Dialog.
        This method sets up the layout and widgets for the dialog, including input fields
        for district, model, point of marking, IP address, and communication type. It also
        includes a button to confirm the action.

        Widgets:
            - QLineEdit for district input.
            - QLineEdit for model input.
            - QLineEdit for point of marking input.
            - QLineEdit for IP address input.
            - QComboBox for selecting the communication type (TCP/UDP).
            - QPushButton for confirming the action.

        Layout:
            - Uses QVBoxLayout as the main layout.
            - Uses QFormLayout for organizing input fields and labels.

        Signals:
            - Connects the QComboBox `currentIndexChanged` signal to the `on_combobox_changed` slot.

        Attributes:
            self.district_edit (QLineEdit): Input field for the district.
            self.model_edit (QLineEdit): Input field for the model.
            self.point_edit (QLineEdit): Input field for the point of marking.
            self.ip_edit (QLineEdit): Input field for the IP address.
            self.combo_box (QComboBox): Dropdown for selecting the communication type.
            self.communication (str): Stores the current communication type selected in the combo box.
            self.active (bool): Indicates if the device is active (default is True).
            self.battery (bool): Indicates if the device has a battery (default is True).
            self.btn_add (QPushButton): Button to confirm and accept the dialog.
        """
        layout = QVBoxLayout(self)

        form_layout = QFormLayout()

        self.district_edit = QLineEdit(self)
        self.model_edit = QLineEdit(self)
        self.point_edit = QLineEdit(self)
        self.ip_edit = QLineEdit(self)
        # Create a QComboBox
        self.combo_box = QComboBox()
        # Add items to the QComboBox
        self.combo_box.addItem("TCP")
        self.combo_box.addItem("UDP")
        # Connect the QComboBox signal to a slot
        self.combo_box.currentIndexChanged.connect(self.on_combobox_changed)
        self.communication = self.combo_box.currentText()

        self.active = True
        self.battery = True

        form_layout.addRow("Distrito:", self.district_edit)
        form_layout.addRow("Modelo:", self.model_edit)
        form_layout.addRow("Punto de Marcación:", self.point_edit)
        form_layout.addRow("IP:", self.ip_edit)
        form_layout.addRow("Comunicación:", self.combo_box)

        layout.addLayout(form_layout)

        self.btn_add = QPushButton("Agregar", self)
        self.btn_add.clicked.connect(self.accept)
        layout.addWidget(self.btn_add)

        self.setLayout(layout)

    def on_combobox_changed(self, index):
        """
        Handles the event triggered when the selected index of the combo box changes.

        Args:
            index (int): The index of the newly selected item in the combo box.

        Side Effects:
            Updates the `self.communication` attribute with the text of the currently selected option
            in the combo box.
        """
        # Get the text of the selected option
        self.communication = self.combo_box.currentText()
        #logging.debug(self.communication)

    def get_data(self):
        """
        Retrieves and returns the data from the dialog fields.

        Returns:
            (tuple): A tuple containing the following elements:

                - str: The district name in uppercase (from `district_edit`).
                - str: The model name (from `model_edit`).
                - str: The point name in uppercase (from `point_edit`).
                - str: The IP address (from `ip_edit`).
                - Any: The device ID (`self.id`).
                - Any: The communication type or status (`self.communication`).
                - Any: The battery status (`self.battery`).
                - Any: The active status (`self.active`).
        """
        return (
            self.district_edit.text().upper(),
            self.model_edit.text(),
            self.point_edit.text().upper(),
            self.ip_edit.text(),
            self.id,
            self.communication,
            self.battery,
            self.active
        )
__init__(parent=None, id=0)

Initializes the ModifyDeviceDialog class.

Parameters:

Name Type Description Default
parent QWidget

The parent widget for this dialog. Defaults to None.

None
id int

The identifier for the device. Defaults to 0.

0

Raises:

Type Description
BaseError

Custom error raised with code 3501 if an exception occurs during initialization.

Source code in src\ui\modify_device_dialog.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
def __init__(self, parent=None, id=0):
    """
    Initializes the ModifyDeviceDialog class.

    Args:
        parent (QWidget, optional): The parent widget for this dialog. Defaults to None.
        id (int, optional): The identifier for the device. Defaults to 0.

    Raises:
        BaseError: Custom error raised with code 3501 if an exception occurs during initialization.
    """
    try:
        super().__init__(parent)
        self.setWindowTitle("Agregar nuevo dispositivo")
        self.setMinimumSize(400, 300)
        self.id = id
        self.init_ui()
    except Exception as e:
        raise BaseError(3501, str(e))
get_data()

Retrieves and returns the data from the dialog fields.

Returns:

Type Description
tuple

A tuple containing the following elements:

  • str: The district name in uppercase (from district_edit).
  • str: The model name (from model_edit).
  • str: The point name in uppercase (from point_edit).
  • str: The IP address (from ip_edit).
  • Any: The device ID (self.id).
  • Any: The communication type or status (self.communication).
  • Any: The battery status (self.battery).
  • Any: The active status (self.active).
Source code in src\ui\modify_device_dialog.py
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
def get_data(self):
    """
    Retrieves and returns the data from the dialog fields.

    Returns:
        (tuple): A tuple containing the following elements:

            - str: The district name in uppercase (from `district_edit`).
            - str: The model name (from `model_edit`).
            - str: The point name in uppercase (from `point_edit`).
            - str: The IP address (from `ip_edit`).
            - Any: The device ID (`self.id`).
            - Any: The communication type or status (`self.communication`).
            - Any: The battery status (`self.battery`).
            - Any: The active status (`self.active`).
    """
    return (
        self.district_edit.text().upper(),
        self.model_edit.text(),
        self.point_edit.text().upper(),
        self.ip_edit.text(),
        self.id,
        self.communication,
        self.battery,
        self.active
    )
init_ui()

Initializes the user interface for the Modify Device Dialog. This method sets up the layout and widgets for the dialog, including input fields for district, model, point of marking, IP address, and communication type. It also includes a button to confirm the action.

Widgets
  • QLineEdit for district input.
  • QLineEdit for model input.
  • QLineEdit for point of marking input.
  • QLineEdit for IP address input.
  • QComboBox for selecting the communication type (TCP/UDP).
  • QPushButton for confirming the action.
Layout
  • Uses QVBoxLayout as the main layout.
  • Uses QFormLayout for organizing input fields and labels.
Signals
  • Connects the QComboBox currentIndexChanged signal to the on_combobox_changed slot.

Attributes:

Name Type Description
self.district_edit QLineEdit

Input field for the district.

self.model_edit QLineEdit

Input field for the model.

self.point_edit QLineEdit

Input field for the point of marking.

self.ip_edit QLineEdit

Input field for the IP address.

self.combo_box QComboBox

Dropdown for selecting the communication type.

self.communication str

Stores the current communication type selected in the combo box.

self.active bool

Indicates if the device is active (default is True).

self.battery bool

Indicates if the device has a battery (default is True).

self.btn_add QPushButton

Button to confirm and accept the dialog.

Source code in src\ui\modify_device_dialog.py
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
def init_ui(self):
    """
    Initializes the user interface for the Modify Device Dialog.
    This method sets up the layout and widgets for the dialog, including input fields
    for district, model, point of marking, IP address, and communication type. It also
    includes a button to confirm the action.

    Widgets:
        - QLineEdit for district input.
        - QLineEdit for model input.
        - QLineEdit for point of marking input.
        - QLineEdit for IP address input.
        - QComboBox for selecting the communication type (TCP/UDP).
        - QPushButton for confirming the action.

    Layout:
        - Uses QVBoxLayout as the main layout.
        - Uses QFormLayout for organizing input fields and labels.

    Signals:
        - Connects the QComboBox `currentIndexChanged` signal to the `on_combobox_changed` slot.

    Attributes:
        self.district_edit (QLineEdit): Input field for the district.
        self.model_edit (QLineEdit): Input field for the model.
        self.point_edit (QLineEdit): Input field for the point of marking.
        self.ip_edit (QLineEdit): Input field for the IP address.
        self.combo_box (QComboBox): Dropdown for selecting the communication type.
        self.communication (str): Stores the current communication type selected in the combo box.
        self.active (bool): Indicates if the device is active (default is True).
        self.battery (bool): Indicates if the device has a battery (default is True).
        self.btn_add (QPushButton): Button to confirm and accept the dialog.
    """
    layout = QVBoxLayout(self)

    form_layout = QFormLayout()

    self.district_edit = QLineEdit(self)
    self.model_edit = QLineEdit(self)
    self.point_edit = QLineEdit(self)
    self.ip_edit = QLineEdit(self)
    # Create a QComboBox
    self.combo_box = QComboBox()
    # Add items to the QComboBox
    self.combo_box.addItem("TCP")
    self.combo_box.addItem("UDP")
    # Connect the QComboBox signal to a slot
    self.combo_box.currentIndexChanged.connect(self.on_combobox_changed)
    self.communication = self.combo_box.currentText()

    self.active = True
    self.battery = True

    form_layout.addRow("Distrito:", self.district_edit)
    form_layout.addRow("Modelo:", self.model_edit)
    form_layout.addRow("Punto de Marcación:", self.point_edit)
    form_layout.addRow("IP:", self.ip_edit)
    form_layout.addRow("Comunicación:", self.combo_box)

    layout.addLayout(form_layout)

    self.btn_add = QPushButton("Agregar", self)
    self.btn_add.clicked.connect(self.accept)
    layout.addWidget(self.btn_add)

    self.setLayout(layout)
on_combobox_changed(index)

Handles the event triggered when the selected index of the combo box changes.

Parameters:

Name Type Description Default
index int

The index of the newly selected item in the combo box.

required
Side Effects

Updates the self.communication attribute with the text of the currently selected option in the combo box.

Source code in src\ui\modify_device_dialog.py
430
431
432
433
434
435
436
437
438
439
440
441
442
def on_combobox_changed(self, index):
    """
    Handles the event triggered when the selected index of the combo box changes.

    Args:
        index (int): The index of the newly selected item in the combo box.

    Side Effects:
        Updates the `self.communication` attribute with the text of the currently selected option
        in the combo box.
    """
    # Get the text of the selected option
    self.communication = self.combo_box.currentText()

ModifyDevicesDialog

Bases: BaseDialog

Source code in src\ui\modify_device_dialog.py
 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
class ModifyDevicesDialog(BaseDialog):
    def __init__(self, parent=None):
        """
        Initializes the ModifyDeviceDialog class.

        Args:
            parent (QWidget, optional): The parent widget for this dialog. Defaults to None.

        Attributes:
            file_path (str): The path to the "info_devices.txt" file, determined dynamically.
            data (list): A list to store device data.
            max_id (int): The maximum ID value among the devices.

        Raises:
            BaseError: If an exception occurs during initialization, it raises a BaseError with code 3501.
        """
        try:
            super().__init__(parent, window_title="MODIFICAR DISPOSITIVOS")

            self.file_path = os.path.join(find_root_directory(), "info_devices.txt")
            self.data = []
            self.max_id = 0
            self.init_ui()
            super().init_ui()
        except Exception as e:
            raise BaseError(3501, str(e))

    def init_ui(self):
        """
        Initializes the user interface for the Modify Device Dialog.
        This method sets up the layout and widgets for the dialog, including a table
        for displaying device information and buttons for performing various actions
        such as loading, modifying, adding, activating, and deactivating devices.

        Widgets:
            - QTableWidget: Displays device information with 8 columns:
                ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación", 
                "Pila funcionando", "Activado"]. Columns are resizable, sortable, and editable on double-click.
            - QPushButton: Buttons for the following actions:
                - "Cargar": Loads and displays data in the table.
                - "Modificar": Saves modifications made to the data.
                - "Agregar": Adds a new device entry.
                - "Activar todo": Activates all devices.
                - "Desactivar todo": Deactivates all devices.

        Layout:
            - QVBoxLayout: Main layout containing the table widget and button layout.
            - QHBoxLayout: Layout for arranging the action buttons horizontally.

        Notes:
            - Buttons are configured to not retain focus after being clicked.
            - The `load_data_and_show` method is called at the end to populate the table with initial data.
        """
        layout = QVBoxLayout(self)

        self.table_widget = QTableWidget()
        self.table_widget.setColumnCount(8)
        self.table_widget.setHorizontalHeaderLabels(["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación", "Pila funcionando", "Activado"])
        self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.table_widget.horizontalHeader().setStretchLastSection(True)
        self.table_widget.setEditTriggers(QTableWidget.DoubleClicked)
        self.table_widget.setSortingEnabled(True)

        layout.addWidget(self.table_widget)

        button_layout = QHBoxLayout()

        self.btn_load_data = QPushButton("Cargar", self)
        self.btn_load_data.clicked.connect(self.load_data_and_show)
        button_layout.addWidget(self.btn_load_data)

        self.btn_save_data = QPushButton("Modificar", self)
        self.btn_save_data.clicked.connect(self.save_data)
        button_layout.addWidget(self.btn_save_data)

        self.btn_add_data = QPushButton("Agregar", self)
        self.btn_add_data.clicked.connect(self.add_device)
        button_layout.addWidget(self.btn_add_data)

        self.btn_activate_all = QPushButton("Activar todo", self)
        self.btn_activate_all.clicked.connect(self.activate_all)
        button_layout.addWidget(self.btn_activate_all)

        self.btn_deactivate_all = QPushButton("Desactivar todo", self)
        self.btn_deactivate_all.clicked.connect(self.deactivate_all)
        button_layout.addWidget(self.btn_deactivate_all)

        layout.addLayout(button_layout)

        self.setLayout(layout)

        # Clear focus from buttons
        self.btn_load_data.setAutoDefault(False)
        self.btn_load_data.setDefault(False)
        self.btn_save_data.setAutoDefault(False)
        self.btn_save_data.setDefault(False)
        self.btn_add_data.setAutoDefault(False)
        self.btn_add_data.setDefault(False)
        self.btn_activate_all.setAutoDefault(False)
        self.btn_activate_all.setDefault(False)
        self.btn_deactivate_all.setAutoDefault(False)
        self.btn_deactivate_all.setDefault(False)

        self.load_data_and_show()

    # Methods for selecting and deselecting "Active" checkboxes
    def activate_all(self):
        """
        Activates all checkboxes in the specified column of the table widget.

        This method iterates through all rows of the table widget and sets the 
        checkbox in the 7th column (index 7) to a checked state.

        Note:
            - Assumes that the 7th column of the table widget contains checkboxes 
              as cell widgets.
            - The table widget must be properly initialized and populated before 
              calling this method.

        """
        for row in range(self.table_widget.rowCount()):
            checkbox_delegate = self.table_widget.cellWidget(row, 7)
            checkbox_delegate.setChecked(True)

    def deactivate_all(self):
        """
        Deactivates all checkboxes in the specified column of the table widget.

        This method iterates through all rows of the table widget and sets the 
        checkbox in the specified column (column index 7) to an unchecked state.

        Returns:
            None
        """
        for row in range(self.table_widget.rowCount()):
            checkbox_delegate = self.table_widget.cellWidget(row, 7)
            checkbox_delegate.setChecked(False)

    def add_device(self):
        """
        Adds a new device to the list of devices.

        This method calculates a new unique ID for the device, opens a dialog
        to collect the device's details, and appends the new device data to
        the internal data list if the dialog is accepted. It also updates the
        maximum ID and refreshes the data displayed in the table.

        Steps:

        1. Calculate a new unique ID for the device.
        2. Open the AddDevicesDialog to collect device details.
        3. If the dialog is accepted:
            - Retrieve the new device data from the dialog.
            - Append the new device data to the internal list.
            - Update the maximum ID.
            - Reload the data into the table.

        Note:
        - The dialog must return QDialog.Accepted for the new device data to be added.

        """
        new_id = self.max_id + 1  # Calculate new ID
        dialog = AddDevicesDialog(self, new_id)
        if dialog.exec() == QDialog.Accepted:
            new_device_data = dialog.get_data()
            #logging.debug(new_device_data)
            self.data.append(new_device_data)
            self.max_id = new_id  # Update max_id
            self.load_data_into_table()

    def load_data_and_show(self):
        """
        Loads data, processes it, and displays it in the UI.

        This method performs the following steps:
        1. Loads data by calling the `load_data` method.
        2. Populates the UI table with the loaded data by calling the `load_data_into_table` method.

        Ensure that the `load_data` and `load_data_into_table` methods are properly implemented
        for this method to function as expected.
        """
        self.data = self.load_data()
        self.load_data_into_table()

    def load_data(self):
        """
        Loads data from a file specified by `self.file_path`.

        The method reads each line of the file, splits it into parts, and processes
        the data if it matches the expected format. Each line is expected to have
        8 components separated by ' - '. The components are:
        district, model, point, ip, id, communication, battery, and active.

        The method updates `self.max_id` with the maximum value of the `id` field
        encountered in the file. The `battery` and `active` fields are evaluated
        as Python literals using `literal_eval`.

        Returns:
            (list): A list of tuples containing the processed data. Each tuple has
                  the following structure:
                  (district, model, point, ip, id, communication, battery, active)

        Raises:
            BaseErrorWithMessageBox: If an exception occurs during file reading or
                                     processing, it raises this custom error with
                                     an error code (3001) and the exception message.
        """
        data = []
        try:
            with open(self.file_path, 'r') as file:
                for line in file:
                    parts = line.strip().split(' - ')
                    if len(parts) == 8:
                        district, model, point, ip, id, communication, battery, active = parts
                        self.max_id = max(self.max_id, int(id))  # Update max_id
                        data.append((district, model, point, ip, id, communication, literal_eval(battery), literal_eval(active)))
        except Exception as e:
            raise BaseErrorWithMessageBox(3001, str(e), parent=self)
        return data

    def load_data_into_table(self):
        """
        Populates the table widget with data from the `self.data` attribute.
        This method iterates over the rows of data and populates each row in the table widget.

        It sets up the following columns:

        - Column 0: District (string)
        - Column 1: Model (string)
        - Column 2: Point (string)
        - Column 3: IP Address (string)
        - Column 4: ID (integer, converted to string)
        - Column 5: Communication (string, with a ComboBoxDelegate)
        - Column 6: Battery (boolean, with a CheckBoxDelegate)
        - Column 7: Active (boolean, with a CheckBoxDelegate)

        Delegates are used for specific columns:

        - A ComboBoxDelegate is set for column 5 to allow selection of communication options.
        - CheckBoxDelegates are used for columns 6 and 7 to represent boolean values.

        After populating the table, the method adjusts the table size to fit the content.

        Raises:
            BaseErrorWithMessageBox: If an exception occurs during the execution, it raises
            a custom error with a message box displaying the error details.
        """
        try:
            self.table_widget.setRowCount(0)
            for row, (district, model, point, ip, id, communication, battery, active) in enumerate(self.data):
                self.table_widget.insertRow(row)
                self.table_widget.setItem(row, 0, QTableWidgetItem(district))
                self.table_widget.setItem(row, 1, QTableWidgetItem(model))
                self.table_widget.setItem(row, 2, QTableWidgetItem(point))
                self.table_widget.setItem(row, 3, QTableWidgetItem(ip))
                self.table_widget.setItem(row, 4, QTableWidgetItem(str(id)))
                # Set ComboBoxDelegate for column 5
                combo_box_delegate = ComboBoxDelegate(self.table_widget)
                self.table_widget.setItemDelegateForColumn(5, combo_box_delegate)
                # Set the value in the model for column 5
                self.table_widget.setItem(row, 5, QTableWidgetItem(communication))
                # Set CheckBoxDelegate for column 6
                checkbox_battery = CheckBoxDelegate()
                checkbox_battery.setChecked(battery)
                self.table_widget.setCellWidget(row, 6, checkbox_battery)
                # Set CheckBoxDelegate for column 7
                checkbox_active = CheckBoxDelegate()
                checkbox_active.setChecked(active)
                self.table_widget.setCellWidget(row, 7, checkbox_active)

            self.adjust_size_to_table()
        except Exception as e:
            raise BaseErrorWithMessageBox(3500, str(e), parent=self)

    def save_data(self):
        """
        Saves the data from the table widget to a file.

        This method iterates through all rows of the table widget, retrieves the data
        from each cell, and writes it to a file specified by `self.file_path`. The data
        is saved in a formatted string with each row's values separated by " - ".
        Boolean values from checkboxes in the table are also included.

        Raises:
            BaseErrorWithMessageBox: If an exception occurs during the file writing process,
            it raises a custom error with a message box displaying the error details.

        Side Effects:
            - Writes data to the file specified by `self.file_path`.
            - Displays a success message box upon successful save.
            - Displays an error message box if an exception occurs.

        Notes:
            - The `district` and `point` values are converted to uppercase before saving.
            - The `battery` and `active` values are retrieved from checkbox widgets in the table.

        """
        try:
            with open(self.file_path, 'w') as file:
                for row in range(self.table_widget.rowCount()):
                    district = self.table_widget.item(row, 0).text().upper()
                    model = self.table_widget.item(row, 1).text()
                    point = self.table_widget.item(row, 2).text().upper()
                    ip = self.table_widget.item(row, 3).text()
                    id = self.table_widget.item(row, 4).text()
                    communication = self.table_widget.item(row, 5).text()
                    battery = self.table_widget.cellWidget(row, 6).isChecked()
                    active = self.table_widget.cellWidget(row, 7).isChecked()
                    #logging.debug(f"{district} - {model} - {point} - {ip} - {id} - {communication} - {battery} - {active}")
                    file.write(f"{district} - {model} - {point} - {ip} - {id} - {communication} - {battery} - {active}\n")
            QMessageBox.information(self, "Éxito", "Datos guardados correctamente.")
        except Exception as e:
            raise BaseErrorWithMessageBox(3001, str(e), parent=self)
__init__(parent=None)

Initializes the ModifyDeviceDialog class.

Parameters:

Name Type Description Default
parent QWidget

The parent widget for this dialog. Defaults to None.

None

Attributes:

Name Type Description
file_path str

The path to the "info_devices.txt" file, determined dynamically.

data list

A list to store device data.

max_id int

The maximum ID value among the devices.

Raises:

Type Description
BaseError

If an exception occurs during initialization, it raises a BaseError with code 3501.

Source code in src\ui\modify_device_dialog.py
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
def __init__(self, parent=None):
    """
    Initializes the ModifyDeviceDialog class.

    Args:
        parent (QWidget, optional): The parent widget for this dialog. Defaults to None.

    Attributes:
        file_path (str): The path to the "info_devices.txt" file, determined dynamically.
        data (list): A list to store device data.
        max_id (int): The maximum ID value among the devices.

    Raises:
        BaseError: If an exception occurs during initialization, it raises a BaseError with code 3501.
    """
    try:
        super().__init__(parent, window_title="MODIFICAR DISPOSITIVOS")

        self.file_path = os.path.join(find_root_directory(), "info_devices.txt")
        self.data = []
        self.max_id = 0
        self.init_ui()
        super().init_ui()
    except Exception as e:
        raise BaseError(3501, str(e))
activate_all()

Activates all checkboxes in the specified column of the table widget.

This method iterates through all rows of the table widget and sets the checkbox in the 7th column (index 7) to a checked state.

Note
  • Assumes that the 7th column of the table widget contains checkboxes as cell widgets.
  • The table widget must be properly initialized and populated before calling this method.
Source code in src\ui\modify_device_dialog.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def activate_all(self):
    """
    Activates all checkboxes in the specified column of the table widget.

    This method iterates through all rows of the table widget and sets the 
    checkbox in the 7th column (index 7) to a checked state.

    Note:
        - Assumes that the 7th column of the table widget contains checkboxes 
          as cell widgets.
        - The table widget must be properly initialized and populated before 
          calling this method.

    """
    for row in range(self.table_widget.rowCount()):
        checkbox_delegate = self.table_widget.cellWidget(row, 7)
        checkbox_delegate.setChecked(True)
add_device()

Adds a new device to the list of devices.

This method calculates a new unique ID for the device, opens a dialog to collect the device's details, and appends the new device data to the internal data list if the dialog is accepted. It also updates the maximum ID and refreshes the data displayed in the table.

Steps:

  1. Calculate a new unique ID for the device.
  2. Open the AddDevicesDialog to collect device details.
  3. If the dialog is accepted:
    • Retrieve the new device data from the dialog.
    • Append the new device data to the internal list.
    • Update the maximum ID.
    • Reload the data into the table.

Note: - The dialog must return QDialog.Accepted for the new device data to be added.

Source code in src\ui\modify_device_dialog.py
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
def add_device(self):
    """
    Adds a new device to the list of devices.

    This method calculates a new unique ID for the device, opens a dialog
    to collect the device's details, and appends the new device data to
    the internal data list if the dialog is accepted. It also updates the
    maximum ID and refreshes the data displayed in the table.

    Steps:

    1. Calculate a new unique ID for the device.
    2. Open the AddDevicesDialog to collect device details.
    3. If the dialog is accepted:
        - Retrieve the new device data from the dialog.
        - Append the new device data to the internal list.
        - Update the maximum ID.
        - Reload the data into the table.

    Note:
    - The dialog must return QDialog.Accepted for the new device data to be added.

    """
    new_id = self.max_id + 1  # Calculate new ID
    dialog = AddDevicesDialog(self, new_id)
    if dialog.exec() == QDialog.Accepted:
        new_device_data = dialog.get_data()
        #logging.debug(new_device_data)
        self.data.append(new_device_data)
        self.max_id = new_id  # Update max_id
        self.load_data_into_table()
deactivate_all()

Deactivates all checkboxes in the specified column of the table widget.

This method iterates through all rows of the table widget and sets the checkbox in the specified column (column index 7) to an unchecked state.

Returns:

Type Description

None

Source code in src\ui\modify_device_dialog.py
152
153
154
155
156
157
158
159
160
161
162
163
164
def deactivate_all(self):
    """
    Deactivates all checkboxes in the specified column of the table widget.

    This method iterates through all rows of the table widget and sets the 
    checkbox in the specified column (column index 7) to an unchecked state.

    Returns:
        None
    """
    for row in range(self.table_widget.rowCount()):
        checkbox_delegate = self.table_widget.cellWidget(row, 7)
        checkbox_delegate.setChecked(False)
init_ui()

Initializes the user interface for the Modify Device Dialog. This method sets up the layout and widgets for the dialog, including a table for displaying device information and buttons for performing various actions such as loading, modifying, adding, activating, and deactivating devices.

Widgets
  • QTableWidget: Displays device information with 8 columns: ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación", "Pila funcionando", "Activado"]. Columns are resizable, sortable, and editable on double-click.
  • QPushButton: Buttons for the following actions:
    • "Cargar": Loads and displays data in the table.
    • "Modificar": Saves modifications made to the data.
    • "Agregar": Adds a new device entry.
    • "Activar todo": Activates all devices.
    • "Desactivar todo": Deactivates all devices.
Layout
  • QVBoxLayout: Main layout containing the table widget and button layout.
  • QHBoxLayout: Layout for arranging the action buttons horizontally.
Notes
  • Buttons are configured to not retain focus after being clicked.
  • The load_data_and_show method is called at the end to populate the table with initial data.
Source code in src\ui\modify_device_dialog.py
 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
def init_ui(self):
    """
    Initializes the user interface for the Modify Device Dialog.
    This method sets up the layout and widgets for the dialog, including a table
    for displaying device information and buttons for performing various actions
    such as loading, modifying, adding, activating, and deactivating devices.

    Widgets:
        - QTableWidget: Displays device information with 8 columns:
            ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación", 
            "Pila funcionando", "Activado"]. Columns are resizable, sortable, and editable on double-click.
        - QPushButton: Buttons for the following actions:
            - "Cargar": Loads and displays data in the table.
            - "Modificar": Saves modifications made to the data.
            - "Agregar": Adds a new device entry.
            - "Activar todo": Activates all devices.
            - "Desactivar todo": Deactivates all devices.

    Layout:
        - QVBoxLayout: Main layout containing the table widget and button layout.
        - QHBoxLayout: Layout for arranging the action buttons horizontally.

    Notes:
        - Buttons are configured to not retain focus after being clicked.
        - The `load_data_and_show` method is called at the end to populate the table with initial data.
    """
    layout = QVBoxLayout(self)

    self.table_widget = QTableWidget()
    self.table_widget.setColumnCount(8)
    self.table_widget.setHorizontalHeaderLabels(["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación", "Pila funcionando", "Activado"])
    self.table_widget.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
    self.table_widget.horizontalHeader().setStretchLastSection(True)
    self.table_widget.setEditTriggers(QTableWidget.DoubleClicked)
    self.table_widget.setSortingEnabled(True)

    layout.addWidget(self.table_widget)

    button_layout = QHBoxLayout()

    self.btn_load_data = QPushButton("Cargar", self)
    self.btn_load_data.clicked.connect(self.load_data_and_show)
    button_layout.addWidget(self.btn_load_data)

    self.btn_save_data = QPushButton("Modificar", self)
    self.btn_save_data.clicked.connect(self.save_data)
    button_layout.addWidget(self.btn_save_data)

    self.btn_add_data = QPushButton("Agregar", self)
    self.btn_add_data.clicked.connect(self.add_device)
    button_layout.addWidget(self.btn_add_data)

    self.btn_activate_all = QPushButton("Activar todo", self)
    self.btn_activate_all.clicked.connect(self.activate_all)
    button_layout.addWidget(self.btn_activate_all)

    self.btn_deactivate_all = QPushButton("Desactivar todo", self)
    self.btn_deactivate_all.clicked.connect(self.deactivate_all)
    button_layout.addWidget(self.btn_deactivate_all)

    layout.addLayout(button_layout)

    self.setLayout(layout)

    # Clear focus from buttons
    self.btn_load_data.setAutoDefault(False)
    self.btn_load_data.setDefault(False)
    self.btn_save_data.setAutoDefault(False)
    self.btn_save_data.setDefault(False)
    self.btn_add_data.setAutoDefault(False)
    self.btn_add_data.setDefault(False)
    self.btn_activate_all.setAutoDefault(False)
    self.btn_activate_all.setDefault(False)
    self.btn_deactivate_all.setAutoDefault(False)
    self.btn_deactivate_all.setDefault(False)

    self.load_data_and_show()
load_data()

Loads data from a file specified by self.file_path.

The method reads each line of the file, splits it into parts, and processes the data if it matches the expected format. Each line is expected to have 8 components separated by ' - '. The components are: district, model, point, ip, id, communication, battery, and active.

The method updates self.max_id with the maximum value of the id field encountered in the file. The battery and active fields are evaluated as Python literals using literal_eval.

Returns:

Type Description
list

A list of tuples containing the processed data. Each tuple has the following structure: (district, model, point, ip, id, communication, battery, active)

Raises:

Type Description
BaseErrorWithMessageBox

If an exception occurs during file reading or processing, it raises this custom error with an error code (3001) and the exception message.

Source code in src\ui\modify_device_dialog.py
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
def load_data(self):
    """
    Loads data from a file specified by `self.file_path`.

    The method reads each line of the file, splits it into parts, and processes
    the data if it matches the expected format. Each line is expected to have
    8 components separated by ' - '. The components are:
    district, model, point, ip, id, communication, battery, and active.

    The method updates `self.max_id` with the maximum value of the `id` field
    encountered in the file. The `battery` and `active` fields are evaluated
    as Python literals using `literal_eval`.

    Returns:
        (list): A list of tuples containing the processed data. Each tuple has
              the following structure:
              (district, model, point, ip, id, communication, battery, active)

    Raises:
        BaseErrorWithMessageBox: If an exception occurs during file reading or
                                 processing, it raises this custom error with
                                 an error code (3001) and the exception message.
    """
    data = []
    try:
        with open(self.file_path, 'r') as file:
            for line in file:
                parts = line.strip().split(' - ')
                if len(parts) == 8:
                    district, model, point, ip, id, communication, battery, active = parts
                    self.max_id = max(self.max_id, int(id))  # Update max_id
                    data.append((district, model, point, ip, id, communication, literal_eval(battery), literal_eval(active)))
    except Exception as e:
        raise BaseErrorWithMessageBox(3001, str(e), parent=self)
    return data
load_data_and_show()

Loads data, processes it, and displays it in the UI.

This method performs the following steps: 1. Loads data by calling the load_data method. 2. Populates the UI table with the loaded data by calling the load_data_into_table method.

Ensure that the load_data and load_data_into_table methods are properly implemented for this method to function as expected.

Source code in src\ui\modify_device_dialog.py
198
199
200
201
202
203
204
205
206
207
208
209
210
def load_data_and_show(self):
    """
    Loads data, processes it, and displays it in the UI.

    This method performs the following steps:
    1. Loads data by calling the `load_data` method.
    2. Populates the UI table with the loaded data by calling the `load_data_into_table` method.

    Ensure that the `load_data` and `load_data_into_table` methods are properly implemented
    for this method to function as expected.
    """
    self.data = self.load_data()
    self.load_data_into_table()
load_data_into_table()

Populates the table widget with data from the self.data attribute. This method iterates over the rows of data and populates each row in the table widget.

It sets up the following columns:

  • Column 0: District (string)
  • Column 1: Model (string)
  • Column 2: Point (string)
  • Column 3: IP Address (string)
  • Column 4: ID (integer, converted to string)
  • Column 5: Communication (string, with a ComboBoxDelegate)
  • Column 6: Battery (boolean, with a CheckBoxDelegate)
  • Column 7: Active (boolean, with a CheckBoxDelegate)

Delegates are used for specific columns:

  • A ComboBoxDelegate is set for column 5 to allow selection of communication options.
  • CheckBoxDelegates are used for columns 6 and 7 to represent boolean values.

After populating the table, the method adjusts the table size to fit the content.

Raises:

Type Description
BaseErrorWithMessageBox

If an exception occurs during the execution, it raises

Source code in src\ui\modify_device_dialog.py
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
def load_data_into_table(self):
    """
    Populates the table widget with data from the `self.data` attribute.
    This method iterates over the rows of data and populates each row in the table widget.

    It sets up the following columns:

    - Column 0: District (string)
    - Column 1: Model (string)
    - Column 2: Point (string)
    - Column 3: IP Address (string)
    - Column 4: ID (integer, converted to string)
    - Column 5: Communication (string, with a ComboBoxDelegate)
    - Column 6: Battery (boolean, with a CheckBoxDelegate)
    - Column 7: Active (boolean, with a CheckBoxDelegate)

    Delegates are used for specific columns:

    - A ComboBoxDelegate is set for column 5 to allow selection of communication options.
    - CheckBoxDelegates are used for columns 6 and 7 to represent boolean values.

    After populating the table, the method adjusts the table size to fit the content.

    Raises:
        BaseErrorWithMessageBox: If an exception occurs during the execution, it raises
        a custom error with a message box displaying the error details.
    """
    try:
        self.table_widget.setRowCount(0)
        for row, (district, model, point, ip, id, communication, battery, active) in enumerate(self.data):
            self.table_widget.insertRow(row)
            self.table_widget.setItem(row, 0, QTableWidgetItem(district))
            self.table_widget.setItem(row, 1, QTableWidgetItem(model))
            self.table_widget.setItem(row, 2, QTableWidgetItem(point))
            self.table_widget.setItem(row, 3, QTableWidgetItem(ip))
            self.table_widget.setItem(row, 4, QTableWidgetItem(str(id)))
            # Set ComboBoxDelegate for column 5
            combo_box_delegate = ComboBoxDelegate(self.table_widget)
            self.table_widget.setItemDelegateForColumn(5, combo_box_delegate)
            # Set the value in the model for column 5
            self.table_widget.setItem(row, 5, QTableWidgetItem(communication))
            # Set CheckBoxDelegate for column 6
            checkbox_battery = CheckBoxDelegate()
            checkbox_battery.setChecked(battery)
            self.table_widget.setCellWidget(row, 6, checkbox_battery)
            # Set CheckBoxDelegate for column 7
            checkbox_active = CheckBoxDelegate()
            checkbox_active.setChecked(active)
            self.table_widget.setCellWidget(row, 7, checkbox_active)

        self.adjust_size_to_table()
    except Exception as e:
        raise BaseErrorWithMessageBox(3500, str(e), parent=self)
save_data()

Saves the data from the table widget to a file.

This method iterates through all rows of the table widget, retrieves the data from each cell, and writes it to a file specified by self.file_path. The data is saved in a formatted string with each row's values separated by " - ". Boolean values from checkboxes in the table are also included.

Raises:

Type Description
BaseErrorWithMessageBox

If an exception occurs during the file writing process,

Side Effects
  • Writes data to the file specified by self.file_path.
  • Displays a success message box upon successful save.
  • Displays an error message box if an exception occurs.
Notes
  • The district and point values are converted to uppercase before saving.
  • The battery and active values are retrieved from checkbox widgets in the table.
Source code in src\ui\modify_device_dialog.py
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
def save_data(self):
    """
    Saves the data from the table widget to a file.

    This method iterates through all rows of the table widget, retrieves the data
    from each cell, and writes it to a file specified by `self.file_path`. The data
    is saved in a formatted string with each row's values separated by " - ".
    Boolean values from checkboxes in the table are also included.

    Raises:
        BaseErrorWithMessageBox: If an exception occurs during the file writing process,
        it raises a custom error with a message box displaying the error details.

    Side Effects:
        - Writes data to the file specified by `self.file_path`.
        - Displays a success message box upon successful save.
        - Displays an error message box if an exception occurs.

    Notes:
        - The `district` and `point` values are converted to uppercase before saving.
        - The `battery` and `active` values are retrieved from checkbox widgets in the table.

    """
    try:
        with open(self.file_path, 'w') as file:
            for row in range(self.table_widget.rowCount()):
                district = self.table_widget.item(row, 0).text().upper()
                model = self.table_widget.item(row, 1).text()
                point = self.table_widget.item(row, 2).text().upper()
                ip = self.table_widget.item(row, 3).text()
                id = self.table_widget.item(row, 4).text()
                communication = self.table_widget.item(row, 5).text()
                battery = self.table_widget.cellWidget(row, 6).isChecked()
                active = self.table_widget.cellWidget(row, 7).isChecked()
                #logging.debug(f"{district} - {model} - {point} - {ip} - {id} - {communication} - {battery} - {active}")
                file.write(f"{district} - {model} - {point} - {ip} - {id} - {communication} - {battery} - {active}\n")
        QMessageBox.information(self, "Éxito", "Datos guardados correctamente.")
    except Exception as e:
        raise BaseErrorWithMessageBox(3001, str(e), parent=self)

obtain_attendances_devices_dialog

ObtainAttendancesDevicesDialog

Bases: SelectDevicesDialog

Source code in src\ui\obtain_attendances_devices_dialog.py
 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
class ObtainAttendancesDevicesDialog(SelectDevicesDialog):
    def __init__(self, parent=None):
        """
        Initializes the ObtainAttendancesDevicesDialog class.

        Args:
            parent (Optional[QWidget]): The parent widget for this dialog. Defaults to None.

        Attributes:
            failed_devices (list[str]): A list to store devices that failed during the operation.
            attendances_manager (AttendancesManager): An instance of AttendancesManager to handle attendance management.

        Raises:
            BaseError: If an exception occurs during initialization, it raises a BaseError with code 3501 and the exception message.
        """
        try:
            self.failed_devices: list[str] = []
            self.attendances_manager = AttendancesManager()
            super().__init__(parent, op_function=self.attendances_manager.manage_devices_attendances, window_title="OBTENER MARCACIONES")
            self.init_ui()
        except Exception as e:
            raise BaseError(3501, str(e))

    def init_ui(self):
        """
        Initializes the user interface for the attendance devices dialog.

        This method sets up the UI components, including labels, buttons, and layouts,
        to display and manage attendance device information. It also configures event
        handlers for user interactions.

        Raises:
            BaseError: If an exception occurs during the initialization process, it
                       raises a BaseError with code 3501 and the exception message.

        UI Components:
            - Header Labels: ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación"]
            - QPushButton: A button to retry failed connections, initially hidden.
            - QPushButton: A button to update and obtain attendance records, with updated text.
            - QLabel: A label to display the total number of attendances, initially hidden.
        """
        try:
            header_labels = ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación"]
            super().init_ui(header_labels=header_labels)
            self.btn_retry_failed_connection = QPushButton("Reintentar fallidos", self)
            self.btn_retry_failed_connection.clicked.connect(self.on_retry_failed_connection_clicked)
            self.button_layout.addWidget(self.btn_retry_failed_connection)
            self.btn_retry_failed_connection.setVisible(False)
            self.btn_update.setText("Obtener marcaciones")
            self.label_total_attendances = QLabel("Total de Marcaciones: 0", self)
            self.label_total_attendances.setAlignment(Qt.AlignCenter)
            self.layout().addWidget(self.label_total_attendances)
            self.label_total_attendances.setVisible(False)
        except Exception as e:
            raise BaseError(3501, str(e))

    def operation_with_selected_ips(self):
        """
        Executes an operation with the selected IP addresses.

        This method hides the retry button and the total attendances label before
        attempting to perform the operation. It calls the parent class's 
        `operation_with_selected_ips` method to execute the operation. If an 
        exception occurs during the execution, it displays the retry button 
        for failed connections.

        Exceptions:
            Exception: Catches any exception that occurs during the operation 
            and triggers the display of the retry button.
        """
        self.btn_retry_failed_connection.setVisible(False)
        self.label_total_attendances.setVisible(False)
        try:
            super().operation_with_selected_ips()
        except Exception as e:
            self.show_btn_retry_failed_connection()
        return

    def op_terminate(self, devices: dict[str, dict[str, str]] = None):
        """
        Finalizes the operation of obtaining attendance data from devices and updates the UI accordingly.

        Args:
            devices (dict[str, dict[str, str]]): A dictionary where the keys are device IPs and the values are dictionaries 
                containing device-specific information, such as "connection failed" status and "attendance count".

        Functionality:
            - Iterates through the rows of the table widget to update attendance data for each device.
            - Checks if the device IP is in the list of selected IPs and updates the corresponding table cell:
                - Marks devices with failed connections in red and adds them to the failed devices list.
                - Updates the attendance count for devices with successful connections and marks them in green.
            - Ensures the "Cant. de Marcaciones" column exists in the table and updates it with attendance data.
            - Calculates the total number of attendances across all devices.
            - Adjusts the table size, enables sorting, and sorts the table by a specific column in descending order.
            - Deselects all rows in the table and centers the window.
            - Attempts to check attendance files and handles any exceptions with a warning-level error.
            - Displays a retry button for failed connections and updates the total attendance label.

        Raises:
            BaseErrorWithMessageBox: If an unexpected exception occurs during the operation, it is raised with an error message box.
        """
        try:
            self.failed_devices = []
            #logging.debug(f'selected devices: {self.selected_ips} - devices from operation: {devices} - failed devices: {self.failed_devices}')

            actual_column = self.ensure_column_exists("Cant. de Marcaciones")

            total_marcaciones = 0
            for row in range (self.table_widget.rowCount()):
                ip_selected = self.table_widget.item(row, 3).text()  # Column 3 holds the IP
                attendances_count_item = QTableWidgetItem("")
                if ip_selected not in self.selected_ips:
                    attendances_count_item.setBackground(QColor(Qt.white))
                else:
                    device = devices.get(ip_selected)
                    if device:
                        if device.get("connection failed", False):
                            attendances_count_item.setText("Conexión fallida")
                            attendances_count_item.setBackground(QColor(Qt.red))
                            self.failed_devices.append(ip_selected)
                        else:
                            attendance_count = device.get("attendance count")
                            try:
                                total_marcaciones += int(attendance_count)
                            except ValueError:
                                attendance_count = 0
                                BaseError(3500, f"Error al obtener la cantidad de marcaciones del dispositivo {ip_selected}")
                            attendances_count_item.setText(str(attendance_count))
                            attendances_count_item.setBackground(QColor(Qt.green))
                attendances_count_item.setFlags(attendances_count_item.flags() & ~Qt.ItemIsEditable)
                self.table_widget.setItem(row, actual_column, attendances_count_item)
            self.adjust_size_to_table()
            self.table_widget.setSortingEnabled(True)
            self.table_widget.sortByColumn(6, Qt.DescendingOrder)  
            self.deselect_all_rows()

            self.center_window()
            try:
                self.check_attendance_files()
            except Exception as e:
                BaseError(3000, str(e), level="warning")
            self.show_btn_retry_failed_connection()
            self.label_total_attendances.setText(f"Total de Marcaciones: {total_marcaciones}")
            self.label_total_attendances.setVisible(True)
            super().op_terminate()
        except Exception as e:
            raise BaseErrorWithMessageBox(3500, str(e), parent=self)

    def parse_attendance(self, line: str):
        """
        Parses a line of attendance data and extracts the timestamp.

        Args:
            line (str): A string containing attendance data, expected to have at least
                        three parts separated by spaces, where the second and third parts
                        represent the date and time respectively.

        Returns:
            (datetime or None): A datetime object representing the parsed timestamp if the
                              line is well-formed and the date and time are valid.
                              Returns None if the line is malformed or the date/time
                              cannot be parsed.
        """
        parts = line.split()
        if len(parts) < 3:
            return None
        try:
            date_str, time_str = parts[1], parts[2]
            timestamp: datetime = datetime.strptime(f"{date_str} {time_str}", "%d/%m/%Y %H:%M")
            return timestamp
        except ValueError:
            return None

    def check_attendance_files(self):
        """
        Checks attendance files in the "devices" directory for errors.
        A daily record (in a temporary file) is kept of each already reported incorrect check‑in,
        so that new ones are only reported once. For reporting, information is grouped by file,
        displaying each error only once (by file_path) as in the original version.

        Raises:
            BaseError: if the 'devices' folder is not found or if attendance errors are detected.
        """
        import tempfile
        from datetime import timedelta

        # Get today's and yesterday's dates
        today_str = datetime.now().strftime("%Y-%m-%d")
        yesterday_str = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")

        devices_path = os.path.join(find_root_directory(), "devices")
        temp_dir = tempfile.gettempdir()
        # logging.debug(f"Directorio temporal: {temp_dir}")

        # Temporary file for today and yesterday
        temp_file_path = os.path.join(
            temp_dir,
            f"reported_incorrect_attendances_{today_str}.tmp"
        )
        temp_file_yesterday = os.path.join(
            temp_dir,
            f"reported_incorrect_attendances_{yesterday_str}.tmp"
        )

        # Remove yesterday's file if it exists
        if os.path.exists(temp_file_yesterday):
            try:
                os.remove(temp_file_yesterday)
            except Exception as e:
                logging.warning(f"No se pudo eliminar el archivo de ayer: {e}")

        # Load errors already reported today: each line is a unique error identifier (file_path + line)
        reported_errors = set()
        if os.path.exists(temp_file_path):
            with open(temp_file_path, "r", encoding="utf-8") as tf:
                for line in tf:
                    reported_errors.add(line.strip())

        new_reported = set()            # Newly found errors (by line)
        files_with_new_errors = {}      # Grouped by file: key = file.path, value = info for report

        if not os.path.isdir(devices_path):
            raise BaseError(3000, "No se encontró la carpeta 'devices'", level="warning")

        # Traverse the devices directory
        with os.scandir(devices_path) as entries:
            for subfolder in entries:
                if not subfolder.is_dir():
                    continue

                subfolder_path = os.path.join(devices_path, subfolder.name)
                with os.scandir(subfolder_path) as sub_entries:
                    for sub_entry in sub_entries:
                        if not sub_entry.is_dir():
                            continue

                        with os.scandir(sub_entry.path) as files:
                            for file in files:
                                if today_str in file.name and file.name.endswith(".cro"):
                                    file_has_new_error = False  # Flag for new errors in this file

                                    with open(file.path, "r", encoding="utf-8") as f:
                                        # Check each line for attendance errors
                                        for line_number, line in enumerate(f, start=1):
                                            attendance = Attendance(timestamp=self.parse_attendance(line))
                                            if attendance is not None and (
                                                attendance.is_three_months_old() or attendance.is_in_the_future()
                                            ):
                                                # Create a unique identifier for this error line
                                                error_id = str(line)
                                                if error_id in reported_errors:
                                                    continue  # This error was already reported

                                                # New error found
                                                new_reported.add(error_id)
                                                file_has_new_error = True
                                                # To continue scanning the file for all new errors (even though only one link per file is shown),
                                                # do not break here. Uncomment the next line to only report the first occurrence per file.
                                                # break

                                    if file_has_new_error:
                                        # Add one entry per file, as in the original version
                                        if file.path not in files_with_new_errors:
                                            files_with_new_errors[file.path] = {
                                                "ip": self.extract_ip(file.name),
                                                "date": self.extract_date(file.name),
                                                "file_path": self.format_file_uri(file.path)
                                            }

        # Append newly found errors to the temporary file
        if new_reported:
            with open(temp_file_path, "a", encoding="utf-8") as tf:
                for err in new_reported:
                    tf.write(err + "\n")

        # Build report from files that have new errors
        devices_with_error = list(files_with_new_errors.values())
        if devices_with_error:
            error_info = (
                "<html><br>" +
                "<br>".join(
                    [f"- <a href='{device['file_path']}'>{device['date']}: {device['ip']}</a>"
                    for device in devices_with_error]
                ) +
                "</html>"
            )
            error_code = 2003
            error = BaseError(error_code, error_info)

            config.read(os.path.join(find_root_directory(), 'config.ini'))
            clear_attendance: bool = config.getboolean('Device_config', 'clear_attendance')
            if clear_attendance:
                self.ask_force_clear_attendances(error_code, error_info, parent=self)
            else:
                error.show_message_box_html(parent=self)

    def ask_force_clear_attendances(self, error_code, error_info, parent):
        msg_box = QMessageBox(parent)
        msg_box.setIcon(QMessageBox.Warning)
        msg_box.setWindowTitle(f"Error {error_code}")
        msg_box.setTextFormat(Qt.RichText)
        msg_box.setText(error_info)
        msg_box.setInformativeText("¿Desea habilitar el forzado de eliminado de marcaciones en su proxima obtencion de marcaciones?")
        msg_box.setStandardButtons(QMessageBox.Ok | QMessageBox.No)
        msg_box.setDefaultButton(QMessageBox.No)
        result = msg_box.exec_()
        if result == QMessageBox.Ok:
            config['Device_config']['force_clear_attendance'] = 'True'

            # Write the changes back to the configuration file
            try:
                with open('config.ini', 'w') as config_file:
                    config.write(config_file)
            except Exception as e:
                BaseError(3001, str(e))

            logging.info("Se ha habilitado el forzado de eliminacion de marcaciones")

    def format_file_uri(self, file_path: str):
        """
        Converts a local file path to a file URI.

        This method takes a file path as input, replaces backslashes with forward slashes
        (to ensure compatibility across different operating systems), and encodes special
        characters to make the path safe for use in a URI. It then constructs a file URI
        by prefixing the encoded path with 'file:'.

        Args:
            file_path (str): The local file path to be converted into a file URI.

        Returns:
            (str): The formatted file URI.
        """
        file_url = urllib.parse.urljoin('file:', urllib.parse.quote(file_path.replace("\\", "/")))
        # logging.debug(file_url)
        return file_url

    def extract_ip(self, filename: str):
        """
        Extracts an IP address from a given filename.

        The method uses a regular expression to match filenames that follow the
        pattern: `<IP_ADDRESS>_<DATE>_file.cro`, where:

        - `<IP_ADDRESS>` is a valid IPv4 address (e.g., 192.168.1.1).
        - `<DATE>` is in the format YYYY-MM-DD.

        Args:
            filename (str): The filename to extract the IP address from.

        Returns:
            (str): The extracted IP address if the filename matches the pattern, 
                 otherwise None.
        """
        match = re.match(r"^(\d+\.\d+\.\d+\.\d+)_\d{4}-\d{2}-\d{2}_file\.cro$", filename)
        return match.group(1) if match else None

    def extract_date(self, filename: str):
        """
        Extracts a date string from a given filename if it matches a specific pattern.

        The method uses a regular expression to identify filenames that follow the
        pattern: `<IP_ADDRESS>_<YYYY-MM-DD>_file.cro`, where:

        - `<IP_ADDRESS>` is a sequence of four groups of digits separated by dots.
        - `<YYYY-MM-DD>` is a date in the format year-month-day.

        Args:
            filename (str): The filename to extract the date from.

        Returns:
            (str or None): The extracted date string in the format 'YYYY-MM-DD' if the
                        filename matches the pattern; otherwise, None.
        """
        match = re.match(r"^\d+\.\d+\.\d+\.\d+_(\d{4}-\d{2}-\d{2})_file\.cro$", filename)
        return match.group(1) if match else None

    def show_btn_retry_failed_connection(self):
        """
        Displays the "Retry Failed Connection" button if there are failed devices.

        This method checks if the `failed_devices` attribute exists and contains 
        one or more entries. If so, it makes the `btn_retry_failed_connection` 
        button visible. If an exception occurs during execution, it raises a 
        `BaseError` with an appropriate error code and message.

        Raises:
            BaseError: If an exception occurs, it is wrapped in a `BaseError` 
                       with code 3500 and the exception message.
        """
        try:            
            if self.failed_devices and len(self.failed_devices) > 0:
                self.btn_retry_failed_connection.setVisible(True)
        except Exception as e:
            raise BaseError(3500, str(e))

    def on_retry_failed_connection_clicked(self):
        """
        Handles the retry action for failed device connections.

        This method is triggered when the "Retry Failed Connection" button is clicked.
        It attempts to reconnect to the devices that previously failed to connect and
        updates the UI to reflect the retry process. A separate thread is used to manage
        the reconnection attempts and progress updates.

        Steps performed:

        - Hides certain UI elements (e.g., table widget, buttons) and updates labels.
        - Initializes and starts an `OperationThread` to handle the reconnection logic.
        - Connects thread signals to appropriate methods for progress updates and cleanup.
        - Hides the retry button after it is clicked.

        Raises:
            BaseError: If an exception occurs during the execution of the method.

        Attributes:
            self.failed_devices (list): List of devices that failed to connect.
            self.attendances_manager (object): Manager responsible for handling device attendances.
        """
        try:
            self.table_widget.setVisible(False)
            self.table_widget.sortByColumn(3, Qt.DescendingOrder)
            self.label_total_attendances.setVisible(False)
            self.label_updating.setText("Reintentando conexiones...")
            self.btn_update.setVisible(False)
            self.btn_activate_all.setVisible(False)
            self.btn_deactivate_all.setVisible(False)
            self.label_updating.setVisible(True)
            self.progress_bar.setVisible(True)
            self.progress_bar.setValue(0)
            # logging.debug(f"Dispositivos seleccionados: {self.failed_devices}")
            self.op_thread = OperationThread(self.attendances_manager.manage_devices_attendances, self.failed_devices)
            self.op_thread.progress_updated.connect(self.update_progress)
            self.op_thread.op_terminate.connect(self.op_terminate)
            self.op_thread.finished.connect(self.cleanup_thread)
            self.op_thread.start()
            self.btn_retry_failed_connection.setVisible(False)  # Hide the button after clicking
        except Exception as e:
            raise BaseError(3500, str(e))
__init__(parent=None)

Initializes the ObtainAttendancesDevicesDialog class.

Parameters:

Name Type Description Default
parent Optional[QWidget]

The parent widget for this dialog. Defaults to None.

None

Attributes:

Name Type Description
failed_devices list[str]

A list to store devices that failed during the operation.

attendances_manager AttendancesManager

An instance of AttendancesManager to handle attendance management.

Raises:

Type Description
BaseError

If an exception occurs during initialization, it raises a BaseError with code 3501 and the exception message.

Source code in src\ui\obtain_attendances_devices_dialog.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def __init__(self, parent=None):
    """
    Initializes the ObtainAttendancesDevicesDialog class.

    Args:
        parent (Optional[QWidget]): The parent widget for this dialog. Defaults to None.

    Attributes:
        failed_devices (list[str]): A list to store devices that failed during the operation.
        attendances_manager (AttendancesManager): An instance of AttendancesManager to handle attendance management.

    Raises:
        BaseError: If an exception occurs during initialization, it raises a BaseError with code 3501 and the exception message.
    """
    try:
        self.failed_devices: list[str] = []
        self.attendances_manager = AttendancesManager()
        super().__init__(parent, op_function=self.attendances_manager.manage_devices_attendances, window_title="OBTENER MARCACIONES")
        self.init_ui()
    except Exception as e:
        raise BaseError(3501, str(e))
check_attendance_files()

Checks attendance files in the "devices" directory for errors. A daily record (in a temporary file) is kept of each already reported incorrect check‑in, so that new ones are only reported once. For reporting, information is grouped by file, displaying each error only once (by file_path) as in the original version.

Raises:

Type Description
BaseError

if the 'devices' folder is not found or if attendance errors are detected.

Source code in src\ui\obtain_attendances_devices_dialog.py
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
def check_attendance_files(self):
    """
    Checks attendance files in the "devices" directory for errors.
    A daily record (in a temporary file) is kept of each already reported incorrect check‑in,
    so that new ones are only reported once. For reporting, information is grouped by file,
    displaying each error only once (by file_path) as in the original version.

    Raises:
        BaseError: if the 'devices' folder is not found or if attendance errors are detected.
    """
    import tempfile
    from datetime import timedelta

    # Get today's and yesterday's dates
    today_str = datetime.now().strftime("%Y-%m-%d")
    yesterday_str = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")

    devices_path = os.path.join(find_root_directory(), "devices")
    temp_dir = tempfile.gettempdir()
    # logging.debug(f"Directorio temporal: {temp_dir}")

    # Temporary file for today and yesterday
    temp_file_path = os.path.join(
        temp_dir,
        f"reported_incorrect_attendances_{today_str}.tmp"
    )
    temp_file_yesterday = os.path.join(
        temp_dir,
        f"reported_incorrect_attendances_{yesterday_str}.tmp"
    )

    # Remove yesterday's file if it exists
    if os.path.exists(temp_file_yesterday):
        try:
            os.remove(temp_file_yesterday)
        except Exception as e:
            logging.warning(f"No se pudo eliminar el archivo de ayer: {e}")

    # Load errors already reported today: each line is a unique error identifier (file_path + line)
    reported_errors = set()
    if os.path.exists(temp_file_path):
        with open(temp_file_path, "r", encoding="utf-8") as tf:
            for line in tf:
                reported_errors.add(line.strip())

    new_reported = set()            # Newly found errors (by line)
    files_with_new_errors = {}      # Grouped by file: key = file.path, value = info for report

    if not os.path.isdir(devices_path):
        raise BaseError(3000, "No se encontró la carpeta 'devices'", level="warning")

    # Traverse the devices directory
    with os.scandir(devices_path) as entries:
        for subfolder in entries:
            if not subfolder.is_dir():
                continue

            subfolder_path = os.path.join(devices_path, subfolder.name)
            with os.scandir(subfolder_path) as sub_entries:
                for sub_entry in sub_entries:
                    if not sub_entry.is_dir():
                        continue

                    with os.scandir(sub_entry.path) as files:
                        for file in files:
                            if today_str in file.name and file.name.endswith(".cro"):
                                file_has_new_error = False  # Flag for new errors in this file

                                with open(file.path, "r", encoding="utf-8") as f:
                                    # Check each line for attendance errors
                                    for line_number, line in enumerate(f, start=1):
                                        attendance = Attendance(timestamp=self.parse_attendance(line))
                                        if attendance is not None and (
                                            attendance.is_three_months_old() or attendance.is_in_the_future()
                                        ):
                                            # Create a unique identifier for this error line
                                            error_id = str(line)
                                            if error_id in reported_errors:
                                                continue  # This error was already reported

                                            # New error found
                                            new_reported.add(error_id)
                                            file_has_new_error = True
                                            # To continue scanning the file for all new errors (even though only one link per file is shown),
                                            # do not break here. Uncomment the next line to only report the first occurrence per file.
                                            # break

                                if file_has_new_error:
                                    # Add one entry per file, as in the original version
                                    if file.path not in files_with_new_errors:
                                        files_with_new_errors[file.path] = {
                                            "ip": self.extract_ip(file.name),
                                            "date": self.extract_date(file.name),
                                            "file_path": self.format_file_uri(file.path)
                                        }

    # Append newly found errors to the temporary file
    if new_reported:
        with open(temp_file_path, "a", encoding="utf-8") as tf:
            for err in new_reported:
                tf.write(err + "\n")

    # Build report from files that have new errors
    devices_with_error = list(files_with_new_errors.values())
    if devices_with_error:
        error_info = (
            "<html><br>" +
            "<br>".join(
                [f"- <a href='{device['file_path']}'>{device['date']}: {device['ip']}</a>"
                for device in devices_with_error]
            ) +
            "</html>"
        )
        error_code = 2003
        error = BaseError(error_code, error_info)

        config.read(os.path.join(find_root_directory(), 'config.ini'))
        clear_attendance: bool = config.getboolean('Device_config', 'clear_attendance')
        if clear_attendance:
            self.ask_force_clear_attendances(error_code, error_info, parent=self)
        else:
            error.show_message_box_html(parent=self)
extract_date(filename)

Extracts a date string from a given filename if it matches a specific pattern.

The method uses a regular expression to identify filenames that follow the pattern: <IP_ADDRESS>_<YYYY-MM-DD>_file.cro, where:

  • <IP_ADDRESS> is a sequence of four groups of digits separated by dots.
  • <YYYY-MM-DD> is a date in the format year-month-day.

Parameters:

Name Type Description Default
filename str

The filename to extract the date from.

required

Returns:

Type Description
str or None

The extracted date string in the format 'YYYY-MM-DD' if the filename matches the pattern; otherwise, None.

Source code in src\ui\obtain_attendances_devices_dialog.py
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
def extract_date(self, filename: str):
    """
    Extracts a date string from a given filename if it matches a specific pattern.

    The method uses a regular expression to identify filenames that follow the
    pattern: `<IP_ADDRESS>_<YYYY-MM-DD>_file.cro`, where:

    - `<IP_ADDRESS>` is a sequence of four groups of digits separated by dots.
    - `<YYYY-MM-DD>` is a date in the format year-month-day.

    Args:
        filename (str): The filename to extract the date from.

    Returns:
        (str or None): The extracted date string in the format 'YYYY-MM-DD' if the
                    filename matches the pattern; otherwise, None.
    """
    match = re.match(r"^\d+\.\d+\.\d+\.\d+_(\d{4}-\d{2}-\d{2})_file\.cro$", filename)
    return match.group(1) if match else None
extract_ip(filename)

Extracts an IP address from a given filename.

The method uses a regular expression to match filenames that follow the pattern: <IP_ADDRESS>_<DATE>_file.cro, where:

  • <IP_ADDRESS> is a valid IPv4 address (e.g., 192.168.1.1).
  • <DATE> is in the format YYYY-MM-DD.

Parameters:

Name Type Description Default
filename str

The filename to extract the IP address from.

required

Returns:

Type Description
str

The extracted IP address if the filename matches the pattern, otherwise None.

Source code in src\ui\obtain_attendances_devices_dialog.py
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
def extract_ip(self, filename: str):
    """
    Extracts an IP address from a given filename.

    The method uses a regular expression to match filenames that follow the
    pattern: `<IP_ADDRESS>_<DATE>_file.cro`, where:

    - `<IP_ADDRESS>` is a valid IPv4 address (e.g., 192.168.1.1).
    - `<DATE>` is in the format YYYY-MM-DD.

    Args:
        filename (str): The filename to extract the IP address from.

    Returns:
        (str): The extracted IP address if the filename matches the pattern, 
             otherwise None.
    """
    match = re.match(r"^(\d+\.\d+\.\d+\.\d+)_\d{4}-\d{2}-\d{2}_file\.cro$", filename)
    return match.group(1) if match else None
format_file_uri(file_path)

Converts a local file path to a file URI.

This method takes a file path as input, replaces backslashes with forward slashes (to ensure compatibility across different operating systems), and encodes special characters to make the path safe for use in a URI. It then constructs a file URI by prefixing the encoded path with 'file:'.

Parameters:

Name Type Description Default
file_path str

The local file path to be converted into a file URI.

required

Returns:

Type Description
str

The formatted file URI.

Source code in src\ui\obtain_attendances_devices_dialog.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
def format_file_uri(self, file_path: str):
    """
    Converts a local file path to a file URI.

    This method takes a file path as input, replaces backslashes with forward slashes
    (to ensure compatibility across different operating systems), and encodes special
    characters to make the path safe for use in a URI. It then constructs a file URI
    by prefixing the encoded path with 'file:'.

    Args:
        file_path (str): The local file path to be converted into a file URI.

    Returns:
        (str): The formatted file URI.
    """
    file_url = urllib.parse.urljoin('file:', urllib.parse.quote(file_path.replace("\\", "/")))
    # logging.debug(file_url)
    return file_url
init_ui()

Initializes the user interface for the attendance devices dialog.

This method sets up the UI components, including labels, buttons, and layouts, to display and manage attendance device information. It also configures event handlers for user interactions.

Raises:

Type Description
BaseError

If an exception occurs during the initialization process, it raises a BaseError with code 3501 and the exception message.

UI Components
  • Header Labels: ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación"]
  • QPushButton: A button to retry failed connections, initially hidden.
  • QPushButton: A button to update and obtain attendance records, with updated text.
  • QLabel: A label to display the total number of attendances, initially hidden.
Source code in src\ui\obtain_attendances_devices_dialog.py
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
def init_ui(self):
    """
    Initializes the user interface for the attendance devices dialog.

    This method sets up the UI components, including labels, buttons, and layouts,
    to display and manage attendance device information. It also configures event
    handlers for user interactions.

    Raises:
        BaseError: If an exception occurs during the initialization process, it
                   raises a BaseError with code 3501 and the exception message.

    UI Components:
        - Header Labels: ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación"]
        - QPushButton: A button to retry failed connections, initially hidden.
        - QPushButton: A button to update and obtain attendance records, with updated text.
        - QLabel: A label to display the total number of attendances, initially hidden.
    """
    try:
        header_labels = ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación"]
        super().init_ui(header_labels=header_labels)
        self.btn_retry_failed_connection = QPushButton("Reintentar fallidos", self)
        self.btn_retry_failed_connection.clicked.connect(self.on_retry_failed_connection_clicked)
        self.button_layout.addWidget(self.btn_retry_failed_connection)
        self.btn_retry_failed_connection.setVisible(False)
        self.btn_update.setText("Obtener marcaciones")
        self.label_total_attendances = QLabel("Total de Marcaciones: 0", self)
        self.label_total_attendances.setAlignment(Qt.AlignCenter)
        self.layout().addWidget(self.label_total_attendances)
        self.label_total_attendances.setVisible(False)
    except Exception as e:
        raise BaseError(3501, str(e))
on_retry_failed_connection_clicked()

Handles the retry action for failed device connections.

This method is triggered when the "Retry Failed Connection" button is clicked. It attempts to reconnect to the devices that previously failed to connect and updates the UI to reflect the retry process. A separate thread is used to manage the reconnection attempts and progress updates.

Steps performed:

  • Hides certain UI elements (e.g., table widget, buttons) and updates labels.
  • Initializes and starts an OperationThread to handle the reconnection logic.
  • Connects thread signals to appropriate methods for progress updates and cleanup.
  • Hides the retry button after it is clicked.

Raises:

Type Description
BaseError

If an exception occurs during the execution of the method.

Attributes:

Name Type Description
self.failed_devices list

List of devices that failed to connect.

self.attendances_manager object

Manager responsible for handling device attendances.

Source code in src\ui\obtain_attendances_devices_dialog.py
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
def on_retry_failed_connection_clicked(self):
    """
    Handles the retry action for failed device connections.

    This method is triggered when the "Retry Failed Connection" button is clicked.
    It attempts to reconnect to the devices that previously failed to connect and
    updates the UI to reflect the retry process. A separate thread is used to manage
    the reconnection attempts and progress updates.

    Steps performed:

    - Hides certain UI elements (e.g., table widget, buttons) and updates labels.
    - Initializes and starts an `OperationThread` to handle the reconnection logic.
    - Connects thread signals to appropriate methods for progress updates and cleanup.
    - Hides the retry button after it is clicked.

    Raises:
        BaseError: If an exception occurs during the execution of the method.

    Attributes:
        self.failed_devices (list): List of devices that failed to connect.
        self.attendances_manager (object): Manager responsible for handling device attendances.
    """
    try:
        self.table_widget.setVisible(False)
        self.table_widget.sortByColumn(3, Qt.DescendingOrder)
        self.label_total_attendances.setVisible(False)
        self.label_updating.setText("Reintentando conexiones...")
        self.btn_update.setVisible(False)
        self.btn_activate_all.setVisible(False)
        self.btn_deactivate_all.setVisible(False)
        self.label_updating.setVisible(True)
        self.progress_bar.setVisible(True)
        self.progress_bar.setValue(0)
        # logging.debug(f"Dispositivos seleccionados: {self.failed_devices}")
        self.op_thread = OperationThread(self.attendances_manager.manage_devices_attendances, self.failed_devices)
        self.op_thread.progress_updated.connect(self.update_progress)
        self.op_thread.op_terminate.connect(self.op_terminate)
        self.op_thread.finished.connect(self.cleanup_thread)
        self.op_thread.start()
        self.btn_retry_failed_connection.setVisible(False)  # Hide the button after clicking
    except Exception as e:
        raise BaseError(3500, str(e))
op_terminate(devices=None)

Finalizes the operation of obtaining attendance data from devices and updates the UI accordingly.

Parameters:

Name Type Description Default
devices dict[str, dict[str, str]]

A dictionary where the keys are device IPs and the values are dictionaries containing device-specific information, such as "connection failed" status and "attendance count".

None
Functionality
  • Iterates through the rows of the table widget to update attendance data for each device.
  • Checks if the device IP is in the list of selected IPs and updates the corresponding table cell:
    • Marks devices with failed connections in red and adds them to the failed devices list.
    • Updates the attendance count for devices with successful connections and marks them in green.
  • Ensures the "Cant. de Marcaciones" column exists in the table and updates it with attendance data.
  • Calculates the total number of attendances across all devices.
  • Adjusts the table size, enables sorting, and sorts the table by a specific column in descending order.
  • Deselects all rows in the table and centers the window.
  • Attempts to check attendance files and handles any exceptions with a warning-level error.
  • Displays a retry button for failed connections and updates the total attendance label.

Raises:

Type Description
BaseErrorWithMessageBox

If an unexpected exception occurs during the operation, it is raised with an error message box.

Source code in src\ui\obtain_attendances_devices_dialog.py
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
def op_terminate(self, devices: dict[str, dict[str, str]] = None):
    """
    Finalizes the operation of obtaining attendance data from devices and updates the UI accordingly.

    Args:
        devices (dict[str, dict[str, str]]): A dictionary where the keys are device IPs and the values are dictionaries 
            containing device-specific information, such as "connection failed" status and "attendance count".

    Functionality:
        - Iterates through the rows of the table widget to update attendance data for each device.
        - Checks if the device IP is in the list of selected IPs and updates the corresponding table cell:
            - Marks devices with failed connections in red and adds them to the failed devices list.
            - Updates the attendance count for devices with successful connections and marks them in green.
        - Ensures the "Cant. de Marcaciones" column exists in the table and updates it with attendance data.
        - Calculates the total number of attendances across all devices.
        - Adjusts the table size, enables sorting, and sorts the table by a specific column in descending order.
        - Deselects all rows in the table and centers the window.
        - Attempts to check attendance files and handles any exceptions with a warning-level error.
        - Displays a retry button for failed connections and updates the total attendance label.

    Raises:
        BaseErrorWithMessageBox: If an unexpected exception occurs during the operation, it is raised with an error message box.
    """
    try:
        self.failed_devices = []
        #logging.debug(f'selected devices: {self.selected_ips} - devices from operation: {devices} - failed devices: {self.failed_devices}')

        actual_column = self.ensure_column_exists("Cant. de Marcaciones")

        total_marcaciones = 0
        for row in range (self.table_widget.rowCount()):
            ip_selected = self.table_widget.item(row, 3).text()  # Column 3 holds the IP
            attendances_count_item = QTableWidgetItem("")
            if ip_selected not in self.selected_ips:
                attendances_count_item.setBackground(QColor(Qt.white))
            else:
                device = devices.get(ip_selected)
                if device:
                    if device.get("connection failed", False):
                        attendances_count_item.setText("Conexión fallida")
                        attendances_count_item.setBackground(QColor(Qt.red))
                        self.failed_devices.append(ip_selected)
                    else:
                        attendance_count = device.get("attendance count")
                        try:
                            total_marcaciones += int(attendance_count)
                        except ValueError:
                            attendance_count = 0
                            BaseError(3500, f"Error al obtener la cantidad de marcaciones del dispositivo {ip_selected}")
                        attendances_count_item.setText(str(attendance_count))
                        attendances_count_item.setBackground(QColor(Qt.green))
            attendances_count_item.setFlags(attendances_count_item.flags() & ~Qt.ItemIsEditable)
            self.table_widget.setItem(row, actual_column, attendances_count_item)
        self.adjust_size_to_table()
        self.table_widget.setSortingEnabled(True)
        self.table_widget.sortByColumn(6, Qt.DescendingOrder)  
        self.deselect_all_rows()

        self.center_window()
        try:
            self.check_attendance_files()
        except Exception as e:
            BaseError(3000, str(e), level="warning")
        self.show_btn_retry_failed_connection()
        self.label_total_attendances.setText(f"Total de Marcaciones: {total_marcaciones}")
        self.label_total_attendances.setVisible(True)
        super().op_terminate()
    except Exception as e:
        raise BaseErrorWithMessageBox(3500, str(e), parent=self)
operation_with_selected_ips()

Executes an operation with the selected IP addresses.

This method hides the retry button and the total attendances label before attempting to perform the operation. It calls the parent class's operation_with_selected_ips method to execute the operation. If an exception occurs during the execution, it displays the retry button for failed connections.

Raises:

Type Description
Exception

Catches any exception that occurs during the operation

Source code in src\ui\obtain_attendances_devices_dialog.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def operation_with_selected_ips(self):
    """
    Executes an operation with the selected IP addresses.

    This method hides the retry button and the total attendances label before
    attempting to perform the operation. It calls the parent class's 
    `operation_with_selected_ips` method to execute the operation. If an 
    exception occurs during the execution, it displays the retry button 
    for failed connections.

    Exceptions:
        Exception: Catches any exception that occurs during the operation 
        and triggers the display of the retry button.
    """
    self.btn_retry_failed_connection.setVisible(False)
    self.label_total_attendances.setVisible(False)
    try:
        super().operation_with_selected_ips()
    except Exception as e:
        self.show_btn_retry_failed_connection()
    return
parse_attendance(line)

Parses a line of attendance data and extracts the timestamp.

Parameters:

Name Type Description Default
line str

A string containing attendance data, expected to have at least three parts separated by spaces, where the second and third parts represent the date and time respectively.

required

Returns:

Type Description
datetime or None

A datetime object representing the parsed timestamp if the line is well-formed and the date and time are valid. Returns None if the line is malformed or the date/time cannot be parsed.

Source code in src\ui\obtain_attendances_devices_dialog.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def parse_attendance(self, line: str):
    """
    Parses a line of attendance data and extracts the timestamp.

    Args:
        line (str): A string containing attendance data, expected to have at least
                    three parts separated by spaces, where the second and third parts
                    represent the date and time respectively.

    Returns:
        (datetime or None): A datetime object representing the parsed timestamp if the
                          line is well-formed and the date and time are valid.
                          Returns None if the line is malformed or the date/time
                          cannot be parsed.
    """
    parts = line.split()
    if len(parts) < 3:
        return None
    try:
        date_str, time_str = parts[1], parts[2]
        timestamp: datetime = datetime.strptime(f"{date_str} {time_str}", "%d/%m/%Y %H:%M")
        return timestamp
    except ValueError:
        return None
show_btn_retry_failed_connection()

Displays the "Retry Failed Connection" button if there are failed devices.

This method checks if the failed_devices attribute exists and contains one or more entries. If so, it makes the btn_retry_failed_connection button visible. If an exception occurs during execution, it raises a BaseError with an appropriate error code and message.

Raises:

Type Description
BaseError

If an exception occurs, it is wrapped in a BaseError with code 3500 and the exception message.

Source code in src\ui\obtain_attendances_devices_dialog.py
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
def show_btn_retry_failed_connection(self):
    """
    Displays the "Retry Failed Connection" button if there are failed devices.

    This method checks if the `failed_devices` attribute exists and contains 
    one or more entries. If so, it makes the `btn_retry_failed_connection` 
    button visible. If an exception occurs during execution, it raises a 
    `BaseError` with an appropriate error code and message.

    Raises:
        BaseError: If an exception occurs, it is wrapped in a `BaseError` 
                   with code 3500 and the exception message.
    """
    try:            
        if self.failed_devices and len(self.failed_devices) > 0:
            self.btn_retry_failed_connection.setVisible(True)
    except Exception as e:
        raise BaseError(3500, str(e))

operation_thread

OperationThread

Bases: QThread

Source code in src\ui\operation_thread.py
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
class OperationThread(QThread):
    op_terminate = pyqtSignal(dict)
    op_start_time = pyqtSignal(float)
    progress_updated = pyqtSignal(int, str, int, int)  # Signal for progress

    def __init__(self, op_func: Callable, selected_ips: list[str] = None, parent = None):
        """
        Initializes the OperationThread instance.

        Args:
            op_func (Callable): The operation function to be executed in the thread.
            selected_ips (list[str], optional): A list of selected IP addresses. Defaults to None.
            parent (QObject, optional): The parent object for the thread. Defaults to None.
        """
        super().__init__(parent)
        self.op_func: Callable = op_func
        self.selected_ips: list[str] = selected_ips
        self.result: dict = {}

    def run(self):
        """
        Executes the operation function (`op_func`) with the selected IPs or without them,
        handles the result, and emits appropriate signals.

        This method is designed to run in a separate thread to perform operations
        asynchronously. It captures any exceptions raised during execution and
        raises a `BaseError` with a specific error code and message.

        Attributes:
            selected_ips (list): A list of selected IP addresses to pass to the operation
                function. If not provided, the operation function is called without IPs.
            result (dict): The result of the operation function execution.

        Emits:
            op_terminate (dict): Signal emitted with the result of the operation function
                or an empty dictionary if the result is `None`.

        Raises:
            BaseError: If an exception occurs during the execution of the operation
                function, a `BaseError` is raised with error code 3000 and the exception
                message.
        """
        try:
            #import time
            #start_time: float = time.time()
            if self.selected_ips:
                self.result: dict = self.op_func(self.selected_ips, emit_progress=self.emit_progress)
            else:
                self.result: dict = self.op_func(emit_progress=self.emit_progress)
            if self.result is None:
                self.op_terminate.emit({})
            else:
                self.op_terminate.emit(self.result)
            #self.op_start_time.emit(start_time)
        except Exception as e:
            raise BaseError(3000, str(e), "critical")

    def emit_progress(self, percent_progress: int = None, device_progress: str = None, processed_devices: int = None, total_devices: int = None):
        """
        Emits a progress update signal with the provided progress details.

        Args:
            percent_progress (int, optional): The overall percentage of progress completed. Defaults to None.
            device_progress (str, optional): A string describing the progress of the current device. Defaults to None.
            processed_devices (int, optional): The number of devices that have been processed so far. Defaults to None.
            total_devices (int, optional): The total number of devices to be processed. Defaults to None.
        """
        self.progress_updated.emit(percent_progress, device_progress, processed_devices, total_devices)  # Emit the progress signal
__init__(op_func, selected_ips=None, parent=None)

Initializes the OperationThread instance.

Parameters:

Name Type Description Default
op_func Callable

The operation function to be executed in the thread.

required
selected_ips list[str]

A list of selected IP addresses. Defaults to None.

None
parent QObject

The parent object for the thread. Defaults to None.

None
Source code in src\ui\operation_thread.py
27
28
29
30
31
32
33
34
35
36
37
38
39
def __init__(self, op_func: Callable, selected_ips: list[str] = None, parent = None):
    """
    Initializes the OperationThread instance.

    Args:
        op_func (Callable): The operation function to be executed in the thread.
        selected_ips (list[str], optional): A list of selected IP addresses. Defaults to None.
        parent (QObject, optional): The parent object for the thread. Defaults to None.
    """
    super().__init__(parent)
    self.op_func: Callable = op_func
    self.selected_ips: list[str] = selected_ips
    self.result: dict = {}
emit_progress(percent_progress=None, device_progress=None, processed_devices=None, total_devices=None)

Emits a progress update signal with the provided progress details.

Parameters:

Name Type Description Default
percent_progress int

The overall percentage of progress completed. Defaults to None.

None
device_progress str

A string describing the progress of the current device. Defaults to None.

None
processed_devices int

The number of devices that have been processed so far. Defaults to None.

None
total_devices int

The total number of devices to be processed. Defaults to None.

None
Source code in src\ui\operation_thread.py
79
80
81
82
83
84
85
86
87
88
89
def emit_progress(self, percent_progress: int = None, device_progress: str = None, processed_devices: int = None, total_devices: int = None):
    """
    Emits a progress update signal with the provided progress details.

    Args:
        percent_progress (int, optional): The overall percentage of progress completed. Defaults to None.
        device_progress (str, optional): A string describing the progress of the current device. Defaults to None.
        processed_devices (int, optional): The number of devices that have been processed so far. Defaults to None.
        total_devices (int, optional): The total number of devices to be processed. Defaults to None.
    """
    self.progress_updated.emit(percent_progress, device_progress, processed_devices, total_devices)  # Emit the progress signal
run()

Executes the operation function (op_func) with the selected IPs or without them, handles the result, and emits appropriate signals.

This method is designed to run in a separate thread to perform operations asynchronously. It captures any exceptions raised during execution and raises a BaseError with a specific error code and message.

Attributes:

Name Type Description
selected_ips list

A list of selected IP addresses to pass to the operation function. If not provided, the operation function is called without IPs.

result dict

The result of the operation function execution.

Emits

op_terminate (dict): Signal emitted with the result of the operation function or an empty dictionary if the result is None.

Raises:

Type Description
BaseError

If an exception occurs during the execution of the operation function, a BaseError is raised with error code 3000 and the exception message.

Source code in src\ui\operation_thread.py
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
def run(self):
    """
    Executes the operation function (`op_func`) with the selected IPs or without them,
    handles the result, and emits appropriate signals.

    This method is designed to run in a separate thread to perform operations
    asynchronously. It captures any exceptions raised during execution and
    raises a `BaseError` with a specific error code and message.

    Attributes:
        selected_ips (list): A list of selected IP addresses to pass to the operation
            function. If not provided, the operation function is called without IPs.
        result (dict): The result of the operation function execution.

    Emits:
        op_terminate (dict): Signal emitted with the result of the operation function
            or an empty dictionary if the result is `None`.

    Raises:
        BaseError: If an exception occurs during the execution of the operation
            function, a `BaseError` is raised with error code 3000 and the exception
            message.
    """
    try:
        #import time
        #start_time: float = time.time()
        if self.selected_ips:
            self.result: dict = self.op_func(self.selected_ips, emit_progress=self.emit_progress)
        else:
            self.result: dict = self.op_func(emit_progress=self.emit_progress)
        if self.result is None:
            self.op_terminate.emit({})
        else:
            self.op_terminate.emit(self.result)
        #self.op_start_time.emit(start_time)
    except Exception as e:
        raise BaseError(3000, str(e), "critical")

ping_devices_dialog

PingDevicesDialog

Bases: SelectDevicesDialog

Source code in src\ui\ping_devices_dialog.py
 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
class PingDevicesDialog(SelectDevicesDialog):
    def __init__(self, parent=None):
        """
        Initializes the PingDevicesDialog class.

        This constructor sets up the dialog window for testing device connections.
        It initializes the user interface and sets the operation function to obtain
        connection information.

        Args:
            parent (QWidget, optional): The parent widget for this dialog. Defaults to None.

        Raises:
            BaseError: If an exception occurs during initialization, it raises a BaseError
                       with code 3501 and the exception message.
        """
        try:
            connection_info = ConnectionsInfo()
            super().__init__(parent, op_function=connection_info.obtain_connections_info, window_title="PROBAR CONEXIONES")
            self.init_ui()
        except Exception as e:
            raise BaseError(3501, str(e))

    def init_ui(self):
        """
        Initializes the user interface for the ping devices dialog.

        This method sets up the table headers and updates the text of the 
        update button to "Probar conexiones" (Test connections).

        Header Labels:
            - Distrito: Represents the district or region.
            - Modelo: Represents the device model.
            - Punto de Marcación: Represents the marking point.
            - IP: Represents the IP address of the device.
            - ID: Represents the device identifier.
            - Comunicación: Represents the communication status.

        Overrides:
            This method overrides the `init_ui` method from the parent class
            and passes the custom header labels to it.
        """
        header_labels = ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación"]
        super().init_ui(header_labels=header_labels)
        self.btn_update.setText("Probar conexiones")

    def op_terminate(self, devices=None):
        """
        Updates the table widget with the connection status and device information for a list of devices.
        This method iterates through the rows of the table widget and updates the columns with information
        such as connection status, attendance count, serial number, platform, and firmware version for each device.
        The information is retrieved from the `devices` dictionary, which maps IP addresses to device data.

        Args:
            devices (dict, optional): A dictionary where keys are IP addresses (str) and values are dictionaries
                                      containing device information. Each device dictionary may include:

                - "connection_failed" (bool): Indicates if the connection failed.
                - "device_info" (dict): Contains device-specific information such as:
                    - "attendance_count" (int or str): The number of attendance records.
                    - "serial_number" (str): The serial number of the device.
                    - "platform" (str): The platform of the device.
                    - "firmware_version" (str): The firmware version of the device.

        Raises:
            BaseErrorWithMessageBox: If an exception occurs during the operation, it raises a custom error
                                     with a message box displaying the error details.

        Notes:
            - The method ensures that specific columns ("Estado de Conexión", "Cant. de Marcaciones",
              "Número de Serie", "Plataforma", "Firmware") exist in the table widget before updating.
            - The background color of each cell is updated based on the device's connection status and
              availability of information.
            - The table widget is resized and sorted by the 6th column in descending order after updates.
            - All rows are deselected at the end of the operation.
        """
        try:
            #logging.debug(devices)

            # Ensure the "Estado de Conexión" column exists
            connection_column = self.ensure_column_exists("Estado de Conexión")
            attendance_count_column = self.ensure_column_exists("Cant. de Marcaciones")            
            serial_number_column = self.ensure_column_exists("Número de Serie")            
            platform_column = self.ensure_column_exists("Plataforma")
            firmware_version_column = self.ensure_column_exists("Firmware")

            for row in range(self.table_widget.rowCount()):
                ip_selected = self.table_widget.item(row, 3).text()  # Column 3 holds the IP
                connection_item = QTableWidgetItem("")
                attendance_count_item = QTableWidgetItem("")
                serial_number_item = QTableWidgetItem("")
                platform_item = QTableWidgetItem("")
                firmware_version_item = QTableWidgetItem("")

                if ip_selected not in self.selected_ips:
                    connection_item.setBackground(QColor(Qt.white))
                    attendance_count_item.setBackground(QColor(Qt.white))
                    serial_number_item.setBackground(QColor(Qt.white))
                    platform_item.setBackground(QColor(Qt.white))
                    firmware_version_item.setBackground(QColor(Qt.white))
                else:
                    device = devices[ip_selected]

                    if device:
                        if device.get("connection_failed"):
                            connection_item.setText("Conexión fallida")
                            connection_item.setBackground(QColor(Qt.red))
                        else:
                            connection_item.setText("Conexión exitosa")
                            connection_item.setBackground(QColor(Qt.green))

                        #logging.debug(str(device))
                        #logging.debug(str(device.get("device_info", "")))
                        if not device.get("device_info") or not device["device_info"].get("attendance_count"):
                            attendance_count_item.setText("No aplica")
                            attendance_count_item.setBackground(QColor(Qt.gray))
                        else:
                            attendance_count_item.setText(str(device.get("device_info", "").get("attendance_count", "")))
                            attendance_count_item.setBackground(QColor(Qt.green))

                        if not device.get("device_info") or not device["device_info"].get("serial_number"):
                            serial_number_item.setText("No aplica")
                            serial_number_item.setBackground(QColor(Qt.gray))
                        else:
                            serial_number_item.setText(str(device.get("device_info", "").get("serial_number", "")))
                            serial_number_item.setBackground(QColor(Qt.green))

                        if not device.get("device_info") or not device["device_info"].get("platform"):
                            platform_item.setText("No aplica")
                            platform_item.setBackground(QColor(Qt.gray))
                        else:
                            platform_item.setText(str(device.get("device_info", "").get("platform", "")))
                            platform_item.setBackground(QColor(Qt.green))

                        if not device.get("device_info") or not device["device_info"].get("firmware_version"):
                            firmware_version_item.setText("No aplica")
                            firmware_version_item.setBackground(QColor(Qt.gray))
                        else:
                            firmware_version_item.setText(str(device.get("device_info", "").get("firmware_version", "")))
                            firmware_version_item.setBackground(QColor(Qt.green))

                connection_item.setFlags(connection_item.flags() & ~Qt.ItemIsEditable)
                attendance_count_item.setFlags(attendance_count_item.flags() & ~Qt.ItemIsEditable)
                serial_number_item.setFlags(serial_number_item.flags() & ~Qt.ItemIsEditable)
                platform_item.setFlags(platform_item.flags() & ~Qt.ItemIsEditable)
                firmware_version_item.setFlags(firmware_version_item.flags() & ~Qt.ItemIsEditable)

                self.table_widget.setItem(row, connection_column, connection_item)
                self.table_widget.setItem(row, attendance_count_column, attendance_count_item)
                self.table_widget.setItem(row, serial_number_column, serial_number_item)
                self.table_widget.setItem(row, platform_column, platform_item)
                self.table_widget.setItem(row, firmware_version_column, firmware_version_item)

            self.adjust_size_to_table()

            self.table_widget.setSortingEnabled(True)
            self.table_widget.sortByColumn(6, Qt.DescendingOrder)  

            self.deselect_all_rows()
            super().op_terminate()
        except Exception as e:
            raise BaseErrorWithMessageBox(3500, str(e), parent=self)
__init__(parent=None)

Initializes the PingDevicesDialog class.

This constructor sets up the dialog window for testing device connections. It initializes the user interface and sets the operation function to obtain connection information.

Parameters:

Name Type Description Default
parent QWidget

The parent widget for this dialog. Defaults to None.

None

Raises:

Type Description
BaseError

If an exception occurs during initialization, it raises a BaseError with code 3501 and the exception message.

Source code in src\ui\ping_devices_dialog.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def __init__(self, parent=None):
    """
    Initializes the PingDevicesDialog class.

    This constructor sets up the dialog window for testing device connections.
    It initializes the user interface and sets the operation function to obtain
    connection information.

    Args:
        parent (QWidget, optional): The parent widget for this dialog. Defaults to None.

    Raises:
        BaseError: If an exception occurs during initialization, it raises a BaseError
                   with code 3501 and the exception message.
    """
    try:
        connection_info = ConnectionsInfo()
        super().__init__(parent, op_function=connection_info.obtain_connections_info, window_title="PROBAR CONEXIONES")
        self.init_ui()
    except Exception as e:
        raise BaseError(3501, str(e))
init_ui()

Initializes the user interface for the ping devices dialog.

This method sets up the table headers and updates the text of the update button to "Probar conexiones" (Test connections).

Header Labels
  • Distrito: Represents the district or region.
  • Modelo: Represents the device model.
  • Punto de Marcación: Represents the marking point.
  • IP: Represents the IP address of the device.
  • ID: Represents the device identifier.
  • Comunicación: Represents the communication status.
Overrides

This method overrides the init_ui method from the parent class and passes the custom header labels to it.

Source code in src\ui\ping_devices_dialog.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def init_ui(self):
    """
    Initializes the user interface for the ping devices dialog.

    This method sets up the table headers and updates the text of the 
    update button to "Probar conexiones" (Test connections).

    Header Labels:
        - Distrito: Represents the district or region.
        - Modelo: Represents the device model.
        - Punto de Marcación: Represents the marking point.
        - IP: Represents the IP address of the device.
        - ID: Represents the device identifier.
        - Comunicación: Represents the communication status.

    Overrides:
        This method overrides the `init_ui` method from the parent class
        and passes the custom header labels to it.
    """
    header_labels = ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación"]
    super().init_ui(header_labels=header_labels)
    self.btn_update.setText("Probar conexiones")
op_terminate(devices=None)

Updates the table widget with the connection status and device information for a list of devices. This method iterates through the rows of the table widget and updates the columns with information such as connection status, attendance count, serial number, platform, and firmware version for each device. The information is retrieved from the devices dictionary, which maps IP addresses to device data.

Parameters:

Name Type Description Default
devices dict

A dictionary where keys are IP addresses (str) and values are dictionaries containing device information. Each device dictionary may include:

  • "connection_failed" (bool): Indicates if the connection failed.
  • "device_info" (dict): Contains device-specific information such as:
    • "attendance_count" (int or str): The number of attendance records.
    • "serial_number" (str): The serial number of the device.
    • "platform" (str): The platform of the device.
    • "firmware_version" (str): The firmware version of the device.
None

Raises:

Type Description
BaseErrorWithMessageBox

If an exception occurs during the operation, it raises a custom error with a message box displaying the error details.

Notes
  • The method ensures that specific columns ("Estado de Conexión", "Cant. de Marcaciones", "Número de Serie", "Plataforma", "Firmware") exist in the table widget before updating.
  • The background color of each cell is updated based on the device's connection status and availability of information.
  • The table widget is resized and sorted by the 6th column in descending order after updates.
  • All rows are deselected at the end of the operation.
Source code in src\ui\ping_devices_dialog.py
 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
def op_terminate(self, devices=None):
    """
    Updates the table widget with the connection status and device information for a list of devices.
    This method iterates through the rows of the table widget and updates the columns with information
    such as connection status, attendance count, serial number, platform, and firmware version for each device.
    The information is retrieved from the `devices` dictionary, which maps IP addresses to device data.

    Args:
        devices (dict, optional): A dictionary where keys are IP addresses (str) and values are dictionaries
                                  containing device information. Each device dictionary may include:

            - "connection_failed" (bool): Indicates if the connection failed.
            - "device_info" (dict): Contains device-specific information such as:
                - "attendance_count" (int or str): The number of attendance records.
                - "serial_number" (str): The serial number of the device.
                - "platform" (str): The platform of the device.
                - "firmware_version" (str): The firmware version of the device.

    Raises:
        BaseErrorWithMessageBox: If an exception occurs during the operation, it raises a custom error
                                 with a message box displaying the error details.

    Notes:
        - The method ensures that specific columns ("Estado de Conexión", "Cant. de Marcaciones",
          "Número de Serie", "Plataforma", "Firmware") exist in the table widget before updating.
        - The background color of each cell is updated based on the device's connection status and
          availability of information.
        - The table widget is resized and sorted by the 6th column in descending order after updates.
        - All rows are deselected at the end of the operation.
    """
    try:
        #logging.debug(devices)

        # Ensure the "Estado de Conexión" column exists
        connection_column = self.ensure_column_exists("Estado de Conexión")
        attendance_count_column = self.ensure_column_exists("Cant. de Marcaciones")            
        serial_number_column = self.ensure_column_exists("Número de Serie")            
        platform_column = self.ensure_column_exists("Plataforma")
        firmware_version_column = self.ensure_column_exists("Firmware")

        for row in range(self.table_widget.rowCount()):
            ip_selected = self.table_widget.item(row, 3).text()  # Column 3 holds the IP
            connection_item = QTableWidgetItem("")
            attendance_count_item = QTableWidgetItem("")
            serial_number_item = QTableWidgetItem("")
            platform_item = QTableWidgetItem("")
            firmware_version_item = QTableWidgetItem("")

            if ip_selected not in self.selected_ips:
                connection_item.setBackground(QColor(Qt.white))
                attendance_count_item.setBackground(QColor(Qt.white))
                serial_number_item.setBackground(QColor(Qt.white))
                platform_item.setBackground(QColor(Qt.white))
                firmware_version_item.setBackground(QColor(Qt.white))
            else:
                device = devices[ip_selected]

                if device:
                    if device.get("connection_failed"):
                        connection_item.setText("Conexión fallida")
                        connection_item.setBackground(QColor(Qt.red))
                    else:
                        connection_item.setText("Conexión exitosa")
                        connection_item.setBackground(QColor(Qt.green))

                    #logging.debug(str(device))
                    #logging.debug(str(device.get("device_info", "")))
                    if not device.get("device_info") or not device["device_info"].get("attendance_count"):
                        attendance_count_item.setText("No aplica")
                        attendance_count_item.setBackground(QColor(Qt.gray))
                    else:
                        attendance_count_item.setText(str(device.get("device_info", "").get("attendance_count", "")))
                        attendance_count_item.setBackground(QColor(Qt.green))

                    if not device.get("device_info") or not device["device_info"].get("serial_number"):
                        serial_number_item.setText("No aplica")
                        serial_number_item.setBackground(QColor(Qt.gray))
                    else:
                        serial_number_item.setText(str(device.get("device_info", "").get("serial_number", "")))
                        serial_number_item.setBackground(QColor(Qt.green))

                    if not device.get("device_info") or not device["device_info"].get("platform"):
                        platform_item.setText("No aplica")
                        platform_item.setBackground(QColor(Qt.gray))
                    else:
                        platform_item.setText(str(device.get("device_info", "").get("platform", "")))
                        platform_item.setBackground(QColor(Qt.green))

                    if not device.get("device_info") or not device["device_info"].get("firmware_version"):
                        firmware_version_item.setText("No aplica")
                        firmware_version_item.setBackground(QColor(Qt.gray))
                    else:
                        firmware_version_item.setText(str(device.get("device_info", "").get("firmware_version", "")))
                        firmware_version_item.setBackground(QColor(Qt.green))

            connection_item.setFlags(connection_item.flags() & ~Qt.ItemIsEditable)
            attendance_count_item.setFlags(attendance_count_item.flags() & ~Qt.ItemIsEditable)
            serial_number_item.setFlags(serial_number_item.flags() & ~Qt.ItemIsEditable)
            platform_item.setFlags(platform_item.flags() & ~Qt.ItemIsEditable)
            firmware_version_item.setFlags(firmware_version_item.flags() & ~Qt.ItemIsEditable)

            self.table_widget.setItem(row, connection_column, connection_item)
            self.table_widget.setItem(row, attendance_count_column, attendance_count_item)
            self.table_widget.setItem(row, serial_number_column, serial_number_item)
            self.table_widget.setItem(row, platform_column, platform_item)
            self.table_widget.setItem(row, firmware_version_column, firmware_version_item)

        self.adjust_size_to_table()

        self.table_widget.setSortingEnabled(True)
        self.table_widget.sortByColumn(6, Qt.DescendingOrder)  

        self.deselect_all_rows()
        super().op_terminate()
    except Exception as e:
        raise BaseErrorWithMessageBox(3500, str(e), parent=self)

restart_devices_dialog

RestartDevicesDialog

Bases: SelectDevicesDialog

Source code in src\ui\restart_devices_dialog.py
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
class RestartDevicesDialog(SelectDevicesDialog):
    def __init__(self, parent=None):
        """
        Initializes the RestartDevicesDialog class.

        Args:
            parent (QWidget, optional): The parent widget for this dialog. Defaults to None.

        Raises:
            BaseError: If an exception occurs during initialization, it raises a BaseError
                       with code 3501 and the exception message.
        """
        try:
            restart_manager: RestartManager = RestartManager()
            super().__init__(parent, op_function=restart_manager.restart_devices, window_title="REINICIAR DISPOSITIVOS")
            self.init_ui()
        except Exception as e:
            raise BaseError(3501, str(e))

    def init_ui(self):
        """
        Initializes the user interface for the restart devices dialog.

        This method sets up the table headers with predefined labels and updates
        the text of the update button to indicate its purpose as restarting devices.

        Header Labels:
            - "Distrito": Represents the district information.
            - "Modelo": Represents the device model.
            - "Punto de Marcación": Represents the marking point.
            - "IP": Represents the IP address of the device.
            - "ID": Represents the device ID.
            - "Comunicación": Represents the communication status of the device.
        """
        header_labels: list[str] = ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación"]
        super().init_ui(header_labels=header_labels)
        self.btn_update.setText("Reiniciar dispositivos")

    def op_terminate(self, devices_errors: dict[str, dict[str, bool]] = None):
        """
        Handles the termination operation for devices, displaying appropriate messages
        based on the presence of errors.

        Args:
            devices_errors (dict[str, dict[str, bool]]): A dictionary containing device error
                information. The keys are device identifiers, and the values are dictionaries
                with error details. Defaults to None.

        Behavior:
            - If `devices_errors` contains entries, an error message box is displayed with
              the list of problematic devices.
            - If `devices_errors` is empty or None, an information message box is displayed
              indicating successful device restarts.
            - Logs the `devices_errors` dictionary for debugging purposes.
            - Calls the parent class's `op_terminate` method after handling the operation.

        Exceptions:
            - Logs any exceptions that occur during the execution of the method.
        """
        try:
            #logging.debug(devices_errors)
            if len(devices_errors) > 0:
                error: BaseError = BaseError(2002, f"{', '.join(devices_errors.keys())}")
                error.show_message_box(parent=self)
            else:
                QMessageBox.information(self, "Éxito", "Dispositivos reiniciados correctamente")
            super().op_terminate()
        except Exception as e:
            logging.error(e)
__init__(parent=None)

Initializes the RestartDevicesDialog class.

Parameters:

Name Type Description Default
parent QWidget

The parent widget for this dialog. Defaults to None.

None

Raises:

Type Description
BaseError

If an exception occurs during initialization, it raises a BaseError with code 3501 and the exception message.

Source code in src\ui\restart_devices_dialog.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def __init__(self, parent=None):
    """
    Initializes the RestartDevicesDialog class.

    Args:
        parent (QWidget, optional): The parent widget for this dialog. Defaults to None.

    Raises:
        BaseError: If an exception occurs during initialization, it raises a BaseError
                   with code 3501 and the exception message.
    """
    try:
        restart_manager: RestartManager = RestartManager()
        super().__init__(parent, op_function=restart_manager.restart_devices, window_title="REINICIAR DISPOSITIVOS")
        self.init_ui()
    except Exception as e:
        raise BaseError(3501, str(e))
init_ui()

Initializes the user interface for the restart devices dialog.

This method sets up the table headers with predefined labels and updates the text of the update button to indicate its purpose as restarting devices.

Header Labels
  • "Distrito": Represents the district information.
  • "Modelo": Represents the device model.
  • "Punto de Marcación": Represents the marking point.
  • "IP": Represents the IP address of the device.
  • "ID": Represents the device ID.
  • "Comunicación": Represents the communication status of the device.
Source code in src\ui\restart_devices_dialog.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def init_ui(self):
    """
    Initializes the user interface for the restart devices dialog.

    This method sets up the table headers with predefined labels and updates
    the text of the update button to indicate its purpose as restarting devices.

    Header Labels:
        - "Distrito": Represents the district information.
        - "Modelo": Represents the device model.
        - "Punto de Marcación": Represents the marking point.
        - "IP": Represents the IP address of the device.
        - "ID": Represents the device ID.
        - "Comunicación": Represents the communication status of the device.
    """
    header_labels: list[str] = ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación"]
    super().init_ui(header_labels=header_labels)
    self.btn_update.setText("Reiniciar dispositivos")
op_terminate(devices_errors=None)

Handles the termination operation for devices, displaying appropriate messages based on the presence of errors.

Parameters:

Name Type Description Default
devices_errors dict[str, dict[str, bool]]

A dictionary containing device error information. The keys are device identifiers, and the values are dictionaries with error details. Defaults to None.

None
Behavior
  • If devices_errors contains entries, an error message box is displayed with the list of problematic devices.
  • If devices_errors is empty or None, an information message box is displayed indicating successful device restarts.
  • Logs the devices_errors dictionary for debugging purposes.
  • Calls the parent class's op_terminate method after handling the operation.
Source code in src\ui\restart_devices_dialog.py
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
def op_terminate(self, devices_errors: dict[str, dict[str, bool]] = None):
    """
    Handles the termination operation for devices, displaying appropriate messages
    based on the presence of errors.

    Args:
        devices_errors (dict[str, dict[str, bool]]): A dictionary containing device error
            information. The keys are device identifiers, and the values are dictionaries
            with error details. Defaults to None.

    Behavior:
        - If `devices_errors` contains entries, an error message box is displayed with
          the list of problematic devices.
        - If `devices_errors` is empty or None, an information message box is displayed
          indicating successful device restarts.
        - Logs the `devices_errors` dictionary for debugging purposes.
        - Calls the parent class's `op_terminate` method after handling the operation.

    Exceptions:
        - Logs any exceptions that occur during the execution of the method.
    """
    try:
        #logging.debug(devices_errors)
        if len(devices_errors) > 0:
            error: BaseError = BaseError(2002, f"{', '.join(devices_errors.keys())}")
            error.show_message_box(parent=self)
        else:
            QMessageBox.information(self, "Éxito", "Dispositivos reiniciados correctamente")
        super().op_terminate()
    except Exception as e:
        logging.error(e)

update_time_device_dialog

UpdateTimeDeviceDialog

Bases: SelectDevicesDialog

Source code in src\ui\update_time_device_dialog.py
 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
class UpdateTimeDeviceDialog(SelectDevicesDialog):
    def __init__(self, parent = None):
        """
        Initializes the UpdateTimeDeviceDialog class.

        Args:
            parent (Optional[QWidget]): The parent widget for this dialog. Defaults to None.

        Attributes:
            device_info (dict[str, bool]): A dictionary containing device information loaded from `load_device_info()`.

        Raises:
            BaseError: If an exception occurs during initialization, it raises a BaseError with code 3501 and the exception message.
        """
        try:
            hour_manager = HourManager()
            super().__init__(parent, op_function=hour_manager.manage_hour_devices, window_title="ACTUALIZAR HORA")
            self.device_info: dict[str, bool] = self.load_device_info()
            self.init_ui()
        except Exception as e:
            raise BaseError(3501, str(e))

    def init_ui(self):
        """
        Initializes the user interface for the update time device dialog.

        This method sets up the table headers with predefined labels and updates
        the text of the update button to "Actualizar hora".

        Header Labels:
            - Distrito
            - Modelo
            - Punto de Marcación
            - IP
            - ID
            - Comunicación
        """
        header_labels: list[str] = ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación"]
        super().init_ui(header_labels=header_labels)
        self.btn_update.setText("Actualizar hora")

    def load_device_info(self):
        """
        Loads device information from a file and returns it as a dictionary.

        The method reads the file "info_devices.txt" line by line, extracts the IP address
        and battery status from each line, and stores them in a dictionary. The IP address
        is used as the key, and the battery status (a boolean) is used as the value.

        Returns:
            (dict[str, bool]): A dictionary where the keys are IP addresses (str) and the
                            values are battery statuses (bool).

        Raises:
            BaseErrorWithMessageBox: If an error occurs while reading the file, an exception
            is raised with an error code and the exception message.
        """
        device_info: dict[str, bool] = {}
        try:
            with open("info_devices.txt", "r") as file:
                for line in file:
                    parts: list[str] = line.strip().split(" - ")
                    if len(parts) == 8:
                        ip: str = parts[3]
                        battery_status: bool = parts[6] == "True"
                        device_info[ip] = battery_status
        except Exception as e:
            raise BaseErrorWithMessageBox(3001, str(e), parent=self)
        return device_info

    def op_terminate(self, devices_errors: dict[str, dict[str, bool]] = None):
        """
        Updates the table widget with the connection and battery status of devices.
        This method processes the `devices_errors` dictionary to update the table widget
        with the connection and battery status for each device. It ensures that the required
        columns exist in the table, updates the rows with the appropriate status and colors,
        and adjusts the table's size and sorting.

        Args:
            devices_errors (dict[str, dict[str, bool]], optional): A dictionary where the keys
                are device IP addresses, and the values are dictionaries containing error
                statuses for the device. The inner dictionary can have the following keys:

                - "connection failed" (bool): Indicates if the connection to the device failed.
                - "battery failing" (bool): Indicates if the device's battery is failing.

                Defaults to None.

        Raises:
            BaseErrorWithMessageBox: If an exception occurs during the operation, it raises
                a custom error with a message box displaying the error details.
        """
        #logging.debug(devices_errors)
        try:
            connection_column = self.ensure_column_exists("Estado de Conexión")
            battery_column = self.ensure_column_exists("Estado de Pila")

            for row in range (self.table_widget.rowCount()):
                ip_selected: str = self.table_widget.item(row, 3).text()  # Column 3 holds the IP
                connection_item: QTableWidgetItem = QTableWidgetItem("")
                battery_item: QTableWidgetItem = QTableWidgetItem("")
                if ip_selected not in self.selected_ips:
                    connection_item.setBackground(QColor(Qt.white))
                    battery_item.setBackground(QColor(Qt.white))
                else:
                    device: dict[str, bool] = devices_errors.get(ip_selected)
                    if device:
                        if device.get("connection failed"):
                            connection_item.setText("Conexión fallida")
                            connection_item.setBackground(QColor(Qt.red))
                        else:
                            connection_item.setText("Conexión exitosa")
                            connection_item.setBackground(QColor(Qt.green))
                        if device.get("connection failed"):
                            battery_item.setText("No aplica")
                            battery_item.setBackground(QColor(Qt.gray))
                        else:
                            battery_failing: bool = device.get("battery failing") or not self.device_info.get(ip_selected, True)
                            if battery_failing:
                                battery_item.setText("Pila fallando")
                                battery_item.setBackground(QColor(Qt.red))
                            else:
                                battery_item.setText("Pila funcionando")
                                battery_item.setBackground(QColor(Qt.green))
                connection_item.setFlags(connection_item.flags() & ~Qt.ItemIsEditable)
                self.table_widget.setItem(row, connection_column, connection_item)
                battery_item.setFlags(battery_item.flags() & ~Qt.ItemIsEditable)
                self.table_widget.setItem(row, battery_column, battery_item)

            self.adjust_size_to_table()

            self.table_widget.setSortingEnabled(True)
            self.table_widget.sortByColumn(6, Qt.DescendingOrder)    

            self.deselect_all_rows()
            super().op_terminate()
            self.table_widget.setVisible(True)
        except Exception as e:
            raise BaseErrorWithMessageBox(3500, str(e), parent=self)
__init__(parent=None)

Initializes the UpdateTimeDeviceDialog class.

Parameters:

Name Type Description Default
parent Optional[QWidget]

The parent widget for this dialog. Defaults to None.

None

Attributes:

Name Type Description
device_info dict[str, bool]

A dictionary containing device information loaded from load_device_info().

Raises:

Type Description
BaseError

If an exception occurs during initialization, it raises a BaseError with code 3501 and the exception message.

Source code in src\ui\update_time_device_dialog.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def __init__(self, parent = None):
    """
    Initializes the UpdateTimeDeviceDialog class.

    Args:
        parent (Optional[QWidget]): The parent widget for this dialog. Defaults to None.

    Attributes:
        device_info (dict[str, bool]): A dictionary containing device information loaded from `load_device_info()`.

    Raises:
        BaseError: If an exception occurs during initialization, it raises a BaseError with code 3501 and the exception message.
    """
    try:
        hour_manager = HourManager()
        super().__init__(parent, op_function=hour_manager.manage_hour_devices, window_title="ACTUALIZAR HORA")
        self.device_info: dict[str, bool] = self.load_device_info()
        self.init_ui()
    except Exception as e:
        raise BaseError(3501, str(e))
init_ui()

Initializes the user interface for the update time device dialog.

This method sets up the table headers with predefined labels and updates the text of the update button to "Actualizar hora".

Header Labels
  • Distrito
  • Modelo
  • Punto de Marcación
  • IP
  • ID
  • Comunicación
Source code in src\ui\update_time_device_dialog.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def init_ui(self):
    """
    Initializes the user interface for the update time device dialog.

    This method sets up the table headers with predefined labels and updates
    the text of the update button to "Actualizar hora".

    Header Labels:
        - Distrito
        - Modelo
        - Punto de Marcación
        - IP
        - ID
        - Comunicación
    """
    header_labels: list[str] = ["Distrito", "Modelo", "Punto de Marcación", "IP", "ID", "Comunicación"]
    super().init_ui(header_labels=header_labels)
    self.btn_update.setText("Actualizar hora")
load_device_info()

Loads device information from a file and returns it as a dictionary.

The method reads the file "info_devices.txt" line by line, extracts the IP address and battery status from each line, and stores them in a dictionary. The IP address is used as the key, and the battery status (a boolean) is used as the value.

Returns:

Type Description
dict[str, bool]

A dictionary where the keys are IP addresses (str) and the values are battery statuses (bool).

Raises:

Type Description
BaseErrorWithMessageBox

If an error occurs while reading the file, an exception

Source code in src\ui\update_time_device_dialog.py
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
def load_device_info(self):
    """
    Loads device information from a file and returns it as a dictionary.

    The method reads the file "info_devices.txt" line by line, extracts the IP address
    and battery status from each line, and stores them in a dictionary. The IP address
    is used as the key, and the battery status (a boolean) is used as the value.

    Returns:
        (dict[str, bool]): A dictionary where the keys are IP addresses (str) and the
                        values are battery statuses (bool).

    Raises:
        BaseErrorWithMessageBox: If an error occurs while reading the file, an exception
        is raised with an error code and the exception message.
    """
    device_info: dict[str, bool] = {}
    try:
        with open("info_devices.txt", "r") as file:
            for line in file:
                parts: list[str] = line.strip().split(" - ")
                if len(parts) == 8:
                    ip: str = parts[3]
                    battery_status: bool = parts[6] == "True"
                    device_info[ip] = battery_status
    except Exception as e:
        raise BaseErrorWithMessageBox(3001, str(e), parent=self)
    return device_info
op_terminate(devices_errors=None)

Updates the table widget with the connection and battery status of devices. This method processes the devices_errors dictionary to update the table widget with the connection and battery status for each device. It ensures that the required columns exist in the table, updates the rows with the appropriate status and colors, and adjusts the table's size and sorting.

Parameters:

Name Type Description Default
devices_errors dict[str, dict[str, bool]]

A dictionary where the keys are device IP addresses, and the values are dictionaries containing error statuses for the device. The inner dictionary can have the following keys:

  • "connection failed" (bool): Indicates if the connection to the device failed.
  • "battery failing" (bool): Indicates if the device's battery is failing.

Defaults to None.

None

Raises:

Type Description
BaseErrorWithMessageBox

If an exception occurs during the operation, it raises a custom error with a message box displaying the error details.

Source code in src\ui\update_time_device_dialog.py
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
def op_terminate(self, devices_errors: dict[str, dict[str, bool]] = None):
    """
    Updates the table widget with the connection and battery status of devices.
    This method processes the `devices_errors` dictionary to update the table widget
    with the connection and battery status for each device. It ensures that the required
    columns exist in the table, updates the rows with the appropriate status and colors,
    and adjusts the table's size and sorting.

    Args:
        devices_errors (dict[str, dict[str, bool]], optional): A dictionary where the keys
            are device IP addresses, and the values are dictionaries containing error
            statuses for the device. The inner dictionary can have the following keys:

            - "connection failed" (bool): Indicates if the connection to the device failed.
            - "battery failing" (bool): Indicates if the device's battery is failing.

            Defaults to None.

    Raises:
        BaseErrorWithMessageBox: If an exception occurs during the operation, it raises
            a custom error with a message box displaying the error details.
    """
    #logging.debug(devices_errors)
    try:
        connection_column = self.ensure_column_exists("Estado de Conexión")
        battery_column = self.ensure_column_exists("Estado de Pila")

        for row in range (self.table_widget.rowCount()):
            ip_selected: str = self.table_widget.item(row, 3).text()  # Column 3 holds the IP
            connection_item: QTableWidgetItem = QTableWidgetItem("")
            battery_item: QTableWidgetItem = QTableWidgetItem("")
            if ip_selected not in self.selected_ips:
                connection_item.setBackground(QColor(Qt.white))
                battery_item.setBackground(QColor(Qt.white))
            else:
                device: dict[str, bool] = devices_errors.get(ip_selected)
                if device:
                    if device.get("connection failed"):
                        connection_item.setText("Conexión fallida")
                        connection_item.setBackground(QColor(Qt.red))
                    else:
                        connection_item.setText("Conexión exitosa")
                        connection_item.setBackground(QColor(Qt.green))
                    if device.get("connection failed"):
                        battery_item.setText("No aplica")
                        battery_item.setBackground(QColor(Qt.gray))
                    else:
                        battery_failing: bool = device.get("battery failing") or not self.device_info.get(ip_selected, True)
                        if battery_failing:
                            battery_item.setText("Pila fallando")
                            battery_item.setBackground(QColor(Qt.red))
                        else:
                            battery_item.setText("Pila funcionando")
                            battery_item.setBackground(QColor(Qt.green))
            connection_item.setFlags(connection_item.flags() & ~Qt.ItemIsEditable)
            self.table_widget.setItem(row, connection_column, connection_item)
            battery_item.setFlags(battery_item.flags() & ~Qt.ItemIsEditable)
            self.table_widget.setItem(row, battery_column, battery_item)

        self.adjust_size_to_table()

        self.table_widget.setSortingEnabled(True)
        self.table_widget.sortByColumn(6, Qt.DescendingOrder)    

        self.deselect_all_rows()
        super().op_terminate()
        self.table_widget.setVisible(True)
    except Exception as e:
        raise BaseErrorWithMessageBox(3500, str(e), parent=self)