From 064f3c6c0cd6f92b5f98dc2c482fe282bf17bf45 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Mon, 4 Nov 2024 15:41:38 -0800 Subject: [PATCH] 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 --- .../modules/networks/templates/wifi_scan.html | 60 +++++++++----- plinth/modules/networks/views.py | 16 ++-- plinth/network.py | 81 +++++++++++++++---- 3 files changed, 120 insertions(+), 37 deletions(-) diff --git a/plinth/modules/networks/templates/wifi_scan.html b/plinth/modules/networks/templates/wifi_scan.html index c7011e363..4fd667f2d 100644 --- a/plinth/modules/networks/templates/wifi_scan.html +++ b/plinth/modules/networks/templates/wifi_scan.html @@ -10,28 +10,52 @@

{{ title }}

-
-
-
- {% for access_point in access_points %} -
- - {% if access_point.ssid %} - - {{ access_point.ssid }} - - {% else %} - -- - {% endif %} - + {% if not device_access_points %} +

{% trans "No Wi-Fi device detected." %}

+ {% else %} +
+
+ {% for device in device_access_points %} + {% if device_access_points|length > 1 %} +

+ {% blocktrans trimmed with interface_name=device.interface_name %} + Device: {{ interface_name }} + {% endblocktrans %} +

+ {% endif %} +

+ {% trans "Last scanned: " %} + {% if device.last_scan_time %} + {{ device.last_scan_time|timesince }} + {% else %} + {% trans "never" %} + {% endif %} + {% if device.scan_requested %} + + {% endif %} +

+
+ {% for access_point in device.access_points %} +
+ + + {{ access_point.ssid_string }} + + - - {{ access_point.strength }}% - + + {{ access_point.strength }}% + +
+ {% empty %} +

+ {% trans "No Wi-Fi networks found." %} +

+ {% endfor %}
{% endfor %}
-
+ {% endif %} {% endblock %} diff --git a/plinth/modules/networks/views.py b/plinth/modules/networks/views.py index 7ebc68700..9d92442d5 100644 --- a/plinth/modules/networks/views.py +++ b/plinth/modules/networks/views.py @@ -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): diff --git a/plinth/network.py b/plinth/network.py index bbc5f5832..dfe99e466 100644 --- a/plinth/network.py +++ b/plinth/network.py @@ -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():