2025-05-28 18:27:10 +02:00

441 lines
18 KiB
Python
Executable File

#!/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()