networks: Overhaul Wi-Fi network scan page

Fixes: #1725.

- Show multiple Wi-Fi devices in separate tables so that users can pick them
understanding what they are. Also avoids some confusion related to why APs are
duplicated.

- Request scanning if the last scan time was long ago.

- Show the last scanned time.

- Refresh page in 10 seconds if scan has been requested so that the results of
scan can be shown without user explicitly refreshing the page. Show spinner when
scan has been requested and we are awaiting results.

- Refresh page every 60 seconds in other cases.

- When an SSID can't be decoded into a string, don't show it.

- Don't show hidden networks with no SSID set.

- Improve the styling for signal strength.

- Show a message when no Wi-Fi devices are present.

- Show a message when no Wi-Fi networks are found for a device.

Tests:

- Test on a machine with Wi-Fi device available.

- When page is loaded is for the first time, spinner is shown and refresh
happens in 10 seconds. After refresh if the scan has not completed, again,
spinner is shown and page is reloaded in 10 seconds. Otherwise, spinner is not
shown and page is reloaded in 60 seconds.

- Hidden networks are not shown.

- On a machine with no Wi-Fi devices, 'No Wi-Fi device detected.' message is
shown.

- Clicking on a network takes us to new Wi-Fi network connection page with
'connection name', 'network interface' and SSID filled in correctly.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
Sunil Mohan Adapa 2024-11-04 15:41:38 -08:00 committed by Veiko Aasa
parent a32a226492
commit 064f3c6c0c
No known key found for this signature in database
GPG Key ID: 478539CAE680674E
3 changed files with 120 additions and 37 deletions

View File

@ -10,28 +10,52 @@
<h3>{{ title }}</h3>
<div class="row">
<div class="col-md-6">
<div class="list-group list-group-two-column">
{% for access_point in access_points %}
<div class="list-group-item">
<span class="primary">
{% if access_point.ssid %}
<a href="{% url 'networks:add_wifi' access_point.ssid access_point.interface_name %}">
{{ access_point.ssid }}
</a>
{% else %}
--
{% endif %}
</span>
{% if not device_access_points %}
<p>{% trans "No Wi-Fi device detected." %}</p>
{% else %}
<div class="row">
<div class="col-md-6">
{% for device in device_access_points %}
{% if device_access_points|length > 1 %}
<h4>
{% blocktrans trimmed with interface_name=device.interface_name %}
Device: {{ interface_name }}
{% endblocktrans %}
</h4>
{% endif %}
<p>
{% trans "Last scanned: " %}
{% if device.last_scan_time %}
{{ device.last_scan_time|timesince }}
{% else %}
{% trans "never" %}
{% endif %}
{% if device.scan_requested %}
<span class="fa fa-spinner fa-spin"></span>
{% endif %}
</p>
<div class="list-group list-group-two-column">
{% for access_point in device.access_points %}
<div class="list-group-item">
<span class="primary">
<a href="{% url 'networks:add_wifi' access_point.ssid_string device.interface_name %}">
{{ access_point.ssid_string }}
</a>
</span>
<span class="btn btn-primary btn-sm secondary">
{{ access_point.strength }}%
</span>
<span class="badge badge-secondary secondary">
{{ access_point.strength }}%
</span>
</div>
{% empty %}
<p>
{% trans "No Wi-Fi networks found." %}
</p>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -385,11 +385,17 @@ def deactivate(request, uuid):
def scan(request):
"""Show a list of nearby visible Wi-Fi access points."""
access_points = network.wifi_scan()
return TemplateResponse(request, 'wifi_scan.html', {
'title': _('Nearby Wi-Fi Networks'),
'access_points': access_points
})
device_access_points = network.wifi_scan()
scanning = any(
(device['scan_requested'] for device in device_access_points))
# Refresh page in 10s if scanning, 60s otherwise
refresh_page_sec = 10 if scanning else 60
return TemplateResponse(
request, 'wifi_scan.html', {
'title': _('Nearby Wi-Fi Networks'),
'device_access_points': device_access_points,
'refresh_page_sec': refresh_page_sec
})
def add(request):

View File

@ -4,6 +4,7 @@ Helper functions for working with network manager.
"""
import collections
import datetime
import logging
import socket
import struct
@ -613,27 +614,79 @@ def delete_connection(connection_uuid):
return name
def _get_access_point_as_dict(access_point, active_ap_path):
"""Return Wi-Fi access point information as a dictionary."""
ssid = access_point.get_ssid()
if not ssid: # Hidden network
return None
try:
ssid_string = ssid.get_data().decode(encoding='utf-8')
except UnicodeError:
# XXX: Can't deal with binary SSIDs. Don't show SSIDs that are
# binary only.
return None
is_active = (active_ap_path == access_point.get_path())
return {
'ssid': ssid,
'ssid_string': ssid_string,
'strength': access_point.get_strength(),
'is_active': is_active
}
def wifi_scan():
"""Scan for available access points across all Wi-Fi devices."""
access_points = []
device_access_points = []
for device in get_nm_client().get_devices():
if device.get_device_type() != nm.DeviceType.WIFI:
continue
for access_point in device.get_access_points():
# Retrieve the bytes in SSID. Don't convert to utf-8 or
# escape it in any way as it may contain null bytes. When
# this is used in the URL it will be escaped properly and
# unescaped when taken as view function's argument.
ssid = access_point.get_ssid()
ssid_string = ssid.get_data().decode() if ssid else ''
access_points.append({
'interface_name': device.get_iface(),
'ssid': ssid_string,
'strength': access_point.get_strength()
})
if device.get_client() is None:
# The device got deleted (see NM:show-wifi-networks.py)
return False
return access_points
# Active access point
active_ap_path = device.get_active_access_point()
# Last scan time
last_scan = device.get_last_scan()
last_scan_time = None
scan_requested = False
if last_scan == -1:
last_scan = None
else:
boot_time = time.clock_gettime(time.CLOCK_BOOTTIME)
last_scan = boot_time - (last_scan / 1000)
last_scan_time = datetime.datetime.now() - datetime.timedelta(
seconds=last_scan)
# Request a scan if the last scan was more than 20 seconds ago
if (not last_scan) or last_scan > 20:
device.request_scan()
scan_requested = True
# Access points
access_points = []
for access_point in device.get_access_points():
ap_dict = _get_access_point_as_dict(access_point, active_ap_path)
if ap_dict:
access_points.append(ap_dict)
access_points = sorted(access_points,
key=lambda point: -point['strength'])
device_access_points.append({
'interface_name': device.get_iface(),
'access_points': access_points,
'last_scan': last_scan,
'last_scan_time': last_scan_time,
'scan_requested': scan_requested
})
return sorted(device_access_points,
key=lambda device: device['interface_name'])
def refeed_dns():