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.
*
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;
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") + '">'+
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;
expect: { result: [] }
});
+const callNetworkDeviceStatus = rpc.declare({
+ object: 'network.device',
+ method: 'status',
+ params: [ 'name' ],
+ expect: { '': {} }
+});
+
function isString(v)
{
return typeof(v) === 'string' && v !== '';
return carrier ? _('Connected') : _('no link');
}
-function formatStats(portdev) {
- var stats = portdev._devstate('stats') || {};
+function getPSEStatus(pse) {
+ if (!pse)
+ return null;
+
+ 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;
- return ui.itemlist(E('span'), [
+ 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),
_('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) {
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 => ({
});
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)
]),
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)
])
]);
}));