'use strict'; 'require baseclass'; 'require dom'; 'require network'; 'require uci'; 'require fs'; 'require rpc'; 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: function(rt) { var s = '%.1f\xa0%s, %d\xa0%s'.format(rt.rate / 1000, _('Mbit/s'), rt.mhz, _('MHz')), ht = rt.ht, vht = rt.vht, mhz = rt.mhz, nss = rt.nss, mcs = rt.mcs, sgi = rt.short_gi, he = rt.he, he_gi = rt.he_gi, he_dcm = rt.he_dcm; if (ht || vht) { if (vht) s += ', VHT-MCS\xa0%d'.format(mcs); if (nss) s += ', VHT-NSS\xa0%d'.format(nss); if (ht) s += ', MCS\xa0%s'.format(mcs); if (sgi) s += ', ' + _('Short GI').replace(/ /g, '\xa0'); } if (he) { s += ', HE-MCS\xa0%d'.format(mcs); if (nss) s += ', HE-NSS\xa0%d'.format(nss); if (he_gi) s += ', HE-GI\xa0%d'.format(he_gi); if (he_dcm) s += ', HE-DCM\xa0%d'.format(he_dcm); } return s; }, handleDelClient: function(wifinet, mac, ev, cmd) { var 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: function(wifinet) { return rpc.declare({ object: 'hostapd.%s'.format(wifinet), method: 'wps_status', })() }, handleCallWPS: function(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: function(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: function(radio, networks) { var chan = null, freq = null, rate = null, badges = []; for (var i = 0; i < networks.length; i++) { var net = networks[i], is_assoc = (net.getBSSID() != '00:00:00:00:00:00' && net.getChannel() && !net.isDisabled()), quality = net.getSignalPercent(); var icon; if (net.isDisabled()) icon = L.resource('icons/signal-none.png'); else if (quality <= 0) icon = L.resource('icons/signal-0.png'); else if (quality < 25) icon = L.resource('icons/signal-0-25.png'); else if (quality < 50) icon = L.resource('icons/signal-25-50.png'); else if (quality < 75) icon = L.resource('icons/signal-50-75.png'); else icon = L.resource('icons/signal-75-100.png'); var 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') ]) } } var 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(); freq = (freq != null) ? freq : net.getFrequency(); rate = (rate != null) ? rate : net.getBitRate(); } 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, ''), _('Channel'), chan ? '%d (%.3f %s)'.format(chan, freq, _('GHz')) : '-', _('Bitrate'), rate ? '%d %s'.format(rate, _('Mbit/s')) : '-', ]), E('div', {}, badges) ]) ]); }, isWPSEnabled: {}, load: function() { 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'), uci.load('wireless') ]).then(L.bind(function(data) { var tasks = [], radios_networks_hints = data[1], hasWPS = L.hasSystemFeature('hostapd', 'wps'); for (var i = 0; i < radios_networks_hints.length; i++) { tasks.push(L.resolveDefault(radios_networks_hints[i].getAssocList(), []).then(L.bind(function(net, list) { net.assoclist = list.sort(function(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(function(net, data) { net.wps_status = data ? data.pbc_status : _('No Data'); }, this, radios_networks_hints[i]))); } } return Promise.all(tasks).then(function() { return data; }); }, this)); }, render: function(data) { var seen = {}, radios = data[0], networks = data[1], hosthints = data[2], hasReadPermission = data[3], hasWritePermission = data[4]; var table = E('div', { 'class': 'network-status-table' }); for (var i = 0; i < radios.sort(function(a, b) { a.getName() > b.getName() }).length; i++) table.appendChild(this.renderbox(radios[i], networks.filter(function(net) { return net.getWifiDeviceName() == radios[i].getName() }))); if (!table.lastElementChild) return null; var 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'))) ]) ]); var rows = []; for (var i = 0; i < networks.length; i++) { var macfilter = uci.get('wireless', networks[i].sid, 'macfilter'), maclist = {}; if (macfilter != null && macfilter != 'disable') { networks[i].maclist = L.toArray(uci.get('wireless', networks[i].sid, 'maclist')); for (var j = 0; j < networks[i].maclist.length; j++) { var mac = networks[i].maclist[j].toUpperCase(); maclist[mac] = true; } } for (var k = 0; k < networks[i].assoclist.length; k++) { var bss = networks[i].assoclist[k], name = hosthints.getHostnameByMACAddr(bss.mac), ipv4 = hosthints.getIPAddrByMACAddr(bss.mac), ipv6 = hosthints.getIP6AddrByMACAddr(bss.mac); var icon; var q = Math.min((bss.signal + 110) / 70 * 100, 100); if (q == 0) icon = L.resource('icons/signal-0.png'); else if (q < 25) icon = L.resource('icons/signal-0-25.png'); else if (q < 50) icon = L.resource('icons/signal-25-50.png'); else if (q < 75) icon = L.resource('icons/signal-50-75.png'); else icon = L.resource('icons/signal-75-100.png'); var 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')); } var 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 || '?'; var 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.png') }), 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 (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([]) ]); } });