Sideband/sbapp/plyer/platforms/linux/wifi.py
2024-06-02 18:31:58 +02:00

483 lines
13 KiB
Python

'''
.. note::
This facade depends on `nmcli` (Network Manager command line tool).
It's found in most of the popular GNU/Linux distributions. Support for other
backends is not provided yet.
'''
from subprocess import Popen, PIPE, call
from sbapp.plyer.facades import Wifi
from sbapp.plyer.utils import whereis_exe, deprecated
try:
import wifi
except ModuleNotFoundError as err:
raise ModuleNotFoundError(
"python-wifi not installed. try:" +
"`pip install --user wifi`.") from err
class NMCLIWifi(Wifi):
'''
.. versionadded:: 1.4.0
'''
def __init__(self, *args, **kwargs):
'''
.. versionadded:: 1.4.0
'''
super().__init__(*args, **kwargs)
self.names = {}
@property
def interfaces(self):
'''
Get all the available interfaces for WiFi.
.. versionadded:: 1.4.0
Tested with nmcli 1.2.6.
'''
if not self._is_enabled():
self._enable()
# fetch the devices
proc = Popen([
'nmcli', '--terse',
'--fields', 'DEVICE,TYPE',
'device'
], stdout=PIPE)
lines = proc.communicate()[0].decode('utf-8').splitlines()
# filter devices by type
interfaces = []
for line in lines:
# bad escape from nmcli's side :<
line = line.replace('\\:', '$$')
device, dtype = line.split(':')
if dtype != 'wifi':
continue
interfaces.append(device.replace('$$', ':'))
# return wifi interfaces
return interfaces
def _is_enabled(self):
'''
Return the status of WiFi device.
.. versionadded:: 1.4.0
Tested with nmcli 1.2.6.
'''
output = Popen(
["nmcli", "radio", "wifi"],
stdout=PIPE
).communicate()[0].decode('utf-8')
if output.split()[0] == 'enabled':
return True
return False
def _is_connected(self, interface=None):
'''
Return whether a specified interface is connected to a WiFi network.
.. versionadded:: 1.4.0
Tested with nmcli 1.2.6.
'''
if not self._is_enabled():
self._enable()
if not interface:
interface = self.interfaces[0]
# fetch all devices
proc = Popen([
'nmcli', '--terse',
'--fields', 'DEVICE,TYPE,STATE',
'device'
], stdout=PIPE)
lines = proc.communicate()[0].decode('utf-8').splitlines()
# filter by wifi type and interface
connected = False
for line in lines:
line = line.replace('\\:', '$$')
device, dtype, state = line.split(':')
device = device.replace('$$', ':')
if dtype != 'wifi':
continue
if device != interface:
continue
if state == 'connected':
connected = True
return connected
def _start_scanning(self, interface=None):
'''
Start scanning for available Wi-Fi networks
for the specified interface.
.. versionadded:: 1.4.0
Tested with nmcli 1.2.6.
'''
if not self._is_enabled():
self._enable()
if not interface:
interface = self.interfaces[0]
# force rescan for fresh data
call(['nmcli', 'device', 'wifi', 'rescan', 'ifname', interface])
# get properties
fields = [
'SSID', 'BSSID', 'MODE', 'CHAN', 'FREQ',
'BARS', 'RATE', 'SIGNAL', 'SECURITY'
]
# fetch all networks for interface
output = Popen([
'nmcli', '--terse',
'--fields', ','.join(fields),
'device', 'wifi', 'list', 'ifname', interface
], stdout=PIPE).communicate()[0].decode('utf-8')
# parse output
for line in output.splitlines():
line = line.replace('\\:', '$$')
row = {
field: value
for field, value in zip(fields, line.split(':'))
}
row['BSSID'] = row['BSSID'].replace('$$', ':')
self.names[row['SSID']] = row
def _get_network_info(self, name):
'''
Get all the network information by network's name (SSID).
.. versionadded:: 1.4.0
Tested with nmcli 1.2.6.
'''
if not self.names:
self._start_scanning()
ret_list = {}
ret_list['ssid'] = self.names[name]['SSID']
ret_list['signal'] = self.names[name]['SIGNAL']
bars = len(self.names[name]['BARS'])
ret_list['quality'] = '{}/100'.format(bars / 5.0 * 100)
ret_list['frequency'] = self.names[name]['FREQ']
ret_list['bitrates'] = self.names[name]['RATE']
# wpa1, wpa2, wpa1 wpa2, wep, (none), perhaps something else
security = self.names[name]['SECURITY'].lower()
ret_list['encrypted'] = True
if 'wpa2' in security:
# wpa2, wpa2+wpa1
ret_list['encryption_type'] = 'wpa2'
elif 'wpa' in security:
ret_list['encryption_type'] = 'wpa'
elif 'wep' in security:
ret_list['encryption_type'] = 'wep'
elif 'none' in security:
ret_list['encrypted'] = False
ret_list['encryption_type'] = 'none'
else:
ret_list['encryption_type'] = security
ret_list['channel'] = int(self.names[name]['CHAN'])
ret_list['address'] = self.names[name]['BSSID']
ret_list['mode'] = self.names[name]['MODE']
return ret_list
def _get_available_wifi(self):
'''
Return the names of all found networks.
.. versionadded:: 1.4.0
Tested with nmcli 1.2.6.
'''
if not self.names:
self._start_scanning()
return list(self.names.keys())
def _connect(self, network, parameters, interface=None):
'''
Connect a specific interface to a WiFi network.
Expects 2 parameters:
- SSID of the network
- parameters: dict
- password: string or None
.. versionadded:: 1.4.0
Tested with nmcli 1.2.6.
'''
self._enable()
if not interface:
interface = self.interfaces[0]
password = parameters.get('password')
command = [
'nmcli', 'device', 'wifi', 'connect', network,
'ifname', interface
]
if password:
command += ['password', password]
call(command)
def _disconnect(self, interface=None):
'''
Disconnect a specific interface from a WiFi network.
.. versionadded:: 1.4.0
Tested with nmcli 1.2.6.
'''
if not self._is_enabled():
return
if not interface:
interface = self.interfaces[0]
if self._nmcli_version() >= (1, 2, 6):
call(['nmcli', 'device', 'disconnect', interface])
else:
call(['nmcli', 'nm', 'enable', 'false'])
def _enable(self):
'''
Turn WiFi device on.
.. versionadded:: 1.4.0
Tested with nmcli 1.2.6.
'''
call(['nmcli', 'radio', 'wifi', 'on'])
def _disable(self):
'''
Turn WiFi device off.
.. versionadded:: 1.4.0
Tested with nmcli 1.2.6.
'''
call(['nmcli', 'radio', 'wifi', 'off'])
def _nmcli_version(self):
'''
Get nmcli version to prevent executing deprecated commands.
.. versionadded:: 1.4.0
Tested with nmcli 1.2.6.
'''
version = Popen(['nmcli', '-v'], stdout=PIPE)
version = version.communicate()[0].decode('utf-8')
while version and not version[0].isdigit():
version = version[1:]
return tuple(map(int, (version.split('.'))))
@deprecated
class LinuxWifi(Wifi):
'''
.. versionadded:: 1.2.5
'''
def __init__(self, *args, **kwargs):
'''
.. versionadded:: 1.4.0
'''
super().__init__(*args, **kwargs)
self.names = {}
@property
def interfaces(self):
'''
.. versionadded:: 1.4.0
'''
proc = Popen([
'nmcli', '--terse',
'--fields', 'DEVICE,TYPE',
'device'
], stdout=PIPE)
lines = proc.communicate()[0].decode('utf-8').splitlines()
interfaces = []
for line in lines:
device, dtype = line.split(':')
if dtype != 'wifi':
continue
interfaces.append(device)
return interfaces
def _is_enabled(self):
'''
Returns `True` if wifi is enabled else `False`.
.. versionadded:: 1.2.5
.. versionchanged:: 1.3.2
nmcli output is properly decoded to unicode
'''
enbl = Popen(["nmcli", "radio", "wifi"], stdout=PIPE, stderr=PIPE)
if enbl.communicate()[0].split()[0].decode('utf-8') == "enabled":
return True
return False
def _is_connected(self, interface=None):
'''
.. versionadded:: 1.4.0
'''
if not interface:
interface = self.interfaces[0]
proc = Popen([
'nmcli', '--terse',
'--fields', 'DEVICE,TYPE,STATE',
'device'
], stdout=PIPE)
lines = proc.communicate()[0].decode('utf-8').splitlines()
connected = False
for line in lines:
device, dtype, state = line.split(':')
if dtype != 'wifi':
continue
if device != interface:
continue
if state == 'connected':
connected = True
return connected
def _start_scanning(self, interface=None):
'''
Returns all the network information.
.. versionadded:: 1.2.5
.. versionchanged:: 1.3.0
scan only if wifi is enabled
'''
if not interface:
interface = self.interfaces[0]
if self._is_enabled():
list_ = list(wifi.Cell.all(interface))
for i in range(len(list_)):
self.names[list_[i].ssid] = list_[i]
else:
raise Exception('Wifi not enabled.')
def _get_network_info(self, name):
'''
Starts scanning for available Wi-Fi networks and returns the available,
devices.
.. versionadded:: 1.2.5
'''
ret_list = {}
ret_list['ssid'] = self.names[name].ssid
ret_list['signal'] = self.names[name].signal
ret_list['quality'] = self.names[name].quality
ret_list['frequency'] = self.names[name].frequency
ret_list['bitrates'] = self.names[name].bitrates
ret_list['encrypted'] = self.names[name].encrypted
ret_list['channel'] = self.names[name].channel
ret_list['address'] = self.names[name].address
ret_list['mode'] = self.names[name].mode
if not ret_list['encrypted']:
return ret_list
else:
ret_list['encryption_type'] = self.names[name].encryption_type
return ret_list
def _get_available_wifi(self):
'''
Returns the name of available networks.
.. versionadded:: 1.2.5
.. versionchanged:: 1.4.0
return a proper list of elements instead of dict_keys
'''
return list(self.names.keys())
def _connect(self, network, parameters, interface=None):
'''
Expects 2 parameters:
- name/ssid of the network.
- parameters: dict type
- password: string or None
.. versionadded:: 1.2.5
'''
if not interface:
interface = self.interfaces[0]
result = None
try:
self._enable()
finally:
password = parameters['password']
cell = self.names[network]
result = wifi.Scheme.for_cell(
interface, network, cell, password
)
return result
def _disconnect(self, interface=None):
'''
Disconnect all the networks managed by Network manager.
.. versionadded:: 1.2.5
'''
if not interface:
interface = self.interfaces[0]
if self._nmcli_version() >= (1, 2, 6):
call(['nmcli', 'dev', 'disconnect', interface])
else:
call(['nmcli', 'nm', 'enable', 'false'])
def _enable(self):
'''
Wifi interface power state is set to "ON".
.. versionadded:: 1.3.2
'''
return call(['nmcli', 'radio', 'wifi', 'on'])
def _disable(self):
'''
Wifi interface power state is set to "OFF".
.. versionadded:: 1.3.2
'''
return call(['nmcli', 'radio', 'wifi', 'off'])
def _nmcli_version(self):
'''
.. versionadded:: 1.3.2
'''
version = Popen(['nmcli', '-v'], stdout=PIPE)
version = version.communicate()[0].decode('utf-8')
while version and not version[0].isdigit():
version = version[1:]
return tuple(map(int, (version.split('.'))))
def instance():
if whereis_exe('nmcli'):
return NMCLIWifi()
return LinuxWifi()