#!/usr/bin/env python3 import dbus import dbus.mainloop.glib from gi.repository import AppIndicator3 from gi.repository import Gtk, GLib, GObject import gi import subprocess import threading import time import sys # Especificar las versiones antes de importar gi.require_version('Gtk', '3.0') class NetworkMenu(Gtk.Window): def __init__(self): super().__init__(title="Menú de Red") # Título estático self.set_default_size(300, 200) self.set_border_width(10) # Crear una caja vertical para organizar los widgets vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) self.add(vbox) # Etiqueta para mostrar el estado de la red self.status_label = Gtk.Label(label="Estado de la red: Desconocido") vbox.pack_start(self.status_label, False, False, 0) # Construir el menú self.build_menu(vbox) # Inicializar D-Bus dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) self.bus = dbus.SystemBus() try: self.network_manager = self.bus.get_object( 'org.freedesktop.NetworkManager', '/org/freedesktop/NetworkManager') self.network_manager_props = dbus.Interface( self.network_manager, 'org.freedesktop.DBus.Properties') except dbus.exceptions.DBusException as e: print( f"Error al conectar con NetworkManager a través de D-Bus: {e}", file=sys.stderr) sys.exit(1) # Suscribirse a las señales de cambio de propiedades self.bus.add_signal_receiver( self.on_properties_changed, dbus_interface='org.freedesktop.DBus.Properties', signal_name='PropertiesChanged', arg0='org.freedesktop.NetworkManager', path='/org/freedesktop/NetworkManager', ) # Obtener estado inicial self.update_status_initial() def build_menu(self, vbox): # Botón Conectar WiFi self.item_connect = Gtk.Button(label='Conectar WiFi') self.item_connect.connect('clicked', self.connect_wifi) vbox.pack_start(self.item_connect, True, True, 0) # Botón Desconectar WiFi self.item_disconnect = Gtk.Button(label='Desconectar WiFi') self.item_disconnect.connect('clicked', self.disconnect_wifi) vbox.pack_start(self.item_disconnect, True, True, 0) # Botón Ver Redes Disponibles self.item_view_networks = Gtk.Button(label='Ver Redes Disponibles') self.item_view_networks.connect('clicked', self.show_network_list) vbox.pack_start(self.item_view_networks, True, True, 0) # Botón Olvidar Red self.item_forget_network = Gtk.Button(label='Olvidar Red') self.item_forget_network.connect('clicked', self.forget_network) vbox.pack_start(self.item_forget_network, True, True, 0) # Botón Crear AP self.item_create_ap = Gtk.Button(label='Crear AP') self.item_create_ap.connect('clicked', self.create_ap) vbox.pack_start(self.item_create_ap, True, True, 0) # Separador separator = Gtk.Separator() vbox.pack_start(separator, False, False, 10) # Botón Salir close_button = Gtk.Button(label='Cerrar') close_button.connect('clicked', self.quit) vbox.pack_start(close_button, False, False, 0) def create_ap(self, _): """Ejecuta el comando para abrir wihotspot-gui.""" try: subprocess.run(['/usr/bin/wihotspot-gui'], check=True) except subprocess.CalledProcessError: self.show_notification("Error al iniciar wihotspot-gui") def update_status_initial(self): """Inicializa el estado de la conexión WiFi al abrir la aplicación.""" try: wifi_state = self.get_wifi_state() ssid = self.get_connected_ssid() self.set_status(wifi_state, ssid) except dbus.exceptions.DBusException as e: print(f"Error al obtener el estado inicial: {e}", file=sys.stderr) def forget_network(self, _): """Función para olvidar una red guardada""" # Obtener la lista de redes WiFi guardadas de manera confiable saved_networks = subprocess.getoutput( "nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless$' | cut -d: -f1").splitlines() if not saved_networks: self.show_notification("No hay redes guardadas para olvidar.") return # Crear una ventana para seleccionar la red a olvidar window = Gtk.Window(title="Olvidar Red Guardada") window.set_default_size(300, 200) window.set_border_width(10) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) # Añadir botones para cada red guardada for network in saved_networks: button = Gtk.Button(label=network) button.connect("clicked", self.delete_network_profile, network) vbox.pack_start(button, True, True, 0) # Botón para cerrar la ventana close_button = Gtk.Button(label="Cerrar") close_button.connect("clicked", lambda x: window.destroy()) vbox.pack_start(close_button, False, False, 0) window.add(vbox) window.show_all() def delete_network_profile(self, button, network): """Elimina el perfil de conexión guardado para la red especificada""" try: subprocess.run( ['nmcli', 'connection', 'delete', network], check=True) self.show_notification(f"Red '{network}' olvidada exitosamente.") except subprocess.CalledProcessError: self.show_notification(f"Error al olvidar la red '{network}'.") def connect_wifi(self, _): """Función llamada al hacer clic en el botón 'Conectar WiFi'. Muestra la lista de redes y permite al usuario seleccionar una.""" self.show_network_list(None) def get_available_networks_list(self): """Obtiene la lista de redes WiFi junto con la intensidad de la señal y la banda (2.4 GHz o 5 GHz), sin duplicados y sin entradas vacías""" try: # Obtener SSID, señal y frecuencia output = subprocess.getoutput( "nmcli -t -f SSID,SIGNAL,FREQ dev wifi list") networks = {} for line in output.splitlines(): if line: # Separar el SSID, señal y frecuencia en tres variables parts = line.split(":") # Validación para evitar entradas vacías en SSID ssid = parts[0].strip() if not ssid: continue # Saltamos esta entrada si el SSID está vacío signal_strength = int(parts[1].strip()) if len( parts) > 1 and parts[1].isdigit() else 0 frequency = int(parts[2].strip().split()[0]) if len( parts) > 2 and parts[2].strip().split()[0].isdigit() else 0 # Determinar la banda en función de la frecuencia band = "5 GHz" if frequency >= 5000 else "2.4 GHz" # Evitar duplicados: guardar solo la señal más alta para cada SSID if ssid in networks: if signal_strength > networks[ssid][0]: networks[ssid] = (signal_strength, band) else: networks[ssid] = (signal_strength, band) # Convertir el diccionario a una lista de tuplas (SSID, señal, banda) return [(ssid, data[0], data[1]) for ssid, data in networks.items()] except Exception as e: print(f"Error al obtener la lista de redes disponibles: { e}", file=sys.stderr) return [] def show_network_list(self, _): # Llamar a la función para obtener la lista de redes con señal y banda networks = self.get_available_networks_list() if not networks: self.show_notification("No se encontraron redes disponibles.") return # Crear una ventana de lista de redes window = Gtk.Window(title="Redes Disponibles") window.set_default_size(300, 200) window.set_border_width(10) vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) # Añadir botones para cada red disponible con la intensidad de señal y la banda for network, signal_strength, band in networks: # Mostramos el nombre de la red junto con su intensidad de señal y la banda (2.4 GHz o 5 GHz) button_label = f"{network} ({signal_strength}%, {band})" button = Gtk.Button(label=button_label) button.connect( "clicked", self.prompt_password_and_connect, network) vbox.pack_start(button, True, True, 0) # Botón para cerrar la ventana close_button = Gtk.Button(label="Cerrar") close_button.connect("clicked", lambda x: window.destroy()) vbox.pack_start(close_button, False, False, 0) window.add(vbox) window.show_all() def prompt_password_and_connect(self, button, network): # Verificar si el perfil de la conexión ya existe existing_profile = subprocess.getoutput( f"nmcli -t -f NAME connection show | grep '^{network}$'") if existing_profile: # Si el perfil ya existe, intentar conectarse sin solicitar la contraseña self.show_notification(f"Conectando a {network}...") # None para indicar que no se requiere contraseña self.connect_to_network_with_nmcli(network, None) else: # Si no existe el perfil, solicitar la contraseña dialog = Gtk.Dialog( title=f"Conectar a {network}", transient_for=self, flags=0 ) dialog.add_buttons( Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK ) # Crear un campo de entrada de texto para la contraseña box = dialog.get_content_area() label = Gtk.Label(label="Introduce la contraseña:") box.add(label) password_entry = Gtk.Entry() # Ocultar la contraseña al escribir password_entry.set_visibility(False) box.add(password_entry) dialog.show_all() # Ejecutar el diálogo y obtener la respuesta del usuario response = dialog.run() if response == Gtk.ResponseType.OK: password = password_entry.get_text() dialog.destroy() # Intentar conectar a la red con nmcli y configuración de seguridad self.connect_to_network_with_nmcli(network, password) else: dialog.destroy() print("Conexión cancelada por el usuario.") def connect_to_network_with_nmcli(self, ssid, password): # Detectar la interfaz de red WiFi principal try: interface = subprocess.getoutput( "nmcli device status | grep wifi | grep -v 'p2p' | awk '{print $1}'").strip() if not interface: self.show_notification( "No se encontró una interfaz WiFi válida.") return except subprocess.CalledProcessError: self.show_notification("Error al obtener la interfaz de red.") return try: # Verificar si ya existe un perfil de conexión con el SSID existing_profile = subprocess.getoutput( f"nmcli -t -f NAME connection show | grep '^{ssid}$'") if existing_profile: # Si el perfil existe y se proporciona una contraseña, actualizarlo if password: subprocess.run([ 'nmcli', 'connection', 'modify', ssid, 'wifi-sec.key-mgmt', 'wpa-psk', 'wifi-sec.psk', password, 'connection.interface-name', interface ], check=True) self.show_notification(f"Perfil actualizado: {ssid}") else: # Si el perfil no existe, crearlo subprocess.run([ 'nmcli', 'connection', 'add', 'type', 'wifi', 'ifname', interface, 'con-name', ssid, 'ssid', ssid, 'wifi-sec.key-mgmt', 'wpa-psk', 'wifi-sec.psk', password ], check=True) self.show_notification(f"Perfil creado: {ssid}") # Intentar conectar usando el perfil existente o actualizado subprocess.run(['nmcli', 'connection', 'up', ssid], check=True) self.show_notification(f"Conectado a {ssid}") except subprocess.CalledProcessError: self.show_notification(f"Error al intentar conectar a {ssid}") def disconnect_wifi(self, _): """Función para desconectar la red WiFi actual.""" try: # Intentar desconectar la red activa subprocess.run(['nmcli', 'networking', 'off'], check=True) # Reactivar después de desconectar subprocess.run(['nmcli', 'networking', 'on'], check=True) self.show_notification("WiFi Desconectado.") except subprocess.CalledProcessError: self.show_notification("Error al desconectar WiFi.") def quit(self, _): Gtk.main_quit() def get_available_networks_list(self): """Obtiene la lista de redes WiFi junto con la intensidad de la señal y la banda (2.4 GHz o 5 GHz), sin duplicados y sin entradas vacías""" try: # Obtener SSID, señal y frecuencia output = subprocess.getoutput( "nmcli -t -f SSID,SIGNAL,FREQ dev wifi list") networks = {} for line in output.splitlines(): if line: # Separar el SSID, señal y frecuencia en tres variables parts = line.split(":") # Validación para evitar entradas vacías en SSID ssid = parts[0].strip() if not ssid: continue # Saltamos esta entrada si el SSID está vacío signal_strength = int(parts[1].strip()) if len( parts) > 1 and parts[1].isdigit() else 0 frequency = int(parts[2].strip().split()[0]) if len( parts) > 2 and parts[2].strip().split()[0].isdigit() else 0 # Determinar la banda en función de la frecuencia band = "5 GHz" if frequency >= 5000 else "2.4 GHz" # Evitar duplicados: guardar solo la señal más alta para cada SSID if ssid in networks: if signal_strength > networks[ssid][0]: networks[ssid] = (signal_strength, band) else: networks[ssid] = (signal_strength, band) # Convertir el diccionario a una lista de tuplas (SSID, señal, banda) return [(ssid, data[0], data[1]) for ssid, data in networks.items()] except Exception as e: print(f"Error al obtener la lista de redes disponibles: { e}", file=sys.stderr) return [] def show_notification(self, message): """Muestra una notificación en el sistema""" subprocess.run(['notify-send', message]) def on_properties_changed(self, interface, changed_properties, invalidated_properties): if 'State' in changed_properties: wifi_state = self.get_wifi_state() ssid = self.get_connected_ssid() self.set_status(wifi_state, ssid) def set_status(self, wifi_state, ssid): if wifi_state == 'disabled': status_text = "WiFi Apagado" # Deshabilitar el botón de Desconectar WiFi y habilitar Conectar WiFi self.item_disconnect.set_sensitive(False) self.item_connect.set_sensitive(True) elif wifi_state == 'enabled' and not ssid: status_text = "WiFi Encendido (No Conectado)" # Habilitar ambos botones self.item_disconnect.set_sensitive(True) self.item_connect.set_sensitive(True) elif wifi_state == 'enabled' and ssid: status_text = f"Conectado a: {ssid}" # Deshabilitar el botón de Conectar WiFi y habilitar Desconectar WiFi self.item_connect.set_sensitive(False) self.item_disconnect.set_sensitive(True) else: status_text = "WiFi Estado Desconocido" # Habilitar ambos botones por defecto self.item_connect.set_sensitive(True) self.item_disconnect.set_sensitive(True) print(f"Actualizando estado: '{status_text}'") self.status_label.set_text(f"Estado de la red: {status_text}") def get_wifi_state(self): try: output = subprocess.getoutput("nmcli radio wifi") print(f"Estado WiFi: {output}") return output.lower() except Exception as e: print(f"Error al obtener el estado de WiFi: {e}", file=sys.stderr) return "unknown" def get_connected_ssid(self): try: # Usar iwgetid para obtener el SSID conectado ssid = subprocess.getoutput("iwgetid -r").strip() if ssid: print(f"SSID conectado obtenido con iwgetid: {ssid}") return ssid else: # Fallback a nmcli si iwgetid no retorna SSID output = subprocess.getoutput( "nmcli -t -f ACTIVE,SSID dev wifi | grep '^yes' | cut -d':' -f2").strip() if output: print(f"SSID conectado obtenido con nmcli: {output}") return output else: print(f"SSID conectado obtenido: None") return None except Exception as e: print(f"Error al obtener SSID conectado: {e}", file=sys.stderr) return None def main(): # Para evitar múltiples instancias, usar una lock con D-Bus o similar # Aquí, simplemente se muestra la ventana window = NetworkMenu() window.connect("destroy", Gtk.main_quit) window.show_all() Gtk.main() if __name__ == "__main__": main()