Initial commit: Hermes Agent Skills collection

This commit is contained in:
Debian
2026-07-12 19:02:59 +00:00
commit e9cc106625
789 changed files with 233126 additions and 0 deletions
@@ -0,0 +1,817 @@
---
name: omada-controller-administration
description: >-
Class-level skill for TP-Link Omada Cloud Controller administration via browser automation.
Covers login flow, SPA navigation quirks, iframe-based site management, VLAN configuration,
ACL rules, inter-VLAN mDNS limitations, WLAN/channel optimization, device management,
and firmware updates. Applies to cloud-based Omada controllers at omada.tplinkcloud.com.
device management, and firmware updates. Applies to cloud-based Omada controllers at omada.tplinkcloud.com.
tags:
- tp-link
- omada
- wlan
- channel-optimization
- network-management
- browser-automation
---
# TP-Link Omada Cloud Controller Administration
## When to Use
Any task involving TP-Link Omada Cloud Controller — WLAN configuration, channel optimization, device management, firmware updates, site settings, or viewing logs/alerts.
## Environment
- **URL**: `https://omada.tplinkcloud.com/`
- **Credentials**: 1Password vault "Hermes", item "omada - Netzwerkverwaltung"
- **Controller name**: "Schön" (Cloud Based System)
- **Site name**: "Rohr"
- **Devices** (all on VLAN 1 / 172.16.1.x management subnet):
- Router: DR3650v-4G HW v1.0, FW 1.1.1, IP 172.16.1.1 (also 10.0.30.1 on VLAN 30)
- Switch: SG3428MP HW v6.20, FW 6.20.26 (update 6.20.27 avail), IP 172.16.1.2
- AP Erdgeschoss: EAP683 UR v1.0, FW 1.4.0, IP 172.16.1.3 (switch port 16)
- AP Obergeschoss: EAP683 UR v1.0, FW 1.4.0, IP 172.16.1.4 (switch port 14)
- AP Garage: EAP615-Wall v1.0, FW 1.5.4, IP 172.16.1.5 (switch port 21)
- AP Garten: EAP225-Outdoor v3.0, FW 5.2.2, IP 172.16.1.6
- **Local router fallback**: `http://172.16.1.1/webpages/login.html` (login blocked when managed by controller — see "Known Issue" section)
- **Local switch UI**: `http://172.16.1.2` (accessible but requires unknown device-specific credentials)
- **Monitoring stack**: CT 141 (proxmox7), Docker-based: prometheus (:9090), grafana (:3000), alertmanager, blackbox, pve-exporter, telegram-bridge (:9099). snmp_exporter runs as systemd binary (:9116) — Docker deployment fails on CT 141.
- **Grafana dashboard**: `network-infra` (uid: `network-infra`) — Proxmox nodes, guests, HA state, blackbox probes. Omada metrics NOT included (no exporter available for cloud controller).
- **Local Omada Controller** (in progress): CT 142 (`omada-controller`, 10.0.30.142/24, VLAN 30, proxmox7, vm_disks storage). Will replace cloud controller to enable SNMP + API exporters.
- **Docker hosts in cluster**: Only CT 141 (6 containers: monitoring stack) and CT 121 (1 container: portainer). All other CTs/VMs run services natively without Docker.
## Login Flow
### Steps
1. Navigate to `https://omada.tplinkcloud.com/#/login`
2. Wait 5-8 seconds — the SPA loads slowly, initial snapshot shows `(empty page)` with 0 elements
3. Take snapshot to confirm login form is visible
4. Type TP-Link ID into the "TP-Link ID (Partner ID)" textbox
5. Type password into the "Password" textbox (nested inside a generic wrapper — use the inner textbox ref, not the outer one)
6. Press **Enter** (do NOT click the "Sign In" link — it redirects to tp-link.com marketing site instead of submitting the form)
7. Wait 5-8 seconds for redirect
8. Dismiss cookie banner if present ("Alle Cookies akzeptieren")
9. Dismiss any notice dialogs ("OK" or "Do not remind again")
### Alternative: Console-Based Login
Instead of `browser_type` + `browser_press`, you can fill the login form via `browser_console`:
```javascript
(async function(){
const sleep = ms => new Promise(r => setTimeout(r, ms));
const inputs = document.querySelectorAll('input');
if (inputs.length >= 2) {
inputs[0].focus();
inputs[0].value = 'dominik@familie-schoen.com';
inputs[0].dispatchEvent(new Event('input', {bubbles: true}));
const pwInputs = document.querySelectorAll('input[type="password"]');
if (pwInputs.length > 0) {
pwInputs[pwInputs.length - 1].focus();
pwInputs[pwInputs.length - 1].value = '<password>';
pwInputs[pwInputs.length - 1].dispatchEvent(new Event('input', {bubbles: true}));
}
}
await sleep(500);
// Submit via Enter keydown event
document.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true}));
// Also click Sign In link as backup
for (const el of document.querySelectorAll('a, button')) {
if (el.textContent.trim() === 'Sign In' && el.tagName === 'A') { el.click(); break; }
}
return 'login submitted';
})()
```
> Note: This approach dispatches React-compatible `input` events. The `browser_type` + `Enter` approach is simpler and equally reliable for the login form.
### Critical Pitfalls
- **Empty page on navigate**: The Omada SPA frequently returns `(empty page)` with `element_count: 0` right after navigation. Always wait 5-8s then re-snapshot before concluding the page failed to load.
- **"Sign In" link vs form submit**: The "Sign In" element (`@e9` in observed session) is a **link**, not a submit button. Clicking it navigates to `https://www.tp-link.com/de/support/`. Use `browser_press` with `Enter` while focused in the password field to submit the form.
- **Password field nesting**: The password input is wrapped in a generic div containing two textboxes — use the one with aria-label "Password" (the inner ref), not the outer wrapper ref.
- **Multiple dismissable dialogs**: After login, expect up to 3 sequential dialogs: cookie consent, regional isolation notice, and firmware/system update announcement. Dismiss each before proceeding.
- **Session expiry / white screen**: After extended interaction (especially after clicking buttons inside the iframe), the SPA may go completely blank/white. The iframe disappears from the DOM entirely (`document.querySelector('iframe')` returns null). Recovery: re-navigate to `https://omada.tplinkcloud.com/#/login` and repeat the full login flow. The session does NOT persist across browser navigations.
- **`browser_console` variable re-declaration**: Repeated `browser_console` calls that declare variables (e.g., `const fr = document.querySelector(...)`) fail with `SyntaxError: Identifier 'xxx' has already been declared` because the execution context persists between calls. Always wrap code in an IIFE: `(function(){ ... })()` to avoid polluting the persistent scope.
- **`browser_click` refs for iframe content**: Refs from `browser_snapshot` for elements inside the iframe (e.g., `@e14` for "Optimize Now") sometimes fail with "Unknown ref". Fall back to `browser_console` with IIFE-scoped DOM queries to click iframe-internal elements directly.
## Navigation: Controller → Site → WLAN
### Structure
```
Omada Cloud Landing
└─ Dropdown: "On Premise Systems" / "Cloud Based Systems" → select "Cloud Based Systems"
└─ Controller card ("Schön") → click to enter
└─ Global View (org overview, site list)
└─ Site "Rohr" → click site name cell
└─ Site Dashboard with left nav:
├─ Management: Dashboard, Devices, Clients
├─ Monitoring: Map, Logs
├─ Configuration: Network Config, Device Config
├─ Hotspot
└─ Maintenance: Network Tools, IntelliRecover
```
### Switching to Cloud Based Systems
The top-left dropdown defaults to "On Premise Systems". Click the combobox, then use `browser_console` to find and click the "Cloud Based Systems" `<li>` inside `.ant-dropdown-menu`.
### Switching Back to a Site from Global View
After performing actions in Global View (e.g., checking org-wide logs), returning to a specific site requires navigating through the Dashboard site list:
1. Click **Dashboard** in the Global View sidebar
2. Find the site name in the Site List table
3. Click the `span.text-link.cursor-pointer` inside the site name cell (NOT the `<td>` itself — it has no click handler)
4. Verify by checking the sidebar: site-level menus (Management, Monitoring, Configuration, Hotspot, Maintenance) should appear instead of Global View menus (Dashboard, Devices, Logs, SD-WAN, Accounts, Settings)
**Pitfall: Combobox Does Not List Sites**
The top-left combobox (showing controller name "Schön") opens a dropdown with controller/system options (e.g., "SchönEssentials") but does **NOT** list individual sites. It cannot be used to switch between sites. Always use the Dashboard → Site List table to navigate to a specific site.
### Entering a Site
Click the site name in the Site List table. **Pitfall**: clicking the `<td>` cell itself does NOT navigate into the site — the cell has `cursor: auto` and no click handler. The actual clickable element is a `span.text-link.cursor-pointer` *inside* the cell. Double-clicking the cell also does not work. Use `browser_console`:
```javascript
(function(){
const fr = document.querySelector('iframe');
if (!fr || !fr.contentDocument) return 'no iframe';
const spans = fr.contentDocument.querySelectorAll('span.text-link, .cursor-pointer');
for (const s of spans) {
if (s.textContent.trim() === 'Rohr') { s.click(); return 'clicked Rohr text-link'; }
}
return 'not found';
})()
```
**Verification**: After clicking, the sidebar should show site-level menus (Management, Monitoring, Configuration, Hotspot, Maintenance) instead of Global View menus (Dashboard, Devices, Logs, SD-WAN, Accounts, Settings). If you still see "SD-WAN" or "Accounts" in the sidebar, you're still in Global View — the click didn't work.
### Iframe Navigation
> **Reference**: See `references/navigation-css-structure.md` for detailed CSS class hierarchy of the Omada site iframe navigation.
Once inside a site, all content is rendered inside an `<iframe>`. Standard `browser_click` refs from the outer snapshot may not work for iframe-internal elements. Use `browser_console` with `document.querySelector('iframe').contentDocument` to find and click menu items:
```javascript
(function(){
const fr = document.querySelector('iframe');
if (!fr || !fr.contentDocument) return 'no iframe';
const items = fr.contentDocument.querySelectorAll('li');
for (const item of items) {
if (item.textContent.trim() === 'WLAN') {
item.click();
return 'clicked WLAN';
}
}
return 'not found';
})()
```
### Full Menu Structure (inside site iframe)
- **Configuration** → expands to:
- Network Config → General Settings (Site Settings, SSH, VoIP), Network Settings (**Internet, LAN, WLAN**), Authentication (Portal), VPN, Security (ACL), Transmission (Routing, NAT, DHCP Reservation, Bandwidth Control, Gateway QoS), Profile (Groups, Time Range, Rate Limit, APN Profile)
- Device Config → Gateway (DNS), Switch (Switch Ports)
## VLAN Configuration
### VLAN Inventory (site "Rohr")
Navigate to Configuration → Network Config → Network Settings → LAN tab.
| VLAN | Name | Subnet | Purpose |
|------|------|--------|---------|
| 1 | CatchAll(Default) | 172.16.1.1/24 | Default/untagged |
| 10 | Infrastructure | 10.0.10.1/24 | Network infrastructure |
| 20 | Server / Hypervisors | — | PVE hosts (10.0.20.x) |
| 30 | Server / VMs & LXCs | — | Containers & VMs (10.0.30.x) |
| 40 | Clients | — | Client devices (Mac, PC, mobile) |
| 50 | IoT | 10.0.50.1/24 | IoT devices |
| 60 | DMZ | 10.0.60.1/24 | Demilitarized zone |
| 192 | 192.168.100.x | 192.168.100.x | Special purpose |
**Pitfall**: The VLAN list in the LAN tab only shows 4 VLANs by default
(1, 10, 50, 60). VLANs 20, 30, 40, 192 are visible in the **Isolation Settings**
view but not in the main VLAN table. They may be configured elsewhere or
inherited from the switch/gateway.
### VLAN Isolation Settings
Navigate to Configuration → Network Config → Network Settings → LAN → **Isolation Settings** button.
Two zones:
- **Isolated Network** — can only access internet, no inter-VLAN communication
- **Interconnected Network** — can communicate with each other
As of 2026-07-05: **0 isolated**, all 8 VLANs are interconnected (all can
reach each other). Inter-VLAN routing is handled by the DR3650 gateway.
### ACL Rules (Gateway)
Navigate to Configuration → Network Config → Security → ACL.
Key rules (LAN→LAN direction, relevant for inter-VLAN access):
| # | Description | Direction | Proto | Source → Destination |
|---|-------------|-----------|-------|---------------------|
| 1 | Permit Internet to everyone | LAN→WAN | All | All VLANs → Any |
| 5 | Allow Hypervisors from Clients | LAN→LAN | All | Clients → Hypervisors |
| 6 | Allow Clients to VMs | LAN→LAN | **TCP** | Clients → VMs & LXCs |
| 8 | Allow Access to IoT | LAN→LAN | All | Clients/Hyper/VMs/192 → IoT |
| 9 | DENY Botnet Stuff for IoT | LAN→LAN | Deny All | IoT/CatchAll → Clients/Hyper/VMs/Infra |
| 10 | Deny Ingress to VLANs | WAN IN | Deny All | Any → Client/Hyper/Infra/IoT/VM IPs |
**Important**: Rule 6 allows **TCP only** from Clients (VLAN 40) to VMs (VLAN 30).
UDP and ICMP are NOT explicitly permitted — they may be allowed by default
(no deny rule for LAN→LAN between these VLANs), but if you need UDP services
(e.g. mDNS, SNMP) from VLAN 40 to VLAN 30, verify with a packet capture.
### mDNS / Bonjour Across VLANs
**Omada does NOT provide mDNS/multicast relay or IGMP proxy functionality.**
mDNS broadcasts (224.0.0.251:5353) are confined to their originating VLAN.
Bonjour service discovery (AirPlay, Time Machine, printer discovery) does NOT
cross VLAN boundaries.
**Workarounds**:
1. **Manual connection** — clients specify the IP/path directly
(e.g. `smb://10.0.30.21/share` for Time Machine)
2. **External mDNS repeater** — run `mdns-repeater` or `avahi-reflector` on
a host with interfaces in both VLANs (e.g. a CT with trunk or dual NIC)
**Deployed Solution**: CT 127 (avahi-reflector) on proxmox6 runs
`avahi-daemon` in reflector mode with interfaces in VLAN 30 (10.0.30.5,
eth0), VLAN 40 (10.0.40.5, eth1), and VLAN 50 (10.0.50.5, eth2).
Configured with a **directional matrix** via iptables:
- 30→40 ✅ (server services visible to clients)
- 50→30 ✅ (IoT services visible to servers)
- 50→40 ✅ (IoT services visible to clients)
- 40→30, 40→50, 30→50 ❌ (blocked by iptables INPUT/OUTPUT rules)
See the `proxmox-ve-administration` skill, `references/avahi-reflector-setup.md`
for full setup details including the multi-VLAN iptables rule pattern.
The DR3650 gateway does not expose multicast forwarding or IGMP proxy settings
in the Omada controller UI. This is a firmware limitation.
### Navigating to VLAN/ACL Views
**VLAN list**: Configuration → Network Config → Network Settings → LAN tab.
Shows only 4 VLANs by default (1, 10, 50, 60). VLANs 20, 30, 40, 192 are
hidden — visible only in Isolation Settings.
**Isolation Settings**: Click the "Isolation Settings" button on the LAN tab
(next to "Add"). Shows all 8 VLANs grouped into Isolated vs Interconnected.
As of 2026-07-05: 0 isolated, all interconnected.
**ACL Rules**: Configuration → Network Config → Security → ACL.
Shows Gateway ACL rules table with INDEX, ENABLED, DESCRIPTION, DIRECTION,
POLICY, PROTOCOLS, SOURCE, DESTINATION, ACTION columns.
## WLAN Management
### SSID Overview
Navigate to Configuration → Network Settings → WLAN. Shows tabs: **SSID**, **WLAN Optimization**, **Optimization History**.
Observed SSIDs (site "Rohr"):
| SSID | Security | Bands | VLAN |
|------|----------|-------|------|
| Schön | WPA-Personal | 2.4/5/6 GHz | 40 |
| Schön - Gäste | WPA-Personal | 2.4/5/6 GHz | — |
| IoT | WPA-Personal | 2.4/5 GHz | 50 |
| 2Schoen | WPA-Personal | 2.4/5 GHz | 40 |
### WLAN Optimization (Channel Optimization)
1. Navigate to WLAN → "WLAN Optimization" tab
2. Click "Optimize Now" link
3. **Optimization Mode dialog** appears with two radio options:
- **Partial** (default) — scans and optimizes only channels needing adjustment
- **Global** — full scan and reassignment of all wireless configuration
- Select mode, then click "Confirm" to start
4. **Warning**: Internet connectivity drops for several minutes during optimization. Schedule during low-traffic periods.
5. Progress can be tracked under "Optimization History" tab
6. "Excluded Devices List" allows excluding specific APs from optimization
### Per-SSID Channel Configuration
To manually set channels (instead of auto-optimization):
1. Go to WLAN → SSID tab
2. Click the SSID name or the action icon in the row
3. Edit the wireless network settings — channel selection is per-band (2.4 GHz, 5 GHz, 6 GHz)
4. Available 5 GHz channels observed: 36, 40, 44, 48, 149, 153, 157, 161
## Async Console Automation Pattern
For multi-step flows (login → dismiss dialogs → navigate to page → action), chaining individual `browser_snapshot` + `browser_click` calls is fragile and slow due to iframe ref instability and session expiry. Instead, use a **single `browser_console` call with an async IIFE** that sleeps between steps:
```javascript
(async function(){
const sleep = ms => new Promise(r => setTimeout(r, ms));
// Step 1: close notice
for (const el of document.querySelectorAll('a, button')) {
if (el.textContent.trim() === 'OK') { el.click(); break; }
}
await sleep(1000);
// Step 2: click site cell
let fr = document.querySelector('iframe');
if (!fr || !fr.contentDocument) return 'no iframe';
for (const c of fr.contentDocument.querySelectorAll('td, [role="cell"]')) {
if (c.textContent.trim() === 'Rohr') { c.click(); break; }
}
await sleep(5000);
// Step 3: expand Configuration + click WLAN
fr = document.querySelector('iframe');
for (const li of fr.contentDocument.querySelectorAll('li')) {
if (li.textContent.trim() === 'Configuration' && li.classList.contains('title')) { li.click(); break; }
}
await sleep(1000);
fr = document.querySelector('iframe');
for (const li of fr.contentDocument.querySelectorAll('li')) {
if (li.textContent.trim() === 'WLAN' && li.classList.contains('navigator-li-effective')) { li.click(); break; }
}
await sleep(3000);
// ... continue with tab click, Optimize Now, mode selection, Confirm
return 'done';
})()
```
### Why This Works Better
- **Atomic**: All steps run in one execution context — no risk of session expiring between tool calls
- **No ref fragility**: Direct DOM queries inside iframe don't depend on snapshot refs staying valid
- **Sleep-based timing**: `await sleep(ms)` gives SPA time to render between steps
- **Early return on failure**: Each step checks for iframe existence and returns descriptive error if missing
### Triggering WLAN Optimization (Full Async Flow)
To run the entire optimization (site click → WLAN → Optimization tab → Optimize Now → Global → Confirm) in one shot:
```javascript
(async function(){
const sleep = ms => new Promise(r => setTimeout(r, ms));
// ... (site click + Configuration expansion + WLAN click as above)
// Click "WLAN Optimization" tab
fr = document.querySelector('iframe');
for (const el of fr.contentDocument.querySelectorAll('span, div, a, li')) {
if (el.textContent.trim() === 'WLAN Optimization' && el.children.length <= 1) { el.click(); break; }
}
await sleep(3000);
// Click "Optimize Now"
fr = document.querySelector('iframe');
for (const el of fr.contentDocument.querySelectorAll('a, button, span')) {
if (el.textContent.trim() === 'Optimize Now' && el.children.length <= 2) { el.click(); break; }
}
await sleep(2000);
// Select "Global" radio
fr = document.querySelector('iframe');
for (const el of fr.contentDocument.querySelectorAll('label, span, div, input')) {
if (el.textContent.trim().startsWith('Global')) { el.click(); break; }
}
await sleep(1000);
// Click "Confirm"
fr = document.querySelector('iframe');
for (const el of fr.contentDocument.querySelectorAll('a, button')) {
if (el.textContent.trim() === 'Confirm') { el.click(); return '✅ Global optimization started!'; }
}
return 'Confirm not found';
})()
```
### Reading Optimization Results
After the optimization starts, the SPA typically goes blank (session expires). To check results:
1. Re-login (navigate + login flow)
2. Navigate to site Rohr → Configuration → WLAN → "Optimization History" tab
3. Read the iframe body text:
```javascript
(async function(){
const sleep = ms => new Promise(r => setTimeout(r, ms));
// ... (navigate to Optimization History tab as above)
fr = document.querySelector('iframe');
return fr.contentDocument.body.innerText.substring(0, 3000);
})()
```
The history table shows: TIME, TYPE (Manual/Adaptive), DEVICES, DURATION, MODE, RESULT (score before→after), and ACTION.
Observed result (Global mode, 2026-06-29):
```
Jun 29, 2026 08:36:34 am 3 4m 21s Manual
86 → 88 (+2)
```
Score scale: 0-100. Improvements of +2-4 points per optimization run are typical. 3 of 4 APs were optimized (one may have already been on an optimal channel).
**Post-optimization**: The optimization generated 2 "monitor link down" alerts (see "Pitfall: Optimization Causes Monitor Link Down Alerts" below). These were batch-resolved, bringing the total to 20 resolved / 0 unresolved.
### Full Optimization Flow (Confirmed Working)
The entire flow — from Global View to optimization confirmation — can be executed as a single async IIFE after login. This was confirmed end-to-end on 2026-06-29:
1. Login (Enter key in password field)
2. Dismiss notice dialog (OK button)
3. Click site "Rohr" cell in iframe
4. Expand Configuration → click WLAN
5. Click "WLAN Optimization" tab
6. Click "Optimize Now" link
7. Select "Global" radio in the optimization mode dialog
8. Click "Confirm"
Steps 3-8 can all run in one `browser_console` async IIFE (see "Triggering WLAN Optimization" code block above). After Confirm, the SPA goes blank — re-login to check results via Optimization History.
### Session Expiry After Confirm
Clicking "Confirm" in the optimization dialog causes the SPA to go blank — the iframe disappears entirely. This is expected: the optimization disrupts connectivity (including the controller's own management plane). Recovery requires a full re-login cycle.
## Device Detail Pages
### Opening Device Details — Two Levels
There are TWO ways to open device details, producing different views:
#### 1. Simple View: Click device name (`span.device-name`)
Click the `span.device-name` element in the Devices table. Opens a
compact detail panel with limited tabs.
```javascript
// Correct selector for device names in the table
const spans = doc.querySelectorAll('span.device-name');
for (const s of spans) {
if (s.textContent.trim() === 'Router') { s.click(); break; }
}
```
> **Pitfall**: `span.text-link` does NOT match device names in the table.
> The correct CSS class is `span.device-name` (class:
> `device-name u-text-word-break-all ellipsis`). The `span.text-link`
> selector works for site names in the Site List, not device names.
#### 2. Rich View: Click "Manage Device" button
After opening the simple view, click the **"Manage Device"** button
(`a.manage-device-button`). This opens a wider drawer with full tabs:
**Router (DR3650v-4G) tabs in Manage Device view:**
- **Overview** — Upload/Download rate, Temperature, CPU%, Memory%, Channel
Utilization, Link graph, Current Clients table (shows connected clients
with MAC, IP, IPv6)
- **Network View** — Network topology visualization
- **Ports** — Port-level statistics (packets, dropped, errors, bytes per port)
- **Logs** — Device-specific log entries
- **Tools** — Network Check, Terminal
- **Config** — Device configuration with left-side menu:
- **General**: Name, Description, LED, Device Labels, Remember Device
- **Wireless**: Radio settings
- **SIM Config**: PIN Management, Statistics, SMS Message, SMS Settings
- **Transmission**: Routing, NAT, Bandwidth Control, Gateway QoS
- **Network Security**: ACL
- **Advanced**: General, DNS
- **No SNMP section exists** (verified 2026-07-05 — see SNMP Monitoring section)
**Switch (SG3428MP) tabs in Manage Device view:**
- Same 6 tabs (Overview, Network View, Ports, Logs, Tools, Config)
- **Config** menu differs: General, VLAN Interface, Services (Loopback
Control, Spanning Tree), Static Route
- **No SNMP section exists**
> **Pitfall**: The Config tab's left-side menu uses `.ant-menu-item` class.
> Some items show as `[skeleton-item fake-menu-item]` placeholders that
never load (20 skeleton items remained permanently on the router config).
Scrolling the menu container does not trigger lazy loading. This appears
to be a bug in the Omada SPA when certain firmware versions don't expose
all expected config sections.
### Known Issue: DR3650 ISP Statistics Empty
**Symptom**: ISP statistics page for the DR3650v-4G router is always empty in Omada.
**Root Cause**: The DR3650v-4G with **Hardware v1.0 / Firmware 1.1.1** does not push ISP-level telemetry (WAN traffic, cellular data volume, signal strength, connection statistics) to the Omada controller. The controller can only read basic device telemetry (temperature, CPU, memory, port status). This is a **hardware/firmware limitation**, not a configuration issue.
**Observations** (2026-06-29):
- Router detail page has only "Overview" and "Ports" tabs — no ISP/Cellular/WAN section
- Active WAN ports: DSL + Port 5 (green/active) — DSL is primary WAN, not 4G/SIM
- Router reports: 75°C, CPU 24%, Memory 57%, uptime 6+ days
- Port statistics ARE available (packets, bytes, errors per port) but aggregated WAN/ISP stats are NOT
**Workaround**: Enable local management on the router to access its built-in web UI directly:
1. In Omada: Configuration → Device Config → Gateway → Local Management → Enable
2. Access router UI at `http://10.0.30.1/webpages/login.html`
3. ISP/cellular statistics are available in the router's own web UI under WAN/Cellular sections
**Pitfall: Router Local Login Blocked When Managed by Controller**
When the DR3650 is adopted by an Omada controller, the local web UI at `http://10.0.30.1/webpages/login.html` displays: *"Note: This Gateway is being managed by Controller [IP]"*. Login attempts with admin credentials or the Omada cloud password will fail. Local management must be explicitly enabled through the Omada controller's Device Config → Gateway → Local Management settings before local login works.
## Logs & Alerts
### Viewing Alerts
Navigate to site → **Logs** (left nav, under Monitoring). The Logs badge shows the count of unresolved alerts. Three tabs: **Alerts**, **Events**, **Audit Logs**.
Filter options:
- **Unresolved / Resolved / All** — toggle between alert states
- **System / Device** — filter by alert source
- **Filter** link — advanced filtering
### Alert Types Observed
| Type | Level | Cause | Action |
|------|-------|-------|--------|
| Device informed monitor link down | Warning | AP lost TCP connection to Controller on port 29817 | Usually transient — see pitfall below |
### Resolving Alerts (Batch Resolve)
To mark multiple alerts as resolved at once:
1. Navigate to site → **Logs****Alerts** tab (must be in site view, NOT Global View — Global View only has "Events" and "Audit Logs" tabs, no "Alerts")
2. Click the **"Select all"** checkbox in the table header (first `ant-checkbox-input` in the table)
3. Click **"Batch Resolved"** link (class `s-button s-button-link`) in the toolbar
4. No confirmation dialog appears — alerts are resolved immediately
5. Verify: the "Unresolved" count drops to 0
```javascript
// Async flow: select all + batch resolve
(async function(){
const sleep = ms => new Promise(r => setTimeout(r, ms));
const fr = document.querySelector('iframe');
if (!fr || !fr.contentDocument) return 'no iframe';
const doc = fr.contentDocument;
// Select all checkboxes
const cbs = doc.querySelectorAll('.ant-checkbox-input');
if (cbs.length > 0) cbs[0].click(); // first visible CB = select all
await sleep(1000);
// Click "Batch Resolved"
for (const l of doc.querySelectorAll('a, button, span')) {
if (l.textContent.trim() === 'Batch Resolved' && l.offsetParent !== null) {
l.click();
return 'batch resolved';
}
}
return 'Batch Resolved not found';
})()
```
### Checking Firmware Versions & Available Updates
Navigate to site → **Devices** (left nav). The device table shows all 6 devices with columns: NAME, IP, STATUS, MODEL, ACTION.
- **Firmware update indicator**: A green upward-arrow icon in the ACTION column signals an available firmware update for that device.
- **"Start Rolling Upgrade"** button (top right) initiates staged firmware updates across multiple devices.
- Devices without the arrow icon are on the latest firmware.
Observed firmware status (2026-06-29):
| Device | Model | HW | Update? |
|--------|-------|----|---------|
| Router | DR3650v-4G | v1.0 | ✅ Current |
| Core (Switch) | SG3428MP | v6.20 | ⬆️ Update available |
| AP Erdgeschoss | EAP683 UR(EU) | v1.0 | ✅ Current |
| AP Obergeschoss | EAP683 UR(EU) | v1.0 | ✅ Current |
| AP Garage | EAP615-Wall(EU) | v1.0 | ✅ Current |
| AP Garten | EAP225-Outdoor(EU) | v3.0 | ✅ Current |
### Pitfall: Optimization Causes "Monitor Link Down" Alerts
Running WLAN Optimization (especially Global mode) disrupts AP connectivity for several minutes. Each affected AP generates a "Device informed monitor link down" Warning alert (TCP connection to Controller on port 29817 failed). These are **expected and harmless** — the APs reconnect after optimization completes. Don't treat them as real network problems. Use "Batch Resolved" (see above) to clear them.
### Pitfall: Global View vs Site View — Different Tab Sets
The Global View Logs page only has "Events" and "Audit Logs" tabs. The "Alerts" tab exists ONLY in the site-level Logs page. If you see only "Events" and "Audit Logs", you're in Global View — navigate back to the site first. Similarly, the site-level sidebar has "Management", "Monitoring", "Configuration", "Hotspot", "Maintenance" sections, while Global View has "Dashboard", "Devices", "Logs", "SD-WAN", "Accounts", "Settings". Use the sidebar structure to verify which view you're in.
## SNMP Monitoring
### CRITICAL FINDING: SNMP Cannot Be Enabled via Omada Cloud Controller
**As of 2026-07-05, the Omada Cloud Controller does NOT expose SNMP
configuration for ANY device type.** This was verified exhaustively:
- **Router (DR3650v-4G, FW 1.1.1)**: Config tab has sections: General,
Wireless, SIM Config (PIN Management, Statistics, SMS Message, SMS
Settings), Transmission (Routing, NAT, Bandwidth Control, Gateway QoS),
Network Security (ACL), Advanced (General, DNS). **None contain SNMP.**
Some sections show `[skeleton-item fake-menu-item]` placeholders that
never load (20 skeleton items remain permanently).
- **Switch (SG3428MP, FW 6.20.26)**: Config tab has sections: General,
VLAN Interface, Services (Loopback Control, Spanning Tree), Static Route.
**No SNMP.**
- **APs**: Config tab not tested for SNMP, but given the pattern above,
unlikely to have SNMP settings either.
**Conclusion**: SNMP configuration is only available through the devices'
local web UIs, NOT through the Omada cloud controller. The cloud controller
simply does not provide this capability.
### Prerequisites for SNMP via Local Device UIs
To enable SNMP on the devices, you need access to each device's local web UI:
1. **Router (172.16.1.1)**: Local login is BLOCKED — shows *"Note: This
Gateway is being managed by Controller 99.80.193.119"*. Must enable
Local Management via Omada controller first (Configuration → Device
Config → Gateway → Local Management — if this setting exists in your
controller version).
2. **Switch (172.16.1.2)**: Local web UI is accessible (no "managed by
controller" message), but requires device-specific admin credentials.
Neither `admin/admin` nor the Omada cloud password work. The local
credentials are NOT stored in 1Password.
3. **APs**: Each AP may have a local web UI at its 172.16.1.x address.
Credentials unknown.
### Omada API Exporters — Do NOT Work with Cloud Controller
Two community Prometheus exporters exist for TP-Link Omada:
- `charlie-haley/omada_exporter` (140 stars) — Docker image `chhaley/omada_exporter`
- `RCooLeR/omada_exporter` (56 stars) — Docker image `ghcr.io/rcooler/omada_exporter`
**Both require a LOCAL Omada controller URL** (`OMADA_HOST=https://192.168.x.x`).
They use the Omada controller's REST API (`/api/v2/...`), which is only
served by on-premise controllers. The cloud controller at
`omada.tplinkcloud.com` returns HTML (SPA) for all API paths — it does NOT
expose a REST API backend.
**To use these exporters, you would need to run a local Omada Software
Controller** (Docker container) and migrate devices from the cloud
controller to the local one.
### Local Omada Software Controller Migration (In Progress)
**Decision (2026-07-05)**: Migrate from cloud controller to a local Omada
Software Controller to enable SNMP and API-based monitoring.
**Rationale**: The cloud controller cannot enable SNMP or serve API
exporters. A local controller unlocks both paths. Devices retain cached
config during controller downtime, so the circular dependency
(controller needs network ↔ network managed by controller) is not
real — devices boot with last-known config and continue operating.
**Deployment target**: CT 142 (`omada-controller`, 10.0.30.142/24,
VLAN 30, proxmox7, vm_disks storage, 2 cores / 4 GB RAM / 32 GB disk).
Created via `pct create` with Debian 12 template.
**Migration steps** (planned):
1. Install Docker on CT 142
2. Deploy Omada Software Controller container (TP-Link official image)
3. Run setup wizard (admin user, site "Rohr")
4. Remove devices from cloud controller
5. Adopt devices on local controller (devices at 172.16.1.x)
6. Deploy `omada_exporter` (charlie-haley or RCooLeR) pointing at
`http://10.0.30.142:8088`
7. Add Prometheus scrape config for omada_exporter
8. Extend Grafana dashboard with Omada panels
**Circular Dependency Analysis** (user-raised concern):
- Cold-start sequence: Gateway boots (cached config) → Switch boots
(cached config) → PVE nodes boot → HA manager starts CT 142 →
Controller reconnects to devices
- Risk: only if gateway config is corrupt AND controller is down
simultaneously — very unlikely
- Mitigation: CT 142 is in HA-manager (auto-restart on any node)
### Alternatives for Omada Metrics Collection
Since neither SNMP nor API exporters work with the cloud controller:
1. **Run a local Omada Software Controller** — IN PROGRESS (CT 142).
Devices will be re-adopted from the local controller. Then both
SNMP (via local device UIs) and API exporters will work.
2. **Browser automation scraping** — Periodically read device metrics
(CPU, memory, temperature, port stats, client counts) from the Omada
cloud controller via browser automation and push to Prometheus via
pushgateway. Hacky but functional.
3. **Accept the limitation** — The existing Grafana dashboard
(`network-infra`, uid: `network-infra`) already tracks PVE nodes,
guests, HA state, and blackbox probes. Omada device metrics (CPU,
memory, temp) are visible in the Omada UI but not in Prometheus.
### Device Inventory (Verified 2026-07-05)
All devices are on VLAN 1 (CatchAll/Default, 172.16.1.0/24):
| Device | Model | HW/FW | IP | Notes |
|--------|-------|-------|-----|-------|
| Router | DR3650v-4G | HW v1.0 / FW 1.1.1 | 172.16.1.1 | Primary gateway, VLAN routing. Also at 10.0.30.1 (VLAN 30 interface) |
| Core (Switch) | SG3428MP | HW v6.20 / FW 6.20.26 | 172.16.1.2 | PoE switch, 28 ports, firmware update available (6.20.27) |
| AP Erdgeschoss | EAP683 UR(EU) | HW v1.0 / FW 1.4.0 | 172.16.1.3 | Connected to switch port 16 |
| AP Obergeschoss | EAP683 UR(EU) | HW v1.0 / FW 1.4.0 | 172.16.1.4 | Connected to switch port 14 |
| AP Garage | EAP615-Wall(EU) | HW v1.0 / FW 1.5.4 | 172.16.1.5 | Connected to switch port 21 |
| AP Garten | EAP225-Outdoor(EU) | HW v3.0 / FW 5.2.2 | 172.16.1.6 | Outdoor AP |
> **Note**: The 10.0.30.1, 10.0.20.1, etc. addresses are the gateway's VLAN
> interfaces, not separate devices. All Omada-managed devices use
> 172.16.1.x (VLAN 1 / CatchAll) as their management IP. The Omada
> controller communicates with devices via the organization connection IP
> (84.39.78.100) through the cloud.
### snmp_exporter Deployment Status
**snmp_exporter IS deployed** on CT 141 as a binary + systemd service
(v0.27.0, port 9116, using the official snmp.yml with all standard MIBs).
Docker-based deployment on CT 141 does not work — see the deployment
section below for details. The exporter is running and ready; it just
needs SNMP to be enabled on the Omada devices to start collecting.
The monitoring stack on CT 141 runs via Docker: prometheus, grafana,
alertmanager, blackbox-exporter, pve-exporter, telegram-bridge.
snmp_exporter is the exception — it runs as a systemd service, not Docker.
### Deploying snmp_exporter in Monitoring Stack
> **Critical**: Docker-based deployment of snmp_exporter on CT 141 FAILS.
> Containers get stuck in "Created" state, `docker start`/`docker logs`/
> `docker inspect` all hang indefinitely. This affects both `docker run`
> and `docker compose up`. The root cause is unclear (other containers
> like prometheus/grafana work fine), but the pattern is 100%
> reproducible. **Use the binary + systemd approach instead** (below).
#### Approach A (RECOMMENDED): Binary + systemd service
Works reliably on CT 141. Tested with snmp_exporter v0.27.0.
```bash
# On CT 141 (via SSH to proxmox7, pct exec 141):
curl -sL -o /tmp/snmp_exporter.tar.gz \
https://github.com/prometheus/snmp_exporter/releases/download/v0.27.0/snmp_exporter-0.27.0.linux-amd64.tar.gz
tar xzf /tmp/snmp_exporter.tar.gz -C /tmp/
# Install binary
cp /tmp/snmp_exporter-0.27.0.linux-amd64/snmp_exporter /usr/local/bin/snmp_exporter
chmod +x /usr/local/bin/snmp_exporter
# Use the official snmp.yml bundled with the release (1.4MB, 63574 lines)
# Contains all standard MIBs including if_mib, host_resources, etc.
cp /tmp/snmp_exporter-0.27.0.linux-amd64/snmp.yml /etc/snmp_exporter.yml
# Create systemd service
cat > /etc/systemd/system/snmp-exporter.service << 'EOF'
[Unit]
Description=SNMP Exporter for Prometheus
After=network.target
[Service]
ExecStart=/usr/local/bin/snmp_exporter --config.file=/etc/snmp_exporter.yml
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now snmp-exporter
# Verify: systemctl status snmp-exporter
# Test: curl -s "http://localhost:9116/snmp?target=172.16.1.1&module=if_mib&auth=public_v2"
# Metrics: curl -s http://localhost:9116/metrics | head -5
```
**Pitfall**: v0.27.0 does NOT have the `/-/healthy` endpoint (returns 404).
Use `curl -s http://localhost:9116/metrics` to verify the exporter is alive.
#### Approach B (NOT RECOMMENDED): Docker container
Adding snmp_exporter as a Docker container on CT 141 does not work.
`docker create` succeeds but `docker start` hangs for 30+ seconds,
then the container enters a crash-restart loop. `docker logs`,
`docker inspect`, and `docker rm` all hang. The only way to recover
is `docker container prune -f` or waiting for operations to eventually
complete after 30-60s timeouts. Both the full 2MB snmp.yml and a
minimal custom snmp.yml produce the same result. This is specific to
CT 141 — not a general snmp_exporter Docker issue.
### Prometheus Scrape Configuration
Add to `/opt/monitoring/prometheus/prometheus.yml` on CT 141:
```yaml
- job_name: snmp_omada
scrape_interval: 30s
static_configs:
- targets:
- 172.16.1.1 # Gateway (DR3650v-4G)
- 172.16.1.2 # Switch (SG3428MP)
- 172.16.1.3 # AP Erdgeschoss (EAP683 UR)
- 172.16.1.4 # AP Obergeschoss (EAP683 UR)
- 172.16.1.5 # AP Garage (EAP615-Wall)
- 172.16.1.6 # AP Garten (EAP225-Outdoor)
metrics_path: /snmp
params:
module: [if_mib]
auth: [public_v2]
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: localhost:9116 # snmp_exporter runs as systemd, not Docker
```
Then reload: `curl -s -X POST http://localhost:9090/-/reload`
Verify: `curl -s http://localhost:9090/api/v1/targets | python3 -m json.tool | grep snmp`
### TP-Link Enterprise OIDs
TP-Link uses enterprise OID 11863 (per IANA assignment). Key MIB subtrees:
- `1.3.6.1.4.1.11863.1` — tp-linkDevice
- `1.3.6.1.4.1.11863.2` — tp-linkCommon
- `1.3.6.1.4.1.11863.3` — tp-linkPort
- `1.3.6.1.4.1.11863.6` — tp-linkSwitch
Standard IF-MIB (`1.3.6.1.2.1.2`) works for interface stats on all devices.
Host Resources MIB (`1.3.6.1.2.1.25`) works for CPU/memory on devices that
expose it.
## Best Practices
- Always wait 5-8s after navigation before snapshotting — the SPA is slow
- Use `browser_console` for iframe-internal interactions when `browser_click` refs fail
- **Prefer async IIFE console chains** over individual tool calls for multi-step flows — fewer session-expiry failures
- Dismiss all popup dialogs before attempting navigation
- Channel optimization disrupts connectivity — confirm with user before triggering
- Check "Optimization History" to verify optimization completed successfully
- After triggering optimization, expect session to drop — re-login to check results
@@ -0,0 +1,54 @@
# Omada Controller Navigation CSS Structure
## Top-Level Dropdown (Outside Iframe)
### System Type Selector
- Container: `.ant-dropdown-menu.ant-dropdown-menu-root`
- Items: `.ant-dropdown-menu-item`
- Selected: `.ant-dropdown-menu-item-selected`
- Options: "On Premise Systems", "Cloud Based Systems"
## Site Iframe Navigation Classes
### Menu Item Levels
| Level | CSS Class | Purpose |
|-------|----------|---------|
| Section title | `li.title` | "Management", "Monitoring", "Configuration", "Maintenance" |
| Level 1 (main) | `li.navigator-li.navigator-li-level1.navigator-li-effective` | Dashboard, Devices, Clients, etc. |
| Level 1 with submenu | `li.navigator-li.navigator-li-level1.has-sub-navi` | "Network Config", "Device Config" |
| Level 2 | `li.navigator-li.navigator-li-level2.has-sub-navi` | "General Settings", "Network Settings", etc. |
| Level 3 (leaf) | `li.navigator-li.navigator-li-level3.navigator-li-effective` | Individual config pages (WLAN, LAN, Internet...) |
### Finding Specific Menu Items
```javascript
// IIFE required to avoid re-declaration errors
(function(){
const fr = document.querySelector('iframe');
if (!fr || !fr.contentDocument) return 'no iframe';
const items = fr.contentDocument.querySelectorAll('li');
for (const item of items) {
if (item.textContent.trim() === 'WLAN') {
item.click();
return 'clicked WLAN';
}
}
return 'not found';
})()
```
### WLAN Tab Elements
Inside WLAN view, tabs are plain `StaticText` spans:
- `"SSID"` — SSID list view
- `"WLAN Optimization"` — optimization view
- `"Optimization History"` — history view
Click these via `browser_console` by matching `textContent.trim()` with `children.length <= 1`.
### Table Structure
SSID table uses standard `<table>` with:
- Column headers: `columnheader` cells ("SSID NAME", "SECURITY", "BAND", etc.)
- Data rows: `cell` elements with text content
### Dialog Elements
- Optimization Mode dialog appears as `PluginObject` with radio buttons labeled "Partial" and "Global"
- Confirm/Cancel are `link` elements inside the dialog