5.4 KiB
n8n webhook: get_signal_airos (wireless Do Stuff)
This is the one remaining hop to light up the wireless (airOS / Cambium) "Do Stuff" panel in OPS. Everything on the OPS side is built and deployed:
- Hub reader/endpoint:
GET /collab/airos-signal?serial=<sn>[&ip=&port=&user=](ticket-collab.airosSignal). Resolves the CPE mgmt IP + login fromService Equipmentand callshttps://n8napi.targo.ca/webhook/get_signal_airos. - UI:
EquipmentDetail.vueshows "Do Stuff en direct (sans-fil)" for wireless equipment and renders the fields below. Until this webhook exists it degrades gracefully ("signal sans-fil en attente : webhook get_signal_airos à configurer").
Why n8n and not the hub directly? Verified empirically: the hub cannot route to
the CPE management network (100.x CGNAT + 10.150/10.151.x) — every probe timed
out. n8n sits where it can reach the field gear (same reason fiber TP-Link Do Stuff
uses /webhook/dostuff). Keeping the fetch in n8n also keeps the AP password out of
OPS/the hub.
This is a straight port of F's device_ajax/airos_ac_ajax.php (statuscgi branch).
Contract
Request — GET /webhook/get_signal_airos, JSON body:
{ "sn": "<serial>", "ip": "<mgmt ip>", "port": 2196, "user": "admin" }
ip/port/user are passed by the hub (resolved from Service Equipment.ip_address
/ login_user). The password must live in n8n (credential/env) — F's fallback is
admin / N0HAk4u$.
Response — JSON object (or 1-element array). The hub reads these keys (snake_case as F emits them; camelCase also accepted):
| key | source in status.cgi |
|---|---|
device_model |
host.devmodel |
device_name |
host.hostname |
version |
host.fwversion |
ssid |
wireless.essid |
frequency |
wireless.frequency |
tx_power |
wireless.txpower |
mac_ap |
wireless.apmac |
signal |
wireless.sta[0].signal (dBm) |
signal_ap |
wireless.sta[0].remote.signal (dBm) |
ccq |
wireless.sta[0].ccq (or wireless.ccq) |
tx_rate |
idx[wireless.tx_idx] |
rx_rate |
idx[wireless.rx_idx] |
airmax_capacity_uplink |
round(wireless.sta[0].airmax.uplink_capacity / 1024, 2) (Mbps) |
airmax_capacity_downlink |
round(wireless.sta[0].airmax.downlink_capacity / 1024, 2) (Mbps) |
connection_time |
secondsToTime(host.uptime) |
if_speed_lan0 / if_speed_lan1 |
interfaces[ifname=eth0/eth1].status.speed + duplex |
where idx = ['1x','2x','2x','4x','4x','6x','6x','8x','8x'] (airMAX MCS multiplier).
On failure return { "error": "<reason>" } (the hub surfaces it in the panel).
Workflow (4 nodes)
-
Webhook — method
GET, pathget_signal_airos, "Respond: Using Respond to Webhook node". -
HTTP Request — airOS auth (airOS ≥ 8.5, fallback < 8.5)
POST https://{{$json.body.ip}}:{{$json.body.port}}/api/auth- body (form-urlencoded):
username={{user}},password={{$credentials}} - Options: ignore SSL issues = on, full response = on (need response
headers to capture
X-CSRF-IDand the session cookie). - If status ≠ 200 → fall back to
POST /login.cgi(same fields).
-
HTTP Request — status.cgi
GET https://{{ip}}:{{port}}/status.cgi- headers:
Cookiefrom node 2, plusX-CSRF-ID: {{...}}when the ≥8.5 path was used. - ignore SSL issues = on. Response format = JSON.
-
Function — normalize → then Respond to Webhook:
const s = items[0].json; // status.cgi
const idx = ['1x','2x','2x','4x','4x','6x','6x','8x','8x'];
const sec2t = (n) => { if(!n||n<=0) return ''; const d=Math.floor(n/86400),h=Math.floor(n%86400/3600),m=Math.floor(n%3600/60),ss=Math.floor(n%60); return `${d}d ${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(ss).padStart(2,'0')}`; };
const w = s.wireless||{}, sta = (w.sta&&w.sta[0])||{}, host = s.host||{};
const iface = (n) => (s.interfaces||[]).find(i=>i.ifname===n)||{};
const eth0 = iface('eth0').status||{}, eth1 = iface('eth1').status||{};
return [{ json: {
device_model: host.devmodel, device_name: host.hostname, version: host.fwversion,
ssid: w.essid, frequency: w.frequency, tx_power: w.txpower, mac_ap: w.apmac,
signal: sta.signal, signal_ap: (sta.remote||{}).signal, ccq: sta.ccq ?? w.ccq,
tx_rate: idx[w.tx_idx], rx_rate: idx[w.rx_idx],
airmax_capacity_uplink: Math.round(((sta.airmax||{}).uplink_capacity||0)/1024*100)/100,
airmax_capacity_downlink: Math.round(((sta.airmax||{}).downlink_capacity||0)/1024*100)/100,
connection_time: sec2t(host.uptime),
if_speed_lan0: eth0.plugged ? `${eth0.speed} Mbps` : 'unplugged',
if_speed_lan1: eth1.plugged ? `${eth1.speed} Mbps` : 'unplugged',
}}];
(Optional) call GET /logout.cgi (with X-AIROS-LUA: 1 + X-CSRF-ID) after node 3
to release the session, as F does.
Cambium
Cambium CPEs use a different API (device/cambium.php in F). Handle as a branch
in the same workflow keyed on the caller's device brand/category (add a brand/
category field to the hub call), or a separate get_signal_cambium webhook. Not
required for the airOS/airMAX park (the majority).
Test
curl -s -X GET https://n8napi.targo.ca/webhook/get_signal_airos \
-H 'Content-Type: application/json' \
-d '{"ip":"<a-reachable-CPE-ip>","port":2196,"user":"admin"}'
Then in OPS: open a wireless device → Do Stuff en direct (sans-fil) → signal / CCQ / TX-RX / airMAX capacity render. No further OPS changes needed.