Hi all,
starting from the "old" post https://forum.openwrt.org/t/dumb-ap-associated-stations-resolver/153358/69 me & AI managed to get the custom menu Associated Stations working satisfactorily as expected.
Here follows what I did:
-
Installed
luci-lua-runtime -
Created the
/pr_home/associated_stations.shscript:
associated_stations.sh
#!/bin/sh
# Script: /pr_home/associated_stations.sh
# Purpose: Generate raw HTML content (styles and table data) for LuCI to capture and display.
# Ref. https://forum.openwrt.org/t/dumb-ap-associated-stations-resolver/153358/69
# It runs onboard two dumb AP, so the script uses /etc/hosts and /etc/ethers to get IP and MAC
TIMESTAMP=$(date '+%d/%m/%Y - %H:%M:%S')
# Establish who I am
ROUTER=$(uci get system.@system[0].hostname)
# ****** Generate the HTML header and CSS block (captured as output) ******
cat << EOF
<style>
/* Embed all necessary CSS directly */
.wrapper {
font-family: Arial, sans-serif;
margin: 0px;
}
h1 {
font-size: 1.8em;
text-align: center;
margin-bottom: 20px;
}
.timestamp {
text-align: center;
font-size: 0.9em;
color: #666;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
font-size: 0.9em;
}
th {
background-color: #f2f2f2;
}
.perfect-signal {
color: green;
}
.good-signal {
color: blue;
}
.medium-signal {
color: orange;
}
.unstable-connection {
color: purple;
}
.unlikely-connection {
color: red;
}
.unknown {
color: #999;
font-style: italic;
}
/* Responsive table */
@media (max-width: 768px) {
table {
width: 100%;
border: none;
font-size: 0.8em;
}
th, td {
padding: 8px;
text-align: left;
display: block;
width: 100%;
}
td {
border-top: 1px solid #ddd;
}
th {
display: none;
}
tr {
margin-bottom: 15px;
display: block;
background-color: #fff;
padding: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
td:before {
content: attr(data-label);
font-weight: bold;
display: block;
margin-bottom: 5px;
}
}
</style>
<div class="wrapper">
<h1>$ROUTER's Associated Stations and IPv4 Neighbours</h1>
<p class="timestamp">Last Updated: $TIMESTAMP</p>
<center><p> Signal Strength:
<span class='perfect-signal'>Perfect signal: green </span> -
<span class='good-signal'>Good signal: blue </span> -
<span class='medium-signal'>Medium signal: orange </span> -
<span class='unstable-connection'>Unstable connection: purple </span> -
<span class='unlikely-connection'>Unlikely connection: red </span>
</p></center>
<center><p> Status:
<span class='perfect-signal'>REACHABLE: the neighbour entry is valid until the reachability timeout expires</span> -
<span class='good-signal'>STALE: the neighbour entry is valid but considered suspicious; this state doesn't change the neighbour</span> -
<span class='unlikely-connection'>FAILED: the maximum number of probes has been exceeded without success; neighbour validation has ultimately failed </span>
</p></center>
<table>
<thead>
<tr>
<th>Device</th>
<th>Freq or Speed</th>
<th>MAC Address</th>
<th>Host</th>
<th>IP Address</th>
<th>Signal Strength or Status</th>
<th>RX MBit/s</th>
<th>TX MBit/s</th>
</tr>
</thead>
<tbody>
EOF
#****** Get the list of wireless interfaces ******
for DEVICE in $(iwinfo | grep ESSID | cut -d' ' -f1); do
# Get clients for this interface
iwinfo $DEVICE assoclist | while read -r line; do
if echo "$line" | grep -q "dBm"; then
case $ROUTER in
Alfa )
if [ $DEVICE == "phy1-ap0" ]; then
FREQ="2.4 GHz"
else
FREQ="5 GHz"
fi;;
Bravo )
if [ $DEVICE == "phy1-ap0" ]; then
FREQ="5 GHz"
else
FREQ="2.4 GHz"
fi;;
* )
# Other interfaces
exit 0;;
esac
# Extract MAC and signal strength
MAC=$(echo "$line" | awk '{print $1}')
SIGNAL=$(echo $line | awk '{print $2}')
# Get hostname and IP from ethers and hosts
HOSTNAME=$(grep -i "$MAC" /etc/ethers | awk '{print $2}')
# Skip to the next two lines to extract RX and TX
read -r line_rx
RX=$(echo "$line_rx" | grep -o 'RX: [0-9.]\+' | awk '{print $2}')
read -r line_tx
TX=$(echo "$line_tx" | grep -o 'TX: [0-9.]\+' | awk '{print $2}')
if [ ! -z "$HOSTNAME" ]; then
IP=$(grep -iE "$HOSTNAME"$ /etc/hosts | awk '{print $1}')
else
IP="Unknown"
HOSTNAME=$(echo "$MAC" | sed 's/://g')
fi
# Format signal strength with color coding
if [ ! -z "$SIGNAL" ]; then
if [ $SIGNAL -ge -30 ]; then
SIGNAL_DISPLAY="<span class='perfect-signal'>${SIGNAL} dBm</span>"
elif [ $SIGNAL -ge -50 ]; then
SIGNAL_DISPLAY="<span class='good-signal'>${SIGNAL} dBm</span>"
elif [ $SIGNAL -ge -70 ]; then
SIGNAL_DISPLAY="<span class='medium-signal'>${SIGNAL} dBm</span>"
elif [ $SIGNAL -ge -80 ]; then
SIGNAL_DISPLAY="<span class='unstable-connection'>${SIGNAL} dBm</span>"
else
SIGNAL_DISPLAY="<span class='unlikely-connection'>${SIGNAL} dBm</span>"
fi
else
SIGNAL_DISPLAY="<span class='unknown'>No Signal</span>"
fi
# Add row to the HTML table with data-labels for mobile view (no redirection)
echo "<tr><td data-label='Device'>$DEVICE</td><td data-label='Freq or Speed'>$FREQ</td><td data-label='MAC Address'>$MAC</td><td data-label='Host'>$HOSTNAME</td><td data-label='IP Address'>$IP</td><td data-label='Signal Strength or Status'>$SIGNAL_DISPLAY</td><td data-label='RX'>$RX</td><td data-label='TX'>$TX</td></tr>"
fi
done
done
#****** Get the IPv4 neighbours for br-lan interface scanning nud states for 3 states: reachable, stale, failed ******
DEVICE="br-lan"
ip neigh show dev br-lan nud reachable | while read -r line; do
SPEED=$(cat /sys/class/net/$DEVICE/speed)
# Extract STATUS
STATUS=$(echo "$line" | awk '{print $10}')
# Extract IP
IP=$(echo "$line" | awk '{print $1}')
# Extract MAC
MAC=$(echo "$line" | awk '{print $3}')
# Get hostname from ethers
HOSTNAME=$(grep -i "$MAC" /etc/ethers | awk '{print $2}')
STATUS_DISPLAY="<span class='perfect-signal'>${STATUS}</span>"
# Add row to the HTML table with data-labels for mobile view (no redirection)
echo "<tr><td data-label='Device'>$DEVICE</td><td data-label='Freq or Speed'>$SPEED</td><td data-label='MAC Address'>$MAC</td><td data-label='Host'>$HOSTNAME</td><td data-label='IP Address'>$IP</td><td data-label='Signal Strength or Status'>$STATUS_DISPLAY</td><td data-label='RX'>-</td><td data-label='TX'>-</td></tr>"
done
ip neigh show dev br-lan nud stale | while read -r line; do
SPEED=$(cat /sys/class/net/$DEVICE/speed)
# Extract STATUS
STATUS=$(echo "$line" | awk '{print $8}')
# Extract IP
IP=$(echo "$line" | awk '{print $1}')
# Extract MAC
MAC=$(echo "$line" | awk '{print $3}')
# Get hostname from ethers
HOSTNAME=$(grep -i "$MAC" /etc/ethers | awk '{print $2}')
STATUS_DISPLAY="<span class='good-signal'>${STATUS}</span>"
echo "<tr><td data-label='Device'>br-lan</td><td data-label='Freq or Speed'>$SPEED</td><td data-label='MAC Address'>$MAC</td><td data-label='Host'>$HOSTNAME</td><td data-label='IP Address'>$IP</td><td data-label='Signal Strength or Status'>$STATUS_DISPLAY</td><td data-label='RX'>-</td><td data-label='TX'>-</td></tr>"
done
ip neigh show dev br-lan nud failed | while read -r line; do
SPEED=$(cat /sys/class/net/$DEVICE/speed)
# Extract STATUS
STATUS=$(echo "$line" | awk '{print $6}')
# Extract IP
IP=$(echo "$line" | awk '{print $1}')
# Extract MAC
MAC=$(echo "$line" | awk '{print $3}')
# Get hostname from hosts
HOSTNAME=$(grep -E "^$IP\s" /etc/hosts | awk '{print $2}')
STATUS_DISPLAY="<span class='unlikely-connection'>${STATUS}</span>"
echo "<tr><td data-label='Device'>br-lan</td><td data-label='Freq or Speed'>$SPEED</td><td data-label='MAC Address'>$MAC</td><td data-label='Host'>$HOSTNAME</td><td data-label='IP Address'>$IP</td><td data-label='Signal Strength or Status'>$STATUS_DISPLAY</td><td data-label='RX'>-</td><td data-label='TX'>-</td></tr>"
done
#****** Generate the HTML footer to close the table and wrapper div ******
cat << EOF
</tbody>
</table>
</div>
EOF
- Created the
/usr/lib/lua/luci/controller/associated_stations.luamenu entry:
associated_stations.lua
module("luci.controller.associated_stations", package.seeall)
function index()
-- Registers the top-level menu entry: /admin/associated_stations
-- Using 'call' forces the execution of 'action_show', which registers the page reliably.
entry({"admin", "associated_stations"}, call("action_show"), "Associated Stations", 70)
end
function action_show()
-- Set no-cache headers to ensure data is always fresh
luci.http.header("Cache-Control", "no-cache, no-store, must-revalidate")
luci.http.header("Expires", "0")
-- Renders the view template where the script execution takes place.
luci.template.render("associated_stations/status")
end
- Created the /usr/lib/lua/luci/view/associated_stations directory to store the status.htm file:
status.htm
-- File /usr/lib/lua/luci/view/associated_stations/status.htm
<%+header%>
<%
-- 1. Execute the shell script and capture its raw HTML output
local script_output = luci.sys.exec("/pr_home/associated_stations.sh")
-- 2. Print the output (raw HTML) without escaping the content
luci.http.write(script_output)
%>
<%+footer%>
As previously stated, the above works very well.
Unfortunately, we (me & AI) failed to migrate it from luci-lua-runtime to the newer JSON framework: can you help me to do that?
Thanks in advance.