mirror of
https://github.com/openwrt/luci.git
synced 2026-04-15 19:01:56 +00:00
luci-base: poe and PSE details & configuration
Adds PoE/PSE configuration support for modern linux (PSE-PD). This change is based on the PSE-PD backport (from 6.17) and netifd|ubus changes. * Add getPSE() [receive all status information] and hasPSE() [has the device PSE hardware?] * Changes ACL permissions to query network.device status information * Add two new PoE icons (PoE active with link up + link down) for the port status page * Changes port status to show PoE information, next to link information and data transfer. * Add a new tab for PoE/PSE to the device configuration, which will only be displayed if the device supports PSE Signed-off-by: Carlo Szelinsky <github@szelinsky.de>
This commit is contained in:
committed by
Paul Donald
parent
77f8596e2a
commit
8e493db75a
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.3 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 6.0 KiB |
@@ -3240,6 +3240,50 @@ Device = baseclass.extend(/** @lends LuCI.network.Device.prototype */ {
|
||||
return (duplex != 'unknown') ? duplex : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the PSE (Power Sourcing Equipment / PoE) status of the device.
|
||||
*
|
||||
* @returns {Object|null}
|
||||
* Returns an object containing PSE status information or null if
|
||||
* PSE is not available on this device. The object may contain:
|
||||
* - c33AdminState: "enabled" or "disabled" (C33 PoE admin state)
|
||||
* - c33PowerStatus: "disabled", "searching", "delivering", "test", "fault", "otherfault"
|
||||
* - c33PowerClass: Power class number (1-8)
|
||||
* - c33ActualPower: Actual power consumption in mW
|
||||
* - c33AvailablePowerLimit: Available power limit in mW
|
||||
* - podlAdminState: "enabled" or "disabled" (PoDL admin state)
|
||||
* - podlPowerStatus: "disabled", "searching", "delivering", "sleep", "idle", "error"
|
||||
* - priority: Current priority level
|
||||
* - priorityMax: Maximum priority level
|
||||
*/
|
||||
getPSE: function() {
|
||||
const pse = this._devstate('pse');
|
||||
if (!pse)
|
||||
return null;
|
||||
|
||||
return {
|
||||
c33AdminState: pse['c33-admin-state'] || null,
|
||||
c33PowerStatus: pse['c33-power-status'] || null,
|
||||
c33PowerClass: pse['c33-power-class'] || null,
|
||||
c33ActualPower: pse['c33-actual-power'] || null,
|
||||
c33AvailablePowerLimit: pse['c33-available-power-limit'] || null,
|
||||
podlAdminState: pse['podl-admin-state'] || null,
|
||||
podlPowerStatus: pse['podl-power-status'] || null,
|
||||
priority: pse['priority'] || null,
|
||||
priorityMax: pse['priority-max'] || null
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if PSE (PoE) is available on this device.
|
||||
*
|
||||
* @returns {boolean}
|
||||
* Returns true if PSE hardware is available on this device.
|
||||
*/
|
||||
hasPSE: function() {
|
||||
return this._devstate('pse') != null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the primary logical interface this device is assigned to.
|
||||
*
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"ubus": {
|
||||
"luci-rpc": [ "getBoardJSON", "getHostHints", "getNetworkDevices", "getWirelessDevices" ],
|
||||
"network": [ "get_proto_handlers" ],
|
||||
"network.device": [ "status" ],
|
||||
"network.interface": [ "dump" ]
|
||||
},
|
||||
"uci": [ "luci", "network", "wireless" ]
|
||||
|
||||
@@ -440,7 +440,7 @@ return baseclass.extend({
|
||||
return s.taboption(tabName, optionClass, optionName, optionTitle, optionDescription);
|
||||
},
|
||||
|
||||
addDeviceOptions: function(s, dev, isNew, rtTables) {
|
||||
addDeviceOptions: function(s, dev, isNew, rtTables, hasPSE) {
|
||||
var parent_dev = dev ? dev.getParent() : null,
|
||||
devname = dev ? dev.getName() : null,
|
||||
o, ss;
|
||||
@@ -449,6 +449,8 @@ return baseclass.extend({
|
||||
s.tab('devadvanced', _('Advanced device options'));
|
||||
s.tab('brport', _('Bridge port specific options'));
|
||||
s.tab('bridgevlan', _('Bridge VLAN filtering'));
|
||||
if (hasPSE)
|
||||
s.tab('devpse', _('PoE / PSE options'));
|
||||
|
||||
o = this.replaceOption(s, 'devgeneral', form.ListValue, 'type', _('Device type'),
|
||||
(!L.hasSystemFeature('bonding') && isNew ? '<a href="' + L.url("admin", "system", "package-manager", "?query=kmod-bonding") + '">'+
|
||||
@@ -1139,6 +1141,34 @@ return baseclass.extend({
|
||||
o.placeholder = dev ? dev._devstate('qlen') : '';
|
||||
o.datatype = 'uinteger';
|
||||
|
||||
/* PSE / PoE options */
|
||||
if (hasPSE) {
|
||||
o = this.replaceOption(s, 'devpse', form.ListValue, 'pse', _('PoE (C33)'),
|
||||
_('Power over Ethernet (IEEE 802.3af/at/bt) control for this port. Requires PSE hardware support.'));
|
||||
o.value('', _('Default'));
|
||||
o.value('1', _('Enabled'));
|
||||
o.value('0', _('Disabled'));
|
||||
|
||||
o = this.replaceOption(s, 'devpse', form.ListValue, 'pse_podl', _('PoDL'),
|
||||
_('Power over Data Lines (IEEE 802.3bu/cg) for single-pair Ethernet.'));
|
||||
o.value('', _('Default'));
|
||||
o.value('1', _('Enabled'));
|
||||
o.value('0', _('Disabled'));
|
||||
|
||||
o = this.replaceOption(s, 'devpse', form.Value, 'pse_power_limit', _('Power limit (mW)'),
|
||||
_('Maximum power budget for this port in milliwatts. Leave empty for default/maximum.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = _('auto');
|
||||
o.rmempty = true;
|
||||
|
||||
o = this.replaceOption(s, 'devpse', form.ListValue, 'pse_priority', _('Port priority'),
|
||||
_('Priority level for power allocation when total power budget is exceeded.'));
|
||||
o.value('', _('Default'));
|
||||
o.value('1', _('Critical'));
|
||||
o.value('2', _('High'));
|
||||
o.value('3', _('Low'));
|
||||
}
|
||||
|
||||
o = this.replaceOption(s, 'devadvanced', cbiFlagTristate, 'promisc', _('Enable promiscuous mode'));
|
||||
o.sysfs_default = (dev && dev.dev && dev.dev.flags) ? dev.dev.flags.promisc : null;
|
||||
|
||||
|
||||
@@ -5,12 +5,20 @@
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require rpc';
|
||||
'require form';
|
||||
'require network';
|
||||
'require firewall';
|
||||
'require tools.widgets as widgets';
|
||||
'require tools.network as nettools';
|
||||
|
||||
const callNetworkDeviceStatus = rpc.declare({
|
||||
object: 'network.device',
|
||||
method: 'status',
|
||||
params: [ 'name' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var isReadonlyView = !L.hasViewPermission() || null;
|
||||
|
||||
function count_changes(section_id) {
|
||||
@@ -1570,10 +1578,20 @@ return view.extend({
|
||||
};
|
||||
|
||||
s.addModalOptions = function(s) {
|
||||
var isNew = (uci.get('network', s.section, 'name') == null),
|
||||
dev = getDevice(s.section);
|
||||
const isNew = (uci.get('network', s.section, 'name') == null),
|
||||
dev = getDevice(s.section),
|
||||
devName = dev ? dev.getName() : null;
|
||||
|
||||
nettools.addDeviceOptions(s, dev, isNew, rtTables);
|
||||
/* Query PSE status from netifd to determine if device has PSE capability */
|
||||
if (devName) {
|
||||
return L.resolveDefault(callNetworkDeviceStatus(devName), {}).then((status) => {
|
||||
const hasPSE = (status.pse != null);
|
||||
nettools.addDeviceOptions(s, dev, isNew, rtTables, hasPSE);
|
||||
});
|
||||
} else {
|
||||
nettools.addDeviceOptions(s, dev, isNew, rtTables, false);
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
s.handleModalCancel = function(map /*, ... */) {
|
||||
|
||||
@@ -13,6 +13,13 @@ var callGetBuiltinEthernetPorts = rpc.declare({
|
||||
expect: { result: [] }
|
||||
});
|
||||
|
||||
const callNetworkDeviceStatus = rpc.declare({
|
||||
object: 'network.device',
|
||||
method: 'status',
|
||||
params: [ 'name' ],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function isString(v)
|
||||
{
|
||||
return typeof(v) === 'string' && v !== '';
|
||||
@@ -236,10 +243,52 @@ function formatSpeed(carrier, speed, duplex) {
|
||||
return carrier ? _('Connected') : _('no link');
|
||||
}
|
||||
|
||||
function formatStats(portdev) {
|
||||
var stats = portdev._devstate('stats') || {};
|
||||
function getPSEStatus(pse) {
|
||||
if (!pse)
|
||||
return null;
|
||||
|
||||
return ui.itemlist(E('span'), [
|
||||
const status = pse['c33-power-status'] || pse['podl-power-status'],
|
||||
power = pse['c33-actual-power'];
|
||||
|
||||
return {
|
||||
status: status,
|
||||
power: power,
|
||||
isDelivering: status === 'delivering' && power > 0
|
||||
};
|
||||
}
|
||||
|
||||
function formatPSEPower(pse) {
|
||||
if (!pse)
|
||||
return null;
|
||||
|
||||
const status = pse['c33-power-status'] || pse['podl-power-status'],
|
||||
power = pse['c33-actual-power'];
|
||||
|
||||
if (status === 'delivering' && power) {
|
||||
const watts = (power / 1000).toFixed(1);
|
||||
/* Format: "⚡ 15.4 W" - lightning bolt + narrow space + watts + narrow space + W */
|
||||
return E('span', { 'style': 'color:#000' },
|
||||
[ '\u26a1\ufe0e\u202f%s\u202fW'.format(watts) ]);
|
||||
}
|
||||
else if (status === 'searching') {
|
||||
return E('span', { 'style': 'color:#000' },
|
||||
[ '\u26a1\ufe0e\u202f' + _('searching') ]);
|
||||
}
|
||||
else if (status === 'fault' || status === 'otherfault' || status === 'error') {
|
||||
return E('span', { 'style': 'color:#d9534f' },
|
||||
[ '\u26a1\ufe0e\u202f' + _('fault') ]);
|
||||
}
|
||||
else if (status === 'disabled') {
|
||||
return E('span', { 'style': 'color:#888' },
|
||||
[ '\u26a1\ufe0e\u202f' + _('off') ]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatStats(portdev, pse) {
|
||||
const stats = portdev._devstate('stats') || {};
|
||||
const items = [
|
||||
_('Received bytes'), '%1024mB'.format(stats.rx_bytes),
|
||||
_('Received packets'), '%1000mPkts.'.format(stats.rx_packets),
|
||||
_('Received multicast'), '%1000mPkts.'.format(stats.multicast),
|
||||
@@ -252,7 +301,27 @@ function formatStats(portdev) {
|
||||
_('Transmit dropped'), '%1000mPkts.'.format(stats.tx_dropped),
|
||||
|
||||
_('Collisions seen'), stats.collisions
|
||||
]);
|
||||
];
|
||||
|
||||
if (pse) {
|
||||
const status = pse['c33-power-status'] || pse['podl-power-status'],
|
||||
power = pse['c33-actual-power'],
|
||||
powerClass = pse['c33-power-class'],
|
||||
powerLimit = pse['c33-available-power-limit'];
|
||||
|
||||
items.push(_('PoE status'), status || _('unknown'));
|
||||
|
||||
if (power)
|
||||
items.push(_('PoE power'), '%.1f W'.format(power / 1000));
|
||||
|
||||
if (powerClass)
|
||||
items.push(_('PoE class'), powerClass);
|
||||
|
||||
if (powerLimit)
|
||||
items.push(_('PoE limit'), '%.1f W'.format(powerLimit / 1000));
|
||||
}
|
||||
|
||||
return ui.itemlist(E('span'), items);
|
||||
}
|
||||
|
||||
function renderNetworkBadge(network, zonename) {
|
||||
@@ -309,16 +378,57 @@ return baseclass.extend({
|
||||
firewall.getZones(),
|
||||
network.getNetworks(),
|
||||
uci.load('network')
|
||||
]);
|
||||
]).then((data) => {
|
||||
/* Get all known port names from builtin ports or board.json */
|
||||
const builtinPorts = data[0] || [];
|
||||
const board = JSON.parse(data[1] || '{}');
|
||||
const allPorts = new Set();
|
||||
|
||||
/* Collect port names from builtin ethernet ports */
|
||||
builtinPorts.forEach((port) => {
|
||||
if (port.device)
|
||||
allPorts.add(port.device);
|
||||
});
|
||||
|
||||
/* Collect port names from board.json if no builtin ports */
|
||||
if (allPorts.size === 0 && board.network) {
|
||||
['lan', 'wan'].forEach((role) => {
|
||||
if (board.network[role]) {
|
||||
if (Array.isArray(board.network[role].ports))
|
||||
board.network[role].ports.forEach((p) => allPorts.add(p));
|
||||
else if (board.network[role].device)
|
||||
allPorts.add(board.network[role].device);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* Query PSE status from netifd for all known ports */
|
||||
const psePromises = Array.from(allPorts).map((devname) => {
|
||||
return L.resolveDefault(callNetworkDeviceStatus(devname), {}).then((status) => {
|
||||
return { name: devname, pse: status.pse || null };
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(psePromises).then((pseResults) => {
|
||||
const pseMap = {};
|
||||
pseResults.forEach((r) => {
|
||||
if (r.pse)
|
||||
pseMap[r.name] = r.pse;
|
||||
});
|
||||
data.push(pseMap);
|
||||
return data;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
if (L.hasSystemFeature('swconfig'))
|
||||
return null;
|
||||
|
||||
var board = JSON.parse(data[1]),
|
||||
known_ports = [],
|
||||
port_map = buildInterfaceMapping(data[2], data[3]);
|
||||
const board = JSON.parse(data[1]),
|
||||
port_map = buildInterfaceMapping(data[2], data[3]),
|
||||
pseMap = data[5] || {};
|
||||
let known_ports = [];
|
||||
|
||||
if (Array.isArray(data[0]) && data[0].length > 0) {
|
||||
known_ports = data[0].map(port => ({
|
||||
@@ -354,16 +464,40 @@ return baseclass.extend({
|
||||
});
|
||||
|
||||
return E('div', { 'style': 'display:grid;grid-template-columns:repeat(auto-fit, minmax(70px, 1fr));margin-bottom:1em' }, known_ports.map(function(port) {
|
||||
var speed = port.netdev.getSpeed(),
|
||||
duplex = port.netdev.getDuplex(),
|
||||
carrier = port.netdev.getCarrier(),
|
||||
pmap = port_map[port.netdev.getName()],
|
||||
pzones = (pmap && pmap.zones.length) ? pmap.zones.sort(function(a, b) { return L.naturalCompare(a.getName(), b.getName()) }) : [ null ];
|
||||
const speed = port.netdev.getSpeed();
|
||||
const duplex = port.netdev.getDuplex();
|
||||
const carrier = port.netdev.getCarrier();
|
||||
const pmap = port_map[port.netdev.getName()];
|
||||
const pzones = (pmap && pmap.zones.length) ? pmap.zones.sort((a, b) => L.naturalCompare(a.getName(), b.getName())) : [ null ];
|
||||
const pse = pseMap[port.device];
|
||||
const pseInfo = getPSEStatus(pse);
|
||||
const psePower = formatPSEPower(pse);
|
||||
|
||||
/* Select port icon based on carrier and PSE status */
|
||||
let portIcon;
|
||||
if (pseInfo && pseInfo.isDelivering) {
|
||||
portIcon = carrier ? 'pse_up' : 'pse_down';
|
||||
} else {
|
||||
portIcon = carrier ? 'up' : 'down';
|
||||
}
|
||||
|
||||
const statsContent = [
|
||||
'\u25b2\u202f%1024.1mB'.format(port.netdev.getTXBytes()),
|
||||
E('br'),
|
||||
'\u25bc\u202f%1024.1mB'.format(port.netdev.getRXBytes())
|
||||
];
|
||||
|
||||
if (psePower) {
|
||||
statsContent.push(E('br'));
|
||||
statsContent.push(psePower);
|
||||
}
|
||||
|
||||
statsContent.push(E('span', { 'class': 'cbi-tooltip' }, formatStats(port.netdev, pse)));
|
||||
|
||||
return E('div', { 'class': 'ifacebox', 'style': 'margin:.25em;min-width:70px;max-width:100px' }, [
|
||||
E('div', { 'class': 'ifacebox-head', 'style': 'font-weight:bold' }, [ port.netdev.getName() ]),
|
||||
E('div', { 'class': 'ifacebox-body' }, [
|
||||
E('img', { 'src': L.resource('icons/port_%s.svg').format(carrier ? 'up' : 'down') }),
|
||||
E('img', { 'src': L.resource('icons/port_%s.svg').format(portIcon) }),
|
||||
E('br'),
|
||||
formatSpeed(carrier, speed, duplex)
|
||||
]),
|
||||
@@ -377,12 +511,7 @@ return baseclass.extend({
|
||||
E('span', { 'class': 'cbi-tooltip left' }, [ renderNetworksTooltip(pmap) ])
|
||||
]),
|
||||
E('div', { 'class': 'ifacebox-body' }, [
|
||||
E('div', { 'class': 'cbi-tooltip-container', 'style': 'text-align:left;font-size:80%' }, [
|
||||
'\u25b2\u202f%1024.1mB'.format(port.netdev.getTXBytes()),
|
||||
E('br'),
|
||||
'\u25bc\u202f%1024.1mB'.format(port.netdev.getRXBytes()),
|
||||
E('span', { 'class': 'cbi-tooltip' }, formatStats(port.netdev))
|
||||
]),
|
||||
E('div', { 'class': 'cbi-tooltip-container', 'style': 'text-align:left;font-size:80%' }, statsContent)
|
||||
])
|
||||
]);
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user