'use strict';
'require baseclass';
'require dom';
'require network';
'require uci';
'require fs';
'require rpc';
'require firewall';
return baseclass.extend({
title: _('Wireless'),
WPSTranslateTbl: {
Disabled: _('Disabled'),
Active: _('Active'),
'Timed-out': _('Timed-out'),
Overlap: _('Overlap'),
Unknown: _('Unknown')
},
callSessionAccess: rpc.declare({
object: 'session',
method: 'access',
params: [ 'scope', 'object', 'function' ],
expect: { 'access': false }
}),
wifirate(rate) {
let s = `${rate.rate / 1000}\xa0${_('Mbit/s')}, ${rate.mhz}\xa0${_('MHz')}`;
if (rate?.ht || rate?.vht) s += [
rate?.vht && `, VHT-MCS\xa0${rate?.mcs}`,
rate?.nss && `, VHT-NSS\xa0${rate?.nss}`,
rate?.ht && `, MCS\xa0${rate?.mcs}`,
rate?.short_gi && ', ' + _('Short GI').replace(/ /g, '\xa0')
].filter(Boolean).join('');
if (rate?.he) s += [
`, HE-MCS\xa0${rate?.mcs}`,
rate?.nss && `, HE-NSS\xa0${rate?.nss}`,
rate?.he_gi && `, HE-GI\xa0${rate?.he_gi}`,
rate?.he_dcm && `, HE-DCM\xa0${rate?.he_dcm}`
].filter(Boolean).join('');
if (rate?.eht) s += [
`, EHT-MCS\xa0${rate?.mcs}`,
rate?.nss && `, EHT-NSS\xa0${rate?.nss}`,
rate?.eht_gi && `, EHT-GI\xa0${rate?.eht_gi}`,
rate?.eht_dcm && `, EHT-DCM\xa0${rate?.eht_dcm}`
].filter(Boolean).join('');
return s;
},
handleDelClient(wifinet, mac, ev, cmd) {
const exec = cmd || 'disconnect';
dom.parent(ev.currentTarget, '.tr').style.opacity = 0.5;
ev.currentTarget.classList.add('spinning');
ev.currentTarget.disabled = true;
ev.currentTarget.blur();
/* Disconnect client before adding to maclist */
wifinet.disconnectClient(mac, true, 5, 60000);
if (exec == 'addlist') {
wifinet.maclist.push(mac);
uci.set('wireless', wifinet.sid, 'maclist', wifinet.maclist);
return uci.save()
.then(L.bind(L.ui.changes.init, L.ui.changes))
.then(L.bind(L.ui.changes.displayChanges, L.ui.changes));
}
},
handleGetWPSStatus(wifinet) {
return rpc.declare({
object: 'hostapd.%s'.format(wifinet),
method: 'wps_status',
})()
},
handleCallWPS(wifinet, ev) {
ev.currentTarget.classList.add('spinning');
ev.currentTarget.disabled = true;
ev.currentTarget.blur();
return rpc.declare({
object: 'hostapd.%s'.format(wifinet),
method: 'wps_start',
})();
},
handleCancelWPS(wifinet, ev) {
ev.currentTarget.classList.add('spinning');
ev.currentTarget.disabled = true;
ev.currentTarget.blur();
return rpc.declare({
object: 'hostapd.%s'.format(wifinet),
method: 'wps_cancel',
})();
},
renderbox(radio, networks) {
let chan = null;
let freq = null;
let rate = null;
let coco = null;
let noise = null;
let tx_power = null;
const badges = [];
for (let i = 0; i < networks.length; i++) {
const net = networks[i];
const is_assoc = (net.getBSSID() != '00:00:00:00:00:00' && net.getChannel() && !net.isDisabled());
const quality = net.getSignalPercent();
let icon;
if (net.isDisabled())
icon = L.resource('icons/signal-none.svg');
else if (quality <= 0)
icon = L.resource('icons/signal-000-000.svg');
else if (quality < 25)
icon = L.resource('icons/signal-000-025.svg');
else if (quality < 50)
icon = L.resource('icons/signal-025-050.svg');
else if (quality < 75)
icon = L.resource('icons/signal-050-075.svg');
else
icon = L.resource('icons/signal-075-100.svg');
let WPS_button = null;
if (net.isWPSEnabled) {
if (net.wps_status == 'Active') {
WPS_button = E('button', {
'class' : 'cbi-button cbi-button-remove',
'click': L.bind(this.handleCancelWPS, this, net.getIfname()),
}, [ _('Stop WPS') ])
} else {
WPS_button = E('button', {
'class' : 'cbi-button cbi-button-apply',
'click': L.bind(this.handleCallWPS, this, net.getIfname()),
}, [ _('Start WPS') ])
}
}
const badge = renderBadge(
icon,
'%s: %d dBm / %s: %d%%'.format(_('Signal'), net.getSignal(), _('Quality'), quality),
_('SSID'), net.getActiveSSID() || '?',
_('Mode'), net.getActiveMode(),
_('BSSID'), is_assoc ? (net.getActiveBSSID() || '-') : null,
_('Encryption'), is_assoc ? net.getActiveEncryption() : null,
_('Associations'), is_assoc ? (net.assoclist.length || '-') : null,
null, is_assoc ? null : E('em', net.isDisabled() ? _('Wireless is disabled') : _('Wireless is not associated')),
_('WPS status'), this.WPSTranslateTbl[net.wps_status],
'', WPS_button
);
badges.push(badge);
chan = (chan != null) ? chan : net.getChannel();
coco = (coco != null) ? coco : net.getCountryCode();
freq = (freq != null) ? freq : net.getFrequency();
rate = (rate != null) ? rate : net.getBitRate();
noise = (noise != null) ? noise : net.getNoise();
tx_power = (tx_power != null) ? tx_power : net.getTXPower();
}
return E('div', { class: 'ifacebox' }, [
E('div', { class: 'ifacebox-head center ' + (radio.isUp() ? 'active' : '') },
E('strong', radio.getName())),
E('div', { class: 'ifacebox-body left' }, [
L.itemlist(E('span'), [
_('Type'), radio.getI18n().replace(/^Generic | Wireless Controller .+$/g, ''),
_('Bitrate'), rate ? '%d %s'.format(rate, _('Mbit/s')) : null,
_('Channel'), chan ? '%d (%.3f %s)'.format(chan, freq, _('GHz')) : null,
_('Country Code'), coco ? '%s'.format(coco) : null,
_('Noise'), noise ? '%.2f %s'.format(noise, _('dBm')) : null,
_('TX Power'), tx_power ? '%.2f %s'.format(tx_power, _('dBm')): null,
]),
E('div', {}, badges)
])
]);
},
isWPSEnabled: {},
load() {
return Promise.all([
network.getWifiDevices(),
network.getWifiNetworks(),
network.getHostHints(),
this.callSessionAccess('access-group', 'luci-mod-status-index-wifi', 'read'),
this.callSessionAccess('access-group', 'luci-mod-status-index-wifi', 'write'),
firewall.getZones(),
L.hasSystemFeature('wifi') ? L.resolveDefault(uci.load('wireless')) : L.resolveDefault(),
]).then(L.bind(data => {
const tasks = [];
const radios_networks_hints = data[1];
const hasWPS = L.hasSystemFeature('hostapd', 'wps');
for (let i = 0; i < radios_networks_hints.length; i++) {
tasks.push(L.resolveDefault(radios_networks_hints[i].getAssocList(), []).then(L.bind((net, list) => {
net.assoclist = list.sort((a, b) => { return a.mac > b.mac });
}, this, radios_networks_hints[i])));
if (hasWPS && uci.get('wireless', radios_networks_hints[i].sid, 'wps_pushbutton') == '1') {
radios_networks_hints[i].isWPSEnabled = true;
tasks.push(L.resolveDefault(this.handleGetWPSStatus(radios_networks_hints[i].getIfname()), null)
.then(L.bind((net, data) => {
net.wps_status = data ? data.pbc_status : _('No Data');
}, this, radios_networks_hints[i])));
}
}
return Promise.all(tasks).then(() => {
return data;
});
}, this));
},
render(data) {
const radios = data[0];
const networks = data[1];
const hosthints = data[2];
const hasReadPermission = data[3];
const hasWritePermission = data[4];
const zones = data[5];
const table = E('div', { 'class': 'network-status-table' });
for (let i = 0; i < radios.sort((a, b) => { a.getName() > b.getName() }).length; i++)
table.appendChild(this.renderbox(radios[i],
networks.filter(net => { return net.getWifiDeviceName() == radios[i].getName() })));
if (!table.lastElementChild)
return null;
const assoclist = E('table', { 'class': 'table assoclist', 'id': 'wifi_assoclist_table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th nowrap' }, _('Network')),
E('th', { 'class': 'th hide-xs' }, _('MAC address')),
E('th', { 'class': 'th' }, _('Host')),
E('th', { 'class': 'th' }, '%s / %s'.format(_('Signal'), _('Noise'))),
E('th', { 'class': 'th' }, '%s / %s'.format(_('RX Rate'), _('TX Rate')))
])
]);
const rows = [];
for (let i = 0; i < networks.length; i++) {
const macfilter = uci.get('wireless', networks[i].sid, 'macfilter');
const maclist = {};
if (macfilter != null && macfilter != 'disable') {
networks[i].maclist = L.toArray(uci.get('wireless', networks[i].sid, 'maclist'));
for (let j = 0; j < networks[i].maclist.length; j++) {
const mac = networks[i].maclist[j].toUpperCase();
maclist[mac] = true;
}
}
for (let k = 0; k < networks[i].assoclist.length; k++) {
const bss = networks[i].assoclist[k];
const name = hosthints.getHostnameByMACAddr(bss.mac);
const ipv4 = hosthints.getIPAddrByMACAddr(bss.mac);
const ipv6 = hosthints.getIP6AddrByMACAddr(bss.mac);
let icon;
const q = Math.min((bss.signal + 110) / 70 * 100, 100);
if (q == 0)
icon = L.resource('icons/signal-000-000.svg');
else if (q < 25)
icon = L.resource('icons/signal-000-025.svg');
else if (q < 50)
icon = L.resource('icons/signal-025-050.svg');
else if (q < 75)
icon = L.resource('icons/signal-050-075.svg');
else
icon = L.resource('icons/signal-075-100.svg');
let sig_title, sig_value;
if (bss.noise) {
sig_value = '%d/%d\xa0%s'.format(bss.signal, bss.noise, _('dBm'));
sig_title = '%s: %d %s / %s: %d %s / %s %d'.format(
_('Signal'), bss.signal, _('dBm'),
_('Noise'), bss.noise, _('dBm'),
_('SNR'), bss.signal - bss.noise);
}
else {
sig_value = '%d\xa0%s'.format(bss.signal, _('dBm'));
sig_title = '%s: %d %s'.format(_('Signal'), bss.signal, _('dBm'));
}
let hint;
if (name && ipv4 && ipv6)
hint = '%s (%s, %s)'.format(name, ipv4, ipv6);
else if (name && (ipv4 || ipv6))
hint = '%s (%s)'.format(name, ipv4 || ipv6);
else
hint = name || ipv4 || ipv6 || '?';
const row = [
E('span', {
'class': 'ifacebadge',
'title': networks[i].getI18n(),
'data-ifname': networks[i].getIfname(),
'data-ssid': networks[i].getActiveSSID()
}, [
E('img', { 'src': L.resource('icons/wifi.svg'), 'style': 'width:32px;height:32px' }),
E('span', {}, [
' ', networks[i].getShortName(),
E('small', {}, [ ' (', networks[i].getIfname(), ')' ])
])
]),
bss.mac,
hint,
E('span', {
'class': 'ifacebadge',
'title': sig_title,
'data-signal': bss.signal,
'data-noise': bss.noise
}, [
E('img', { 'src': icon }),
E('span', {}, [
' ', sig_value
])
]),
E('span', {}, [
E('span', this.wifirate(bss.rx)),
E('br'),
E('span', this.wifirate(bss.tx))
])
];
if (bss.vlan) {
const desc = bss.vlan.getI18n();
const vlan_network = bss.vlan.getNetwork();
let vlan_zone;
if (vlan_network)
for (let zone of zones)
if (zone.getNetworks().includes(vlan_network))
vlan_zone = zone;
row[0].insertBefore(
E('div', {
'class' : 'zonebadge',
'title' : desc,
'style' : firewall.getZoneColorStyle(vlan_zone)
}, [ desc ]), row[0].firstChild);
}
if (networks[i].isClientDisconnectSupported() && hasWritePermission) {
if (assoclist.firstElementChild.childNodes.length < 6)
assoclist.firstElementChild.appendChild(E('th', { 'class': 'th cbi-section-actions' }));
if (macfilter != null && macfilter != 'disable' && !maclist[bss.mac]) {
row.push(new L.ui.ComboButton('button', {
'addlist': macfilter == 'allow' ? _('Add to Whitelist') : _('Add to Blacklist'),
'disconnect': _('Disconnect')
}, {
'click': L.bind(this.handleDelClient, this, networks[i], bss.mac),
'sort': [ 'disconnect', 'addlist' ],
'classes': {
'addlist': 'btn cbi-button cbi-button-remove',
'disconnect': 'btn cbi-button cbi-button-remove'
}
}).render()
)
}
else {
row.push(E('button', {
'class': 'cbi-button cbi-button-remove',
'click': L.bind(this.handleDelClient, this, networks[i], bss.mac)
}, [ _('Disconnect') ]));
}
}
else {
row.push('-');
}
rows.push(row);
}
}
cbi_update_table(assoclist, rows, E('em', _('No information available')));
return E([
table,
hasReadPermission ? E('h3', _('Associated Stations')) : E([]),
hasReadPermission ? assoclist : E([])
]);
}
});