441 lines
18 KiB
Python
Executable File
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()
|