mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 04:33:56 +08:00
453 lines
14 KiB
JavaScript
453 lines
14 KiB
JavaScript
'use strict';
|
|
'require baseclass';
|
|
'require dom';
|
|
'require network';
|
|
'require rpc';
|
|
'require uci';
|
|
'require view.dashboard.lib.charts as charts';
|
|
|
|
/* -67 dBm is the minimum RSSI Cisco specifies for voice-grade coverage;
|
|
below it the client is still usable, just not at that level. */
|
|
const GRADES = [
|
|
{ min: -67, className: 'dashboard-bar-ok', kind: 'success' },
|
|
{ min: -Infinity, className: 'dashboard-bar-mid', kind: 'warning' }
|
|
];
|
|
|
|
const NO_SIGNAL = { className: 'dashboard-bar-none', kind: '' };
|
|
|
|
function signalGrade(rssi) {
|
|
return (rssi != null) ? GRADES.find(grade => rssi >= grade.min) : NO_SIGNAL;
|
|
}
|
|
|
|
function rssiOf(device) {
|
|
return device.signal.value.rssi ?? -Infinity;
|
|
}
|
|
|
|
return baseclass.extend({
|
|
|
|
title: _('Wireless'),
|
|
|
|
params: [],
|
|
|
|
widgets: [
|
|
{ id: 'wifi', slot: 'cards', title: _('Wireless clients'), order: 60, hidden: true },
|
|
{ id: 'distribution', slot: 'charts', title: _('Wireless client distribution'), order: 30, hidden: true },
|
|
{ id: 'signal', slot: 'charts', title: _('Signal Strength'), order: 40, hidden: true },
|
|
{ id: 'traffic', slot: 'charts', title: _('Wireless client data usage'), order: 50, hidden: true },
|
|
{ id: 'wifi', slot: 'tabs', title: _('Wireless'), order: 50, hidden: true }
|
|
],
|
|
|
|
// The "wifi" system feature only says /sbin/wifi exists.
|
|
available() {
|
|
return uci.load('wireless').then(() => uci.sections('wireless', 'wifi-device').length > 0);
|
|
},
|
|
|
|
load() {
|
|
return Promise.all([
|
|
network.getWifiDevices(),
|
|
network.getWifiNetworks(),
|
|
network.getHostHints()
|
|
]).then(radios_networks_hints => {
|
|
const tasks = [];
|
|
|
|
for (let i = 0; i < radios_networks_hints[1].length; i++)
|
|
tasks.push(L.resolveDefault(radios_networks_hints[1][i].getAssocList(), []).then(L.bind((net, list) => {
|
|
net.assoclist = list.sort((a, b) => a.mac.localeCompare(b.mac));
|
|
}, this, radios_networks_hints[1][i])));
|
|
|
|
return Promise.all(tasks).then(() => {
|
|
return radios_networks_hints;
|
|
});
|
|
});
|
|
},
|
|
|
|
renderKpi() {
|
|
const radios = this.params.wifi.radios;
|
|
const active = radios.filter(radio => radio.isactive.value === true).length;
|
|
|
|
return charts.kpi({
|
|
icon: 'wireless',
|
|
title: _('Wireless clients'),
|
|
value: [ String(this.params.wifi.devices.length) ],
|
|
sub: [ _('%d of %d SSIDs active').format(active, radios.length) ]
|
|
});
|
|
},
|
|
|
|
renderDistributionChart() {
|
|
const devices = this.params.wifi.devices;
|
|
const series = this.params.wifi.radios.map(radio => ({
|
|
label: [ radio.ssid.value, ' ', E('small', {}, [ radio.chan.value ]) ],
|
|
value: (radio.isactive.value === true) ? (parseInt(radio.associations.value) || 0) : 0
|
|
}));
|
|
|
|
return charts.card({
|
|
title: _('Wireless client distribution'),
|
|
desc: _('by SSID'),
|
|
body: devices.length
|
|
? charts.donut({ series: series, centerLabel: _('clients'), ariaLabel: _('Wireless client distribution') })
|
|
: charts.empty(_('No wireless clients connected'))
|
|
});
|
|
},
|
|
|
|
renderSignalChart() {
|
|
const devices = this.params.wifi.devices.slice().sort((a, b) => rssiOf(b) - rssiOf(a));
|
|
|
|
return charts.card({
|
|
title: _('Signal Strength'),
|
|
desc: devices.length ? '%s · %s'.format(_('%d clients').format(devices.length), _('dBm')) : '',
|
|
body: devices.length ? [ charts.barChart({
|
|
id: 'signal',
|
|
// iwinfo's nl80211 backend clamps the signal to -110..-40 dBm
|
|
// (the cfg80211 wext compat range) before deriving quality.
|
|
min: -110,
|
|
max: -40,
|
|
ticks: [ -110, -100, -90, -80, -70, -60, -50, -40 ].map(v => ({ value: v, label: '%d'.format(v) })),
|
|
items: devices.map(device => ({
|
|
label: device.hostname.value,
|
|
title: [ device.hostname.value, device.ssid.value, this.signalText(device) ].join(' · '),
|
|
values: [ { value: device.signal.value.rssi, className: signalGrade(device.signal.value.rssi).className } ]
|
|
}))
|
|
}), charts.legend([
|
|
{ className: 'dashboard-bar-ok', label: '%s ≥ -67 %s'.format(_('Good'), _('dBm')) },
|
|
{ className: 'dashboard-bar-mid', label: '%s < -67 %s'.format(_('Weak'), _('dBm')) }
|
|
]) ] : charts.empty(_('No wireless clients connected'))
|
|
});
|
|
},
|
|
|
|
signalText(device) {
|
|
if (device.signal.value.rssi == null)
|
|
return _('No RX signal');
|
|
|
|
const rssi = '%d %s'.format(device.signal.value.rssi, _('dBm'));
|
|
|
|
if (device.signal.value.noise != null)
|
|
return '%d / %d %s'.format(device.signal.value.rssi, device.signal.value.noise, _('dBm'));
|
|
|
|
return rssi;
|
|
},
|
|
|
|
// Fixed five ticks in the unit the peak falls into; the interval is the
|
|
// smallest 1-2-5 step whose fourth multiple covers the peak.
|
|
byteScale(peak) {
|
|
const units = [ 'B', 'KiB', 'MiB', 'GiB', 'TiB' ];
|
|
let exp = 0;
|
|
|
|
while (peak >= 1024 && exp < units.length - 1) {
|
|
peak /= 1024;
|
|
exp++;
|
|
}
|
|
|
|
const raw = peak / 4;
|
|
const magnitude = Math.pow(10, Math.floor(Math.log10(Math.max(raw, 1e-9))));
|
|
const step = [ 1, 2, 5, 10 ].map(n => n * magnitude).find(candidate => candidate >= raw) * Math.pow(1024, exp);
|
|
const unit = Math.pow(1024, exp);
|
|
const decimals = (step / unit < 1) ? 1 : 0;
|
|
|
|
return {
|
|
step: step,
|
|
format: value => '%s %s'.format((value / unit).toFixed(decimals), units[exp])
|
|
};
|
|
},
|
|
|
|
renderTrafficChart() {
|
|
const devices = this.params.wifi.devices.slice().sort((a, b) =>
|
|
(b.transferred.value.bytes.rx + b.transferred.value.bytes.tx) - (a.transferred.value.bytes.rx + a.transferred.value.bytes.tx));
|
|
const peak = Math.max(1, ...devices.map(device => Math.max(device.transferred.value.bytes.rx, device.transferred.value.bytes.tx)));
|
|
const scale = this.byteScale(peak);
|
|
|
|
return charts.card({
|
|
title: _('Wireless client data usage'),
|
|
desc: devices.length ? _('Transferred') : '',
|
|
body: devices.length ? [
|
|
charts.barChart({
|
|
id: 'traffic',
|
|
max: scale.step * 4,
|
|
ticks: [ 0, 1, 2, 3, 4 ].map(n => ({ value: scale.step * n, label: scale.format(scale.step * n) })),
|
|
items: devices.map(device => ({
|
|
label: device.hostname.value,
|
|
title: '%s · %s %s · %s %s'.format(device.hostname.value, _('Download'), device.transferred.value.tx, _('Upload'), device.transferred.value.rx)
|
|
+ (device.connected.value ? ' · %s %s'.format(_('Connected'), device.connected.value) : ''),
|
|
values: [
|
|
{ value: device.transferred.value.bytes.tx, className: 'dashboard-bar-down' },
|
|
{ value: device.transferred.value.bytes.rx, className: 'dashboard-bar-up' }
|
|
]
|
|
}))
|
|
}),
|
|
charts.legend([
|
|
{ className: 'dashboard-bar-down', label: _('Download') },
|
|
{ className: 'dashboard-bar-up', label: _('Upload') }
|
|
])
|
|
] : charts.empty(_('No wireless clients connected'))
|
|
});
|
|
},
|
|
|
|
renderNetworkCard(radio) {
|
|
const active = radio.isactive.value === true;
|
|
const fields = [];
|
|
|
|
for (const key of [ 'chan', 'rate', 'encryption', 'bssid' ])
|
|
if (radio[key].visible)
|
|
fields.push(E('div', {}, [
|
|
E('span', {}, [ radio[key].title ]),
|
|
E('b', {}, [ radio[key].value ])
|
|
]));
|
|
|
|
return E('div', { 'class': 'ifacebox' }, [
|
|
E('div', { 'class': 'ifacebox-head' + (active ? ' active' : '') }, [
|
|
E('strong', {}, [ radio.ssid.value ])
|
|
]),
|
|
E('div', { 'class': 'ifacebox-body' }, [
|
|
E('div', { 'class': 'dashboard-net-count' }, [
|
|
E('b', {}, [ String(active ? radio.associations.value : 0) ]),
|
|
E('small', {}, [ radio.associations.title ])
|
|
]),
|
|
active
|
|
? E('div', { 'class': 'dashboard-net-fields' }, fields)
|
|
: E('div', { 'class': 'dashboard-net-note' }, [ _('Interface is disabled') ])
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderClientTable() {
|
|
return charts.table({
|
|
id: 'dashboard-wifi-table',
|
|
head: [
|
|
_('Hostname'),
|
|
_('IP Address'),
|
|
_('SSID'),
|
|
_('Band'),
|
|
'%s / %s'.format(_('Signal'), _('Noise floor')),
|
|
'%s / %s'.format(_('Up.'), _('Down.')),
|
|
_('Connected')
|
|
],
|
|
rows: this.params.wifi.devices.map(device => [
|
|
device.hostname.value,
|
|
device.addresses.value.length
|
|
? E('div', { 'class': 'dashboard-client-addresses' }, device.addresses.value.map(address => E('div', {}, [ address ])))
|
|
: '-',
|
|
device.ssid.value,
|
|
device.band.value || '-',
|
|
E('span', {}, [
|
|
charts.badge((device.signal.value.rssi != null) ? '%d %s'.format(device.signal.value.rssi, _('dBm')) : _('No RX signal'), signalGrade(device.signal.value.rssi).kind),
|
|
' ',
|
|
(device.signal.value.noise != null)
|
|
? E('small', {}, [ '/ %d %s'.format(device.signal.value.noise, _('dBm')) ])
|
|
: ''
|
|
]),
|
|
E('span', {}, [
|
|
E('span', { 'title': _('Up.') }, [ device.transferred.value.rx ]),
|
|
' / ',
|
|
E('span', { 'title': _('Down.') }, [ device.transferred.value.tx ])
|
|
]),
|
|
device.connected.value || '-'
|
|
]),
|
|
emptyText: _('No wireless clients connected'),
|
|
foot: [ _('Total'), String(this.params.wifi.devices.length) ]
|
|
});
|
|
},
|
|
|
|
frequencyBand(frequency) {
|
|
const freq = Number(frequency);
|
|
let band = null;
|
|
|
|
// Wi-Fi frequency band boundaries (GHz):
|
|
// - Sub-1 GHz: < 1 GHz (NL80211_BAND_S1GHZ, covers 802.11ah HaLow / 802.11af)
|
|
// - 2.4 GHz: 2.312-2.484 GHz (down to ch -19 in ath9k/ath5k; ch 14 upper bound)
|
|
// - 5 GHz: 4.91-5.925 GHz (802.11j public safety through U-NII-4)
|
|
// - 6 GHz: 5.925-7.125 GHz (U-NII-5 through U-NII-8)
|
|
// - 60 GHz: 58.32-70.2 GHz (802.11ad/ay DMG band)
|
|
// 5/6/60 GHz and ch 14 follow Linux kernel ieee80211_freq_khz_to_channel():
|
|
// https://github.com/torvalds/linux/blob/165768bb70265b5c38cf0b73fafd75be235f8b14/net/wireless/util.c#L114-L132
|
|
if (freq > 0 && freq < 1)
|
|
band = 'Sub-1';
|
|
else if (freq >= 2.312 && freq <= 2.484)
|
|
band = '2.4';
|
|
else if (freq >= 4.91 && freq < 5.925)
|
|
band = '5';
|
|
else if (freq >= 5.925 && freq <= 7.125)
|
|
band = '6';
|
|
else if (freq >= 58.32 && freq <= 70.2)
|
|
band = '60';
|
|
|
|
return band ? '%s %s'.format(band, _('GHz')) : null;
|
|
},
|
|
|
|
renderTab() {
|
|
const netGrid = E('div', { 'class': 'dashboard-net-grid' }, this.params.wifi.radios.map(radio => this.renderNetworkCard(radio)));
|
|
const clientTable = this.renderClientTable();
|
|
|
|
if (this.tabNode && this.tabNode.parentNode) {
|
|
const oldGrid = this.tabNode.querySelector('.dashboard-net-grid');
|
|
if (oldGrid)
|
|
this.tabNode.replaceChild(netGrid, oldGrid);
|
|
return this.tabNode;
|
|
}
|
|
|
|
this.tabNode = E('div', {}, [
|
|
netGrid,
|
|
E('h3', {}, [ _('Wireless clients') ]),
|
|
clientTable
|
|
]);
|
|
|
|
return this.tabNode;
|
|
},
|
|
|
|
clientAddresses(ipv4, hint) {
|
|
const ipv6 = L.toArray(hint?.ip6addrs || hint?.ipv6)
|
|
.find(address => address && !/^fe[89ab][0-9a-f]:/i.test(address));
|
|
|
|
return [ ipv4, ipv6 ].filter(Boolean);
|
|
},
|
|
|
|
renderUpdateData(radios, networks, hosthints) {
|
|
for (let i = 0; i < radios.sort((a, b) => a.getName().localeCompare(b.getName())).length; i++) {
|
|
const network_items = networks.filter(net => { return net.getWifiDeviceName() == radios[i].getName() });
|
|
|
|
for (let j = 0; j < network_items.length; j++) {
|
|
const net = network_items[j];
|
|
const is_assoc = (net.getBSSID() != '00:00:00:00:00:00' && net.getChannel() && !net.isDisabled());
|
|
const chan = net.getChannel();
|
|
const freq = net.getFrequency();
|
|
const rate = net.getBitRate();
|
|
|
|
this.params.wifi.radios.push(
|
|
{
|
|
ssid : {
|
|
title: _('SSID'),
|
|
visible: true,
|
|
value: net.getActiveSSID() || '?'
|
|
},
|
|
|
|
isactive : {
|
|
title: _('Active'),
|
|
visible: true,
|
|
value: !net.isDisabled()
|
|
},
|
|
|
|
chan : {
|
|
title: _('Channel'),
|
|
visible: true,
|
|
value: chan ? '%d (%.3f %s)'.format(chan, freq, _('GHz')) : '-'
|
|
},
|
|
|
|
rate : {
|
|
title: _('Bitrate'),
|
|
visible: true,
|
|
value: rate ? '%d %s'.format(rate, _('Mbit/s')) : '-'
|
|
},
|
|
|
|
bssid : {
|
|
title: _('BSSID'),
|
|
visible: true,
|
|
value: is_assoc ? (net.getActiveBSSID() || '-') : '-'
|
|
},
|
|
|
|
encryption : {
|
|
title: _('Encryption'),
|
|
visible: true,
|
|
value: is_assoc ? net.getActiveEncryption() : '-'
|
|
},
|
|
|
|
associations : {
|
|
title: _('Devices Connected'),
|
|
visible: true,
|
|
value: is_assoc ? (net.assoclist.length || '0') : 0
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < networks.length; i++) {
|
|
const band = this.frequencyBand(networks[i].getFrequency());
|
|
|
|
for (let k = 0; k < networks[i].assoclist.length; k++) {
|
|
const bss = networks[i].assoclist[k];
|
|
const mac = bss.mac.toUpperCase();
|
|
const name = hosthints.getHostnameByMACAddr(mac);
|
|
|
|
this.params.wifi.devices.push(
|
|
{
|
|
band : {
|
|
title: _('Band'),
|
|
visible: true,
|
|
value: band
|
|
},
|
|
|
|
hostname : {
|
|
title: _('Hostname'),
|
|
visible: true,
|
|
value: name || '?'
|
|
},
|
|
|
|
addresses: {
|
|
title: _('IP Address'),
|
|
visible: true,
|
|
value: this.clientAddresses(hosthints.getIPAddrByMACAddr(mac), hosthints.hosts[mac])
|
|
},
|
|
|
|
ssid : {
|
|
title: _('SSID'),
|
|
visible: true,
|
|
value: networks[i].getActiveSSID()
|
|
},
|
|
|
|
signal : {
|
|
title: _('Signal Strength'),
|
|
visible: true,
|
|
value: {
|
|
rssi: (typeof(bss.signal) == 'number' && bss.signal < 0) ? bss.signal : null,
|
|
noise: (typeof(bss.noise) == 'number' && bss.noise != 0) ? bss.noise : null
|
|
}
|
|
},
|
|
|
|
// AP-side station counters: rx = received from the client (its
|
|
// upload), tx = sent to the client (its download)
|
|
transferred : {
|
|
title: _('Transferred'),
|
|
visible: true,
|
|
value: {
|
|
rx: '%s'.format('%1024.2mB'.format(bss.rx.bytes)),
|
|
tx: '%s'.format('%1024.2mB'.format(bss.tx.bytes)),
|
|
bytes: { rx: bss.rx.bytes || 0, tx: bss.tx.bytes || 0 }
|
|
}
|
|
},
|
|
|
|
// Connection duration is not reported by every driver.
|
|
connected: {
|
|
title: _('Connected'),
|
|
visible: true,
|
|
value: (typeof(bss.connected_time) == 'number') ? '%t'.format(bss.connected_time) : null
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
render([radios, networks, hosthints]) {
|
|
|
|
this.params.wifi = {
|
|
radios: [],
|
|
devices: []
|
|
};
|
|
|
|
this.renderUpdateData(radios, networks, hosthints);
|
|
|
|
if (!this.params.wifi.radios.length)
|
|
return null;
|
|
|
|
return {
|
|
cards: [ { id: 'wifi', node: () => this.renderKpi() } ],
|
|
charts: [
|
|
{ id: 'distribution', node: () => this.renderDistributionChart() },
|
|
{ id: 'signal', node: () => this.renderSignalChart() },
|
|
{ id: 'traffic', node: () => this.renderTrafficChart() }
|
|
],
|
|
tabs: [
|
|
{ id: 'wifi', title: this.title, count: this.params.wifi.devices.length, content: () => this.renderTab() }
|
|
]
|
|
};
|
|
}
|
|
});
|