luci-base: poe and PSE details & configuration
authorCarlo Szelinsky <github@szelinsky.de>
Sun, 25 Jan 2026 11:40:49 +0000 (12:40 +0100)
committerPaul Donald <newtwen+github@gmail.com>
Mon, 26 Jan 2026 02:40:38 +0000 (03:40 +0100)
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>
modules/luci-base/htdocs/luci-static/resources/icons/port_pse_down.svg [new file with mode: 0644]
modules/luci-base/htdocs/luci-static/resources/icons/port_pse_up.svg [new file with mode: 0644]
modules/luci-base/htdocs/luci-static/resources/network.js
modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json
modules/luci-mod-network/htdocs/luci-static/resources/tools/network.js
modules/luci-mod-network/htdocs/luci-static/resources/view/network/interfaces.js
modules/luci-mod-status/htdocs/luci-static/resources/view/status/include/29_ports.js

diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/port_pse_down.svg b/modules/luci-base/htdocs/luci-static/resources/icons/port_pse_down.svg
new file mode 100644 (file)
index 0000000..81b9c3c
--- /dev/null
@@ -0,0 +1 @@
+<svg width="32" height="32" version="1.1" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><linearGradient id="linearGradient7887" x1="-7.975" x2="-11.01" y1="25.36" y2="-6.568" gradientTransform="matrix(2 0 0 1.933 43.5 .5333)" gradientUnits="userSpaceOnUse"><stop stop-color="#d3d7cf" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient><linearGradient id="linearGradient7889" x1="-7.852" x2="-5.51" y1="3.755" y2="18.94" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#fff" stop-opacity="0" offset="1"/></linearGradient><filter id="filter5386" x="-.01972" y="-.02176" width="1.039" height="1.044" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="0.26293105"/></filter><linearGradient id="linearGradient7907" x1="32.81" x2="32.81" y1="21" y2="26.2" gradientTransform="matrix(1.631 0 0 1.63 98.35 78.44)" gradientUnits="userSpaceOnUse"><stop stop-color="#2e3436" offset="0"/><stop stop-color="#555753" offset="1"/></linearGradient><linearGradient id="linearGradient7909" x1="28.88" x2="29" y1="29" y2="16" gradientTransform="matrix(1.5 0 0 1.5 102.2 81.36)" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#fff" stop-opacity="0" offset="1"/></linearGradient><linearGradient id="linearGradient7919" x1="30.31" x2="30.31" y1="27.31" y2="24.69" gradientUnits="userSpaceOnUse"><stop stop-color="#edd400" offset="0"/><stop stop-color="#edd400" stop-opacity="0" offset="1"/></linearGradient></defs><clipPath id="b"><rect x="68.78" y="-.0933" width="58.26" height="58.26" rx="2.648"/></clipPath><filter id="c" x="-.021" y="-.021" width="1.042" height="1.042" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation=".28282197"/></filter><clipPath id="h"><path d="m-6.562 24.53c-0.3877 0-0.6917 0.219-0.9062 0.469l-0.031-0.03-0.031 0.06-0.3438 0.313-4.25 1.218c-0.03 0-0.06 0.02-0.09 0.03-0.549 0.274-0.902 0.554-1.094 0.937-0.192 0.384-0.116 0.867 0.125 1.156 0.329 0.394 0.858 0.553 1.407 0.657l-0.407 0.437c-0.06 0.05-0.1 0.116-0.125 0.188-0.03 0.121-0.07 0.316-0.125 0.5-0.02 0.04-0.03 0.08-0.03 0.125v6.656c0 0.299 0.113 0.582 0.313 0.813 0.199 0.23 0.511 0.406 0.875 0.406h7.843c0.3236 0 0.6569-0.116 0.9063-0.375l4.651-4.81c0.2259-0.235 0.3437-0.539 0.3437-0.875v-6.625c0-0.335-0.1178-0.64-0.3437-0.875-0.2227-0.229-0.5504-0.375-0.874-0.375z"/></clipPath><filter id="i" x="-.03844" y="-.04352" width="1.077" height="1.087" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation=".1491362"/></filter><filter id="p" x="-.02026" y="-.02354" width="1.04" height="1.047" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation=".071069152"/></filter><filter id="x" x="-.03625" y="-.04222" width="1.071" height="1.083" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation=".16679387"/></filter><clipPath id="A"><path d="m-95.3 143.8s-4.189 3.27-8.1 6.5c-1.95 1.62-3.69 3.22-4.72 4.78-0.51 0.78-0.98 1.62-0.65 2.78 0.16 0.58 0.61 1.12 1.09 1.41 0.48 0.28 0.96 0.4 1.5 0.47 2.13 0.24 4.73-0.4 7.849-1.28 3.113-0.89 6.669-2.09 10.25-3.25 3.581-1.17 7.196-2.31 10.34-3 3.147-0.7 5.835-0.89 7.375-0.5 0.729 0.18 0.745 0.3 0.75 0.31 0 0 0.06 0.33-0.219 0.94-0.567 1.2-2.245 3.11-4.187 4.93-3.885 3.64-8.844 7.13-8.844 7.13l1.75 2.44s5.041-3.52 9.156-7.38c2.058-1.93 3.879-3.89 4.813-5.87 0.466-1 0.776-2.12 0.343-3.25-0.433-1.14-1.541-1.87-2.812-2.19-2.46-0.62-5.444-0.21-8.781 0.53-3.337 0.73-7.019 1.85-10.62 3.03-3.607 1.18-7.129 2.4-10.12 3.25-2.913 0.83-5.293 1.22-6.473 1.13 0.62-0.95 2.3-2.56 4.16-4.1 3.721-3.08 7.969-6.37 7.969-6.37z"/></clipPath><mask id="B" maskUnits="userSpaceOnUse"><rect x="-115.6" y="147" width="51.28" height="17.34" rx=".6657" fill="url(#C)" fill-rule="evenodd" stroke-width="2"/></mask><linearGradient id="C" x1="-112.3" x2="-112.3" y1="148.9" y2="164.5" gradientUnits="userSpaceOnUse"><stop offset="0"/><stop stop-color="#fff" offset=".2177"/><stop stop-color="#fff" offset=".7659"/><stop offset="1"/></linearGradient><filter id="D" x="-.0125" y="-.04971" width="1.026" height="1.099" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="0.001 0.244"/></filter><filter id="H" x="-.395" y="-.5737" width="1.79" height="2.147" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="2.8946329"/></filter><g opacity=".7"><g transform="matrix(1.297 0 0 1.297 -13.6 -.805)" enable-background="new"><rect x="14.5" y="1.5" width="32" height="29" rx="2.877" ry="2.781" fill="url(#linearGradient7887)" fill-rule="evenodd" stroke="#888a85" stroke-dashoffset=".7" stroke-linecap="round" stroke-linejoin="round" stroke-opacity=".9924"/><path transform="matrix(2.143 0 0 2.087 44.43 -.6302)" d="m-12.5 1.498c-0.573 0-1.002 0.429-1.002 1.002v11c0 0.573 0.429 1.002 1.002 1.002h12c0.573 0 1.002-0.429 1.002-1.002v-11c0-0.573-0.429-1.002-1.002-1.002z" fill="none" stroke="url(#linearGradient7889)" stroke-dashoffset=".7" stroke-linecap="round" stroke-linejoin="round" stroke-opacity=".9924" stroke-width=".4729"/><rect transform="matrix(1.016 0 0 1.016 -.49 -.2715)" x="15.38" y="2.875" width="32" height="29" rx="3.723" ry="3.723" fill-rule="evenodd" filter="url(#filter5386)" opacity=".1205"/></g><g transform="translate(-120.7 -96.27)"><path d="m144.9 107c-0.9585 0-1.733 0.7739-1.733 1.731v0.4317c0 0.4398-0.344 0.8148-0.8156 0.8148h-2.345c-0.9585 0-1.733 1.055-1.733 2.013v9.667c0 0.9575 0.7747 1.731 1.733 1.731h12.85c0.9585 0 1.733-0.7739 1.733-1.731v-9.667c0-0.9575-0.7747-2.013-1.733-2.013h-2.345c-0.3927 0.0265-0.8156-0.2794-0.8156-0.7893v-0.4571c0-0.9575-0.7747-1.731-1.733-1.731z" fill="url(#linearGradient7907)" fill-rule="evenodd" stroke-width="1.5"/><path d="m145.1 106c-1.664 0-2.977 1.362-3.094 3h-1.406c-1.739 0-3.188 1.449-3.188 3.188v8.812c0 1.739 1.449 3.188 3.188 3.188h11.81c1.739 0 3.188-1.449 3.188-3.188v-8.812c0-1.739-1.449-3.188-3.188-3.188h-1.406c-0.1167-1.638-1.43-3-3.094-3z" fill="none" stroke="url(#linearGradient7909)" stroke-dashoffset=".7" stroke-linecap="round" stroke-width="1.5"/><g transform="matrix(1.5 0 0 1.5 102.2 79.86)" fill="none" stroke="url(#linearGradient7919)" stroke-linecap="round" stroke-width="1px"><path d="m26.5 26.5v-2"/><path d="m28.5 26.5v-2"/><path d="m30.5 26.5v-2"/><path d="m32.5 26.5v-2"/></g></g></g><!-- POE indicator: red lightning bolt --><g transform="translate(33, 5)"><path d="M8 0 L4 7 L7 7 L5 14 L12 5 L8 5 L11 0 Z" fill="#d06447" stroke="#a04030" stroke-width="0.8" stroke-linejoin="round"/></g></svg>
diff --git a/modules/luci-base/htdocs/luci-static/resources/icons/port_pse_up.svg b/modules/luci-base/htdocs/luci-static/resources/icons/port_pse_up.svg
new file mode 100644 (file)
index 0000000..05201ab
--- /dev/null
@@ -0,0 +1 @@
+<svg width="32" height="32" version="1.1" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><linearGradient id="linearGradient7887" x1="-7.975" x2="-11.01" y1="25.36" y2="-6.568" gradientTransform="matrix(2 0 0 1.933 43.5 .5333)" gradientUnits="userSpaceOnUse"><stop stop-color="#d3d7cf" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient><linearGradient id="linearGradient7889" x1="-7.852" x2="-5.51" y1="3.755" y2="18.94" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#fff" stop-opacity="0" offset="1"/></linearGradient><filter id="filter5386" x="-.01972" y="-.02176" width="1.039" height="1.044" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="0.26293105"/></filter><linearGradient id="linearGradient9" x1="25.05" x2="27.76" y1="16.17" y2="24.54" gradientTransform="matrix(1.088 0 0 1.086 294.4 40.06)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient3983"/><linearGradient id="linearGradient3983"><stop stop-color="#555753" offset="0"/><stop stop-color="#888a85" offset="1"/></linearGradient><linearGradient id="linearGradient11" x1="326.5" x2="326.5" y1="61.7" y2="60.69" gradientUnits="userSpaceOnUse"><stop stop-color="#babdb6" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient><radialGradient id="radialGradient4612-0" cx="306.5" cy="86.38" r="21.91" gradientTransform="matrix(2.238 -.5404 .1485 .615 -392 204.4)" gradientUnits="userSpaceOnUse"><stop offset="0"/><stop stop-opacity="0" offset="1"/></radialGradient><filter id="filter4618-7" x="-.07545" y="-.06204" width="1.151" height="1.143"><feGaussianBlur stdDeviation="0.89652215"/></filter><linearGradient id="linearGradient12" x1="322.3" x2="328.4" y1="62.62" y2="68.74" gradientTransform="matrix(.75 0 0 .8333 81.62 10.42)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient4206"/><linearGradient id="linearGradient4206"><stop stop-color="#fff" offset="0"/><stop stop-color="#babdb6" offset="1"/></linearGradient><linearGradient id="linearGradient13" x1="325.4" x2="328.4" y1="63.27" y2="68.74" gradientTransform="matrix(.5 0 0 .5 -489.8 -97.75)" gradientUnits="userSpaceOnUse" xlink:href="#linearGradient4206"/><linearGradient id="linearGradient14" x1="-47.16" x2="-46.15" y1="39.38" y2="44.17" gradientTransform="translate(386,50)" gradientUnits="userSpaceOnUse"><stop stop-color="#a04030" offset="0"/><stop stop-color="#a04030" stop-opacity="0" offset="1"/></linearGradient><linearGradient id="linearGradient15" x1="-48" x2="-46.75" y1="40.38" y2="45.69" gradientTransform="translate(386,50)" gradientUnits="userSpaceOnUse"><stop stop-color="#d06447" offset="0"/><stop stop-color="#d06447" stop-opacity="0" offset="1"/></linearGradient></defs><g transform="matrix(1.297 0 0 1.297 -13.6 -.805)" enable-background="new"><g enable-background="new"><rect x="14.5" y="1.5" width="32" height="29" rx="2.877" ry="2.781" fill="url(#linearGradient7887)" fill-rule="evenodd" stroke="#888a85" stroke-dashoffset=".7" stroke-linecap="round" stroke-linejoin="round" stroke-opacity=".9924"/><path transform="matrix(2.143 0 0 2.087 44.43 -.6302)" d="m-12.5 1.498c-0.573 0-1.002 0.429-1.002 1.002v11c0 0.573 0.429 1.002 1.002 1.002h12c0.573 0 1.002-0.429 1.002-1.002v-11c0-0.573-0.429-1.002-1.002-1.002z" fill="none" stroke="url(#linearGradient7889)" stroke-dashoffset=".7" stroke-linecap="round" stroke-linejoin="round" stroke-opacity=".9924" stroke-width=".4729"/><rect transform="matrix(1.016 0 0 1.016 -.49 -.2715)" x="15.38" y="2.875" width="32" height="29" rx="3.723" ry="3.723" fill-rule="evenodd" filter="url(#filter5386)" opacity=".1205"/></g></g><g transform="matrix(1.785 0 0 1.785 -557 -94.28)" enable-background="new"><path d="m325 59.07c-0.64 0-1.16 0.516-1.16 1.154v0.288c0 0.293-0.23 0.543-0.54 0.543h-0.12c-0.64 0-1.16 0.704-1.16 1.342v5.445c0 0.638 0.52 1.154 1.16 1.154h6.56c0.64 0 1.16-0.516 1.16-1.154v-5.445c0-0.638-0.52-1.342-1.16-1.342h-0.14c-0.26 0.02-0.55-0.186-0.55-0.526v-0.305c0-0.638-0.51-1.154-1.15-1.154z" fill="url(#linearGradient9)" fill-rule="evenodd" stroke="#fff" stroke-width="2"/><path d="m325 59.07c-0.64 0-1.16 0.516-1.16 1.154v0.288c0 0.293-0.23 0.543-0.54 0.543h-0.12c-0.64 0-1.16 0.704-1.16 1.342v5.445c0 0.638 0.52 1.154 1.16 1.154h6.56c0.64 0 1.16-0.516 1.16-1.154v-5.445c0-0.638-0.52-1.342-1.16-1.342h-0.14c-0.26 0.02-0.55-0.186-0.55-0.526v-0.305c0-0.638-0.51-1.154-1.15-1.154z" fill="url(#linearGradient9)" fill-rule="evenodd"/><rect x="325" y="60" width="3" height="2" rx=".3978" ry=".3978" color="#000000" fill="url(#linearGradient11)"/><rect x="323.5" y="62.5" width="6" height="5" rx=".1768" ry=".1768" color="#000000" fill="#d3d7cf" stroke="url(#linearGradient12)"/><rect transform="scale(-1)" x="-328.5" y="-66.5" width="4" height="3" rx=".1768" ry=".1768" color="#000000" fill="#888a85" stroke="url(#linearGradient13)"/><path d="m325 63.81c-1.03 8.083-9.29 14.95-16.38 19.75-3.54 2.401-6.67 4.247-8.53 5.718-0.46 0.368-0.85 0.712-1.18 1.125-0.34 0.414-0.74 1.02-0.57 1.907 0.17 0.886 0.92 1.362 1.47 1.562 0.56 0.2 1.11 0.299 1.81 0.344 5.52 0.347 13.01-2.019 20.25-4.094 7.25-2.075 14.34-3.76 17.5-2.969 0.73 0.182 0.75 0.332 0.75 0.344 0 0.01 0 0.304-0.25 0.906-0.56 1.204-2.21 3.148-4.15 4.969-3.89 3.642-8.85 7.095-8.85 7.095l1.72 2.47s5.04-3.519 9.16-7.377c2.06-1.93 3.91-3.892 4.84-5.875 0.47-0.992 0.75-2.114 0.32-3.25-0.44-1.137-1.55-1.87-2.82-2.188-4.83-1.209-11.76 0.918-19.03 3-6.88 1.972-13.91 3.965-18.4 3.938 1.62-1.195 4.33-2.874 7.65-5.125 7.26-4.922 16.44-12.08 17.69-21.88z" color="#000000" fill="url(#linearGradient14)" fill-rule="evenodd" stroke="url(#linearGradient15)" stroke-linejoin="round"/><path transform="scale(-1)" d="m-324.5-64c0 0.1-0.1 0.501-0.17 0.501h-3.64c-0.1 0-0.18-0.08-0.18-0.177v-2.646c0-0.1 0.42-0.177 0.5-0.177" color="#000000" fill="none" stroke="url(#linearGradient13)"/></g><!-- POE indicator: red lightning bolt --><g transform="translate(33, 5)"><path d="M8 0 L4 7 L7 7 L5 14 L12 5 L8 5 L11 0 Z" fill="#d06447" stroke="#a04030" stroke-width="0.8" stroke-linejoin="round"/></g></svg>
index a6def061e4ef7243d6bafd1e92c091947b0f744a..5799ae297343db3e3fd0249b0b630650ad42a019 100644 (file)
@@ -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.
         *
index f24d97cf1ba2e13579b9913aee3d0224c1b40c0f..cc7f06b3f07f58221dfdaddf373ceec13984b84b 100644 (file)
@@ -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" ]
index d106e7cdd0b28c61b108d11422a0f1f92c5dbe87..5dd487bdef06ade0091e68024dc20b246d877e74 100644 (file)
@@ -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;
 
index fe5360f6b82bc6ab1ec91a8022790a9716641a67..d2203e4980a46fe0b952d6d29a3812fd31302fd1 100644 (file)
@@ -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);
-
-                       nettools.addDeviceOptions(s, dev, isNew, rtTables);
+                       const isNew = (uci.get('network', s.section, 'name') == null),
+                             dev = getDevice(s.section),
+                             devName = dev ? dev.getName() : null;
+
+                       /* 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 /*, ... */) {
index 905f1f36ddbc25ac2c4815284d81e86def0ff925..d08b84d1e08b6b661e6be6fd3fa010fba83ee80c 100644 (file)
@@ -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;
+
+       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),
@@ -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)
                                ])
                        ]);
                }));