diff --git a/openwisp_monitoring/device/admin.py b/openwisp_monitoring/device/admin.py index 2dbd54ae0..29e202d46 100644 --- a/openwisp_monitoring/device/admin.py +++ b/openwisp_monitoring/device/admin.py @@ -352,15 +352,20 @@ def _get_boolean_html(self, value): icon = static(f'admin/img/icon-{icon_type}.svg') return mark_safe(f'') - def ht(self, obj): - return self._get_boolean_html(obj.wifi_client.ht) + def he(self, obj): + return self._get_boolean_html(obj.wifi_client.he) - ht.short_description = 'HT' + he.short_description = 'WiFi 6 (802.11ax)' def vht(self, obj): return self._get_boolean_html(obj.wifi_client.vht) - vht.short_description = 'VHT' + vht.short_description = 'WiFi 5 (802.11ac)' + + def ht(self, obj): + return self._get_boolean_html(obj.wifi_client.ht) + + ht.short_description = 'WiFi 4 (802.11n)' def wmm(self, obj): return self._get_boolean_html(obj.wifi_client.wmm) @@ -409,8 +414,9 @@ class WiFiSessionInline(WifiSessionAdminHelperMixin, admin.TabularInline): 'vendor', 'ssid', 'interface_name', - 'ht', + 'he', 'vht', + 'ht', 'start_time', 'get_stop_time', ] @@ -463,8 +469,9 @@ class WifiSessionAdmin( 'related_organization', 'related_device', 'ssid', - 'ht', + 'he', 'vht', + 'ht', 'start_time', 'get_stop_time', ] @@ -475,8 +482,9 @@ class WifiSessionAdmin( 'related_device', 'ssid', 'interface_name', - 'ht', + 'he', 'vht', + 'ht', 'wmm', 'wds', 'wps', @@ -499,8 +507,9 @@ def get_readonly_fields(self, request, obj=None): 'related_organization', 'mac_address', 'vendor', - 'ht', + 'he', 'vht', + 'ht', 'wmm', 'wds', 'wps', diff --git a/openwisp_monitoring/device/base/models.py b/openwisp_monitoring/device/base/models.py index 65a63362c..2fc564c03 100644 --- a/openwisp_monitoring/device/base/models.py +++ b/openwisp_monitoring/device/base/models.py @@ -183,27 +183,30 @@ def _transform_data(self): for signal_key, signal_values in interface['mobile']['signal'].items(): for key, value in signal_values.items(): signal_values[key] = float(value) - # If HT/VHT is not being used ie. htmode = 'NOHT', - # set the HT/VHT field of WiFi clients to None. + # If HT/VHT/HE is not being used ie. htmode = 'NOHT', + # set the HT/VHT/HE field of WiFi clients to None. # This is necessary because some clients may be # VHT capable but VHT is not enabled at the radio level, - # which can mislead into thinking the client is not HT/VHT capable. - if ( - 'wireless' in interface - and 'htmode' in interface['wireless'] - and 'clients' in interface['wireless'] - ): - for client in interface['wireless']['clients']: - # NOHT : disables 11n (ie. ht and vht both are False) - if interface['wireless']['htmode'] == 'NOHT': - client['ht'] = None - client['vht'] = None - # If only HT is enabled, set client vht to None - elif ( - interface['wireless']['htmode'].startswith('HT') - and not client['vht'] - ): - client['vht'] = None + # which can mislead into thinking the client is not HT/VHT/HE capable. + wireless = interface.get('wireless') + if wireless and all(key in wireless for key in ('htmode', 'clients')): + for client in wireless['clients']: + htmode = wireless['htmode'] + ht_enabled = htmode.startswith('HT') + vht_enabled = htmode.startswith('VHT') + noht_enabled = htmode == 'NOHT' + if noht_enabled: + client['ht'] = client['vht'] = None + # since 'he' field is optional + if 'he' in client: + client['he'] = None + elif ht_enabled: + if client['vht'] is False: + client['vht'] = None + if client.get('he') is False: + client['he'] = None + elif vht_enabled and client.get('he') is False: + client['he'] = None # add mac vendor to wireless clients if present if ( not mac_detection @@ -371,8 +374,9 @@ class AbstractWifiClient(TimeStampedEditableModel): help_text=_('MAC address'), ) vendor = models.CharField(max_length=200, blank=True, null=True) - ht = models.BooleanField(null=True, blank=True, default=None, verbose_name='HT') + he = models.BooleanField(null=True, blank=True, default=None, verbose_name='HE') vht = models.BooleanField(null=True, blank=True, default=None, verbose_name='VHT') + ht = models.BooleanField(null=True, blank=True, default=None, verbose_name='HT') wmm = models.BooleanField(default=False, verbose_name='WMM') wds = models.BooleanField(default=False, verbose_name='WDS') wps = models.BooleanField(default=False, verbose_name='WPS') diff --git a/openwisp_monitoring/device/migrations/0007_add_wificlient_field_he.py b/openwisp_monitoring/device/migrations/0007_add_wificlient_field_he.py new file mode 100644 index 000000000..3ed461f5d --- /dev/null +++ b/openwisp_monitoring/device/migrations/0007_add_wificlient_field_he.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.18 on 2023-04-20 11:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('device_monitoring', '0006_alter_wificlient_field_ht_vht'), + ] + + operations = [ + migrations.AddField( + model_name='wificlient', + name='he', + field=models.BooleanField( + blank=True, default=None, null=True, verbose_name='HE' + ), + ), + ] diff --git a/openwisp_monitoring/device/schema.py b/openwisp_monitoring/device/schema.py index 4f411d56e..7dca317a7 100644 --- a/openwisp_monitoring/device/schema.py +++ b/openwisp_monitoring/device/schema.py @@ -236,6 +236,7 @@ "authorized": {"type": "boolean"}, "ht": {"type": "boolean"}, "vht": {"type": "boolean"}, + "he": {"type": "boolean"}, "wds": {"type": "boolean"}, "wmm": {"type": "boolean"}, "wps": {"type": "boolean"}, diff --git a/openwisp_monitoring/device/tasks.py b/openwisp_monitoring/device/tasks.py index 9355ea92e..aabaf38ab 100644 --- a/openwisp_monitoring/device/tasks.py +++ b/openwisp_monitoring/device/tasks.py @@ -38,7 +38,7 @@ def trigger_device_checks(pk, recovery=True): @shared_task(base=OpenwispCeleryTask) def save_wifi_clients_and_sessions(device_data, device_pk): - _WIFICLIENT_FIELDS = ['vendor', 'ht', 'vht', 'wmm', 'wds', 'wps'] + _WIFICLIENT_FIELDS = ['vendor', 'ht', 'vht', 'he', 'wmm', 'wds', 'wps'] WifiClient = load_model('device_monitoring', 'WifiClient') WifiSession = load_model('device_monitoring', 'WifiSession') diff --git a/openwisp_monitoring/device/templates/admin/config/device/change_form.html b/openwisp_monitoring/device/templates/admin/config/device/change_form.html index 13ff88de8..027af7ab4 100644 --- a/openwisp_monitoring/device/templates/admin/config/device/change_form.html +++ b/openwisp_monitoring/device/templates/admin/config/device/change_form.html @@ -401,11 +401,14 @@

{% trans 'Interface status' %}: {{ interface.name }}

{% trans 'Vendor' %} {% endif %} - - HT + + WiFi 6 (802.11ax) - VHT + WiFi 5 (802.11ac) + + + WiFi 4 (802.11n) WMM @@ -426,11 +429,14 @@

{% trans 'Interface status' %}: {{ interface.name }}

{{ client.vendor }} {% endif %} - - + + - + + + + diff --git a/openwisp_monitoring/device/tests/__init__.py b/openwisp_monitoring/device/tests/__init__.py index 4fbae8b28..807f6bc9b 100644 --- a/openwisp_monitoring/device/tests/__init__.py +++ b/openwisp_monitoring/device/tests/__init__.py @@ -53,30 +53,26 @@ def _create_device_monitoring(self): return dm def _transform_wireless_interface_test_data(self, data): - interfaces = data.get('interfaces', []) - wireless_interfaces = [ - interface - for interface in interfaces - if 'wireless' in interface - and 'htmode' in interface['wireless'] - and 'clients' in interface['wireless'] - ] - for interface in wireless_interfaces: - for client in interface['wireless']['clients']: - if ( - interface['wireless']['htmode'] == 'NOHT' - and not client['ht'] - and not client['vht'] - ): - client['ht'] = None - client['vht'] = None - elif ( - interface['wireless']['htmode'].startswith('HT') - and client['ht'] - and not client['vht'] - ): - client['vht'] = None - + for interface in data.get('interfaces', []): + wireless = interface.get('wireless') + if wireless and all(key in wireless for key in ('htmode', 'clients')): + for client in wireless['clients']: + htmode = wireless['htmode'] + ht_enabled = htmode.startswith('HT') + vht_enabled = htmode.startswith('VHT') + noht_enabled = htmode == 'NOHT' + if noht_enabled: + client['ht'] = client['vht'] = None + # since 'he' field is optional + if 'he' in client: + client['he'] = None + elif ht_enabled: + if client['vht'] is False: + client['vht'] = None + if client.get('he') is False: + client['he'] = None + elif vht_enabled and client.get('he') is False: + client['he'] = None return data def assertDataDict(self, dd_data, data): @@ -247,6 +243,7 @@ def _data(self): 'assoc': True, 'authorized': True, 'vht': False, + 'he': False, 'wmm': True, 'aid': 1, 'mfp': False, diff --git a/openwisp_monitoring/device/tests/test_admin.py b/openwisp_monitoring/device/tests/test_admin.py index 27e7fc3e4..e187d0db5 100644 --- a/openwisp_monitoring/device/tests/test_admin.py +++ b/openwisp_monitoring/device/tests/test_admin.py @@ -174,44 +174,78 @@ def test_status_data_contains_wifi_version(self): html=True, ) - def test_status_data_contains_wifi_client_ht_vht_unknown(self): + def test_status_data_contains_wifi_client_he_vht_ht_unknown(self): data = deepcopy(self._data()) d = self._create_device(organization=self._create_org()) url = reverse('admin:config_device_change', args=[d.pk]) wireless_interface = data['interfaces'][0]['wireless'] client = data['interfaces'][0]['wireless']['clients'][0] - with self.subTest('Test when HT is disabled'): + with self.subTest('Test when htmode is NOHT'): wireless_interface.update({'htmode': 'NOHT'}) self._post_data(d.id, d.key, data) response = self.client.get(url) + # make sure 'he', 'vht' and 'ht' are set to None self.assertContains( response, """ - + + + + """, html=True, ) - with self.subTest('Test when HT is enabled'): + with self.subTest('Test when htmode is HT and client he and vht are False'): wireless_interface.update({'htmode': 'HT40'}) + # both 'he' and 'vht' are False + self.assertEqual(client['he'], False) self.assertEqual(client['vht'], False) self._post_data(d.id, d.key, data) response = self.client.get(url) + # make sure 'he' and 'vht' are set to None self.assertContains( response, """ + + + + + + - + """, + html=True, + ) + + with self.subTest('Test when htmode is VHT and client he is False'): + wireless_interface.update({'htmode': 'VHT80'}) + client.update({'vht': True}) + self.assertEqual(client['he'], False) + self.assertEqual(client['vht'], True) + self._post_data(d.id, d.key, data) + response = self.client.get(url) + # make sure only 'he' is set to None + self.assertContains( + response, + """ + + + + + + + """, html=True, ) @@ -902,8 +936,8 @@ def test_dashboard_wifi_session_chart(self): {'labels': [], 'values': []}, ) - def test_wifi_client_ht_vht_unknown(self): - test_wifi_client = self._create_wifi_client(ht=None, vht=None) + def test_wifi_client_he_vht_ht_unknown(self): + test_wifi_client = self._create_wifi_client(he=None, vht=None, ht=None) test_wifi_session = self._create_wifi_session(wifi_client=test_wifi_client) device = Device.objects.all().first() @@ -913,12 +947,15 @@ def test_wifi_client_ht_vht_unknown(self): self.assertContains( response, """ - +

+ +

+ """, html=True, ) @@ -930,12 +967,15 @@ def test_wifi_client_ht_vht_unknown(self): self.assertContains( response, """ - + + + + """, html=True, ) @@ -948,9 +988,9 @@ def test_wifi_client_ht_vht_unknown(self): self.assertContains( response, """ -
+
- +
@@ -958,7 +998,15 @@ def test_wifi_client_ht_vht_unknown(self):
- + +
+ +
+
+
+
+
+
diff --git a/openwisp_monitoring/device/tests/test_models.py b/openwisp_monitoring/device/tests/test_models.py index 3b5af8f30..b2c93e655 100644 --- a/openwisp_monitoring/device/tests/test_models.py +++ b/openwisp_monitoring/device/tests/test_models.py @@ -734,24 +734,27 @@ def test_wifi_client_session_created(self): self.assertEqual(WifiSession.objects.count(), 3) wifi_client1 = WifiClient.objects.get(mac_address='00:ee:ad:34:f5:3b') self.assertEqual(wifi_client1.vendor, None) - self.assertEqual(wifi_client1.ht, True) + self.assertEqual(wifi_client1.he, None) self.assertEqual(wifi_client1.vht, None) + self.assertEqual(wifi_client1.ht, True) self.assertEqual(wifi_client1.wmm, True) self.assertEqual(wifi_client1.wds, False) self.assertEqual(wifi_client1.wps, False) wifi_client2 = WifiClient.objects.get(mac_address='b0:e1:7e:30:16:44') self.assertEqual(wifi_client2.vendor, None) - self.assertEqual(wifi_client2.ht, True) + self.assertEqual(wifi_client2.he, None) self.assertEqual(wifi_client2.vht, False) + self.assertEqual(wifi_client2.ht, True) self.assertEqual(wifi_client2.wmm, True) self.assertEqual(wifi_client2.wds, False) self.assertEqual(wifi_client2.wps, False) wifi_client3 = WifiClient.objects.get(mac_address='c0:ee:fb:34:f5:4b') self.assertEqual(wifi_client3.vendor, None) - self.assertEqual(wifi_client3.ht, True) + self.assertEqual(wifi_client3.he, None) self.assertEqual(wifi_client3.vht, False) + self.assertEqual(wifi_client3.ht, True) self.assertEqual(wifi_client3.wmm, True) self.assertEqual(wifi_client3.wds, False) self.assertEqual(wifi_client3.wps, False) diff --git a/requirements.txt b/requirements.txt index 10a2d9e97..5056f06a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ django-nested-admin~=3.4.0 netaddr~=0.8 python-dateutil>=2.7,<3.0 openwisp-utils[rest] @ https://github.com/openwisp/openwisp-utils/tarball/master +urllib3~=1.26.1 diff --git a/tests/openwisp2/sample_device_monitoring/migrations/0001_initial.py b/tests/openwisp2/sample_device_monitoring/migrations/0001_initial.py index 8ef42da19..68552bafb 100644 --- a/tests/openwisp2/sample_device_monitoring/migrations/0001_initial.py +++ b/tests/openwisp2/sample_device_monitoring/migrations/0001_initial.py @@ -140,6 +140,12 @@ class Migration(migrations.Migration): blank=True, default=None, null=True, verbose_name='VHT' ), ), + ( + 'he', + models.BooleanField( + blank=True, default=None, null=True, verbose_name='HE' + ), + ), ('wmm', models.BooleanField(default=False, verbose_name='WMM')), ('wds', models.BooleanField(default=False, verbose_name='WDS')), ('wps', models.BooleanField(default=False, verbose_name='WPS')),