mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2025-12-07 09:36:05 -08:00
Compare commits
18 Commits
16992bb2bd
...
v25.8.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4712a2ff29 | ||
|
|
f9179a1e89 | ||
|
|
a6df204721 | ||
|
|
101189ae7c | ||
|
|
f25c012fbe | ||
|
|
868a85d84c | ||
|
|
771dd4b176 | ||
|
|
ed4d3bf17c | ||
|
|
7c728fbe36 | ||
|
|
4ff9d01ef5 | ||
|
|
1bce2e80e8 | ||
|
|
1556d74406 | ||
|
|
9b3947cc90 | ||
|
|
18b0309ac4 | ||
|
|
0afd4ae115 | ||
|
|
09e360c746 | ||
|
|
5dbe79ba2f | ||
|
|
779707761f |
@@ -13,7 +13,7 @@ ENV PATH="/opt/venv/bin:$PATH"
|
|||||||
|
|
||||||
COPY . ${INSTALL_DIR}/
|
COPY . ${INSTALL_DIR}/
|
||||||
|
|
||||||
RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git \
|
RUN pip install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git \
|
||||||
&& bash -c "find ${INSTALL_DIR} -type d -exec chmod 750 {} \;" \
|
&& bash -c "find ${INSTALL_DIR} -type d -exec chmod 750 {} \;" \
|
||||||
&& bash -c "find ${INSTALL_DIR} -type f -exec chmod 640 {} \;" \
|
&& bash -c "find ${INSTALL_DIR} -type f -exec chmod 640 {} \;" \
|
||||||
&& bash -c "find ${INSTALL_DIR} -type f \( -name '*.sh' -o -name '*.py' -o -name 'speedtest-cli' \) -exec chmod 750 {} \;"
|
&& bash -c "find ${INSTALL_DIR} -type f \( -name '*.sh' -o -name '*.py' -o -name 'speedtest-cli' \) -exec chmod 750 {} \;"
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ RUN phpenmod -v 8.2 sqlite3
|
|||||||
RUN apt-get install -y python3-venv
|
RUN apt-get install -y python3-venv
|
||||||
RUN python3 -m venv myenv
|
RUN python3 -m venv myenv
|
||||||
|
|
||||||
RUN /bin/bash -c "source myenv/bin/activate && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 && pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag "
|
RUN /bin/bash -c "source myenv/bin/activate && update-alternatives --install /usr/bin/python python /usr/bin/python3 10 && pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag "
|
||||||
|
|
||||||
# Create a buildtimestamp.txt to later check if a new version was released
|
# Create a buildtimestamp.txt to later check if a new version was released
|
||||||
RUN date +%s > ${INSTALL_DIR}/front/buildtimestamp.txt
|
RUN date +%s > ${INSTALL_DIR}/front/buildtimestamp.txt
|
||||||
|
|||||||
200
back/device_heuristics_rules.json
Executable file
200
back/device_heuristics_rules.json
Executable file
@@ -0,0 +1,200 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"dev_type": "Gateway",
|
||||||
|
"icon_html": "<i class=\"fa fa-globe\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "INTERNET", "vendor": "" }
|
||||||
|
],
|
||||||
|
"name_pattern": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Access Point",
|
||||||
|
"icon_html": "<i class=\"fa fa-network-wired\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "74ACB9", "vendor": "Ubiquiti" },
|
||||||
|
{ "mac_prefix": "002468", "vendor": "Cisco" },
|
||||||
|
{ "mac_prefix": "F4F5D8", "vendor": "TP-Link" },
|
||||||
|
{ "mac_prefix": "F88E85", "vendor": "Netgear" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["router", "gateway", "ap", "access point", "access-point", "switch"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Phone",
|
||||||
|
"icon_html": "<i class=\"fa-brands fa-apple\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "001A79", "vendor": "Apple" },
|
||||||
|
{ "mac_prefix": "B0BE83", "vendor": "Samsung" },
|
||||||
|
{ "mac_prefix": "BC926B", "vendor": "Motorola" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["iphone", "ipad", "pixel", "galaxy", "redmi"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Phone",
|
||||||
|
"icon_html": "<i class=\"fa-solid fa-mobile\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
],
|
||||||
|
"name_pattern": ["android","samsung"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Tablet",
|
||||||
|
"icon_html": "<i class=\"fa fa-tablet\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "001B63", "vendor": "Apple" },
|
||||||
|
{ "mac_prefix": "BC4C4C", "vendor": "Samsung" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["tablet", "pad"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "IoT",
|
||||||
|
"icon_html": "<i class=\"fa-brands fa-raspberry-pi\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "B827EB", "vendor": "Raspberry Pi" },
|
||||||
|
{ "mac_prefix": "DCA632", "vendor": "Raspberry Pi" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["raspberry", "pi"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "IoT",
|
||||||
|
"icon_html": "<i class=\"fa-solid fa-microchip\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "840D8E", "vendor": "Espressif" },
|
||||||
|
{ "mac_prefix": "ECFABC", "vendor": "Espressif" },
|
||||||
|
{ "mac_prefix": "7C9EBD", "vendor": "Espressif" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["raspberry", "pi"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Desktop",
|
||||||
|
"icon_html": "<i class=\"fa fa-desktop\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "001422", "vendor": "Dell" },
|
||||||
|
{ "mac_prefix": "001874", "vendor": "Lenovo" },
|
||||||
|
{ "mac_prefix": "00E04C", "vendor": "Hewlett Packard" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["desktop", "pc", "computer"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Laptop",
|
||||||
|
"icon_html": "<i class=\"fa fa-laptop\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "3C0754", "vendor": "HP" },
|
||||||
|
{ "mac_prefix": "0017A4", "vendor": "Dell" },
|
||||||
|
{ "mac_prefix": "F4CE46", "vendor": "Lenovo" },
|
||||||
|
{ "mac_prefix": "409F38", "vendor": "Acer" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["macbook", "imac", "laptop", "notebook"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Server",
|
||||||
|
"icon_html": "<i class=\"fa fa-server\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "001CBF", "vendor": "Supermicro" },
|
||||||
|
{ "mac_prefix": "002186", "vendor": "Dell" },
|
||||||
|
{ "mac_prefix": "D02788", "vendor": "Hewlett Packard" },
|
||||||
|
{ "mac_prefix": "002590", "vendor": "IBM" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["server", "nas"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "VM",
|
||||||
|
"icon_html": "<i class=\"fa fa-server\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "525400", "vendor": "QEMU" },
|
||||||
|
{ "mac_prefix": "005056", "vendor": "VMware" },
|
||||||
|
{ "mac_prefix": "000C29", "vendor": "VMware" },
|
||||||
|
{ "mac_prefix": "000569", "vendor": "VMware" },
|
||||||
|
{ "mac_prefix": "00163E", "vendor": "Xen" },
|
||||||
|
{ "mac_prefix": "080027", "vendor": "VirtualBox" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "TV",
|
||||||
|
"icon_html": "<i class=\"fa fa-tv\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "0013CE", "vendor": "Samsung" },
|
||||||
|
{ "mac_prefix": "0017C8", "vendor": "LG" },
|
||||||
|
{ "mac_prefix": "D46E0E", "vendor": "Sony" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["tv", "television", "smarttv"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Gaming Console",
|
||||||
|
"icon_html": "<i class=\"fa fa-gamepad\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "001FA7", "vendor": "Sony" },
|
||||||
|
{ "mac_prefix": "7C04D0", "vendor": "Nintendo" },
|
||||||
|
{ "mac_prefix": "EC26CA", "vendor": "Sony" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["playstation", "xbox"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Camera",
|
||||||
|
"icon_html": "<i class=\"fa fa-camera\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "A45E60", "vendor": "Hikvision" },
|
||||||
|
{ "mac_prefix": "00408C", "vendor": "Axis" },
|
||||||
|
{ "mac_prefix": "00156D", "vendor": "Amcrest" },
|
||||||
|
{ "mac_prefix": "AC9E17", "vendor": "Reolink" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["camera", "cam", "webcam"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Smart Speaker",
|
||||||
|
"icon_html": "<i class=\"fa fa-volume-up\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "44650D", "vendor": "Amazon" },
|
||||||
|
{ "mac_prefix": "74ACB9", "vendor": "Google" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["echo", "alexa", "dot"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Router",
|
||||||
|
"icon_html": "<i class=\"fa fa-random\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "000C29", "vendor": "Cisco" },
|
||||||
|
{ "mac_prefix": "00155D", "vendor": "MikroTik" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["router", "gateway", "ap", "access point", "access-point"],
|
||||||
|
"ip_pattern": [
|
||||||
|
"^192\\.168\\.[0-1]\\.1$",
|
||||||
|
"^10\\.0\\.0\\.1$"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Smart Light",
|
||||||
|
"icon_html": "<i class=\"fa fa-lightbulb\"></i>",
|
||||||
|
"matching_pattern": [],
|
||||||
|
"name_pattern": ["hue", "lifx", "bulb"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Smart Home",
|
||||||
|
"icon_html": "<i class=\"fa fa-house\"></i>",
|
||||||
|
"matching_pattern": [],
|
||||||
|
"name_pattern": ["google", "chromecast", "nest"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Smartwatch",
|
||||||
|
"icon_html": "<i class=\"fa fa-watch\"></i>",
|
||||||
|
"matching_pattern": [],
|
||||||
|
"name_pattern": ["watch", "wear"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Printer",
|
||||||
|
"icon_html": "<i class=\"fa fa-print\"></i>",
|
||||||
|
"matching_pattern": [],
|
||||||
|
"name_pattern": ["printer", "print"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Security Device",
|
||||||
|
"icon_html": "<i class=\"fa fa-shield-alt\"></i>",
|
||||||
|
"matching_pattern": [],
|
||||||
|
"name_pattern": ["doorbell", "lock", "security"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dev_type": "Smart Light",
|
||||||
|
"icon_html": "<i class=\"fa-solid fa-lightbulb\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
],
|
||||||
|
"name_pattern": ["light","bulb"]
|
||||||
|
}
|
||||||
|
]
|
||||||
106
docs/API.md
106
docs/API.md
@@ -221,6 +221,112 @@ Example JSON of the `table_devices.json` endpoint with two Devices (database row
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## API Endpoint: Prometheus Exporter
|
||||||
|
|
||||||
|
* **Endpoint URL**: `/metrics`
|
||||||
|
* **Host**: (where NetAlertX exporter is running)
|
||||||
|
* **Port**: as configured in the `GRAPHQL_PORT` setting (`20212` by default)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example Output of the `/metrics` Endpoint
|
||||||
|
|
||||||
|
Below is a representative snippet of the metrics you may find when querying the `/metrics` endpoint for `netalertx`. It includes both aggregate counters and `device_status` labels per device.
|
||||||
|
|
||||||
|
```
|
||||||
|
netalertx_connected_devices 31
|
||||||
|
netalertx_offline_devices 54
|
||||||
|
netalertx_down_devices 0
|
||||||
|
netalertx_new_devices 0
|
||||||
|
netalertx_archived_devices 31
|
||||||
|
netalertx_favorite_devices 2
|
||||||
|
netalertx_my_devices 54
|
||||||
|
|
||||||
|
netalertx_device_status{device="Net - Huawei", mac="Internet", ip="1111.111.111.111", vendor="None", first_connection="2021-01-01 00:00:00", last_connection="2025-08-04 17:57:00", dev_type="Router", device_status="Online"} 1
|
||||||
|
netalertx_device_status{device="Net - USG", mac="74:ac:74:ac:74:ac", ip="192.168.1.1", vendor="Ubiquiti Networks Inc.", first_connection="2022-02-12 22:05:00", last_connection="2025-06-07 08:16:49", dev_type="Firewall", device_status="Archived"} 1
|
||||||
|
netalertx_device_status{device="Raspberry Pi 4 LAN", mac="74:ac:74:ac:74:74", ip="192.168.1.9", vendor="Raspberry Pi Trading Ltd", first_connection="2022-02-12 22:05:00", last_connection="2025-08-04 17:57:00", dev_type="Singleboard Computer (SBC)", device_status="Online"} 1
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Metrics Explanation
|
||||||
|
|
||||||
|
#### 1. Aggregate Device Counts
|
||||||
|
|
||||||
|
Metric names prefixed with `netalertx_` provide aggregated counts by device status:
|
||||||
|
|
||||||
|
* `netalertx_connected_devices`: number of devices currently connected
|
||||||
|
* `netalertx_offline_devices`: devices currently offline
|
||||||
|
* `netalertx_down_devices`: down/unreachable devices
|
||||||
|
* `netalertx_new_devices`: devices recently detected
|
||||||
|
* `netalertx_archived_devices`: archived devices
|
||||||
|
* `netalertx_favorite_devices`: user-marked favorite devices
|
||||||
|
* `netalertx_my_devices`: devices associated with the current user context
|
||||||
|
|
||||||
|
These numeric values give a high-level overview of device distribution.
|
||||||
|
|
||||||
|
#### 2. Per‑Device Status with Labels
|
||||||
|
|
||||||
|
Each individual device is represented by a `netalertx_device_status` metric, with descriptive labels:
|
||||||
|
|
||||||
|
* `device`: friendly name of the device
|
||||||
|
* `mac`: MAC address (or placeholder)
|
||||||
|
* `ip`: last recorded IP address
|
||||||
|
* `vendor`: manufacturer or "None" if unknown
|
||||||
|
* `first_connection`: timestamp when the device was first observed
|
||||||
|
* `last_connection`: most recent contact timestamp
|
||||||
|
* `dev_type`: device category or type
|
||||||
|
* `device_status`: current status (Online / Offline / Archived / Down / ...)
|
||||||
|
|
||||||
|
The metric value is always `1` (indicating presence or active state) and the combination of labels identifies the device.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### How to Query with `curl`
|
||||||
|
|
||||||
|
To fetch the metrics from the NetAlertX exporter:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl 'http://<server_ip>:<GRAPHQL_PORT>/metrics' \
|
||||||
|
-H 'Authorization: Bearer <API_TOKEN>' \
|
||||||
|
-H 'Accept: text/plain'
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
|
||||||
|
* `<server_ip>`: IP or hostname of the NetAlertX server
|
||||||
|
* `<GRAPHQL_PORT>`: port specified in your `GRAPHQL_PORT` setting (default: `20212`)
|
||||||
|
* `<API_TOKEN>` your Bearer token from the `API_TOKEN` setting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
* **Endpoint**: `/metrics` provides both summary counters and per-device status entries.
|
||||||
|
* **Aggregate metrics** help monitor overall device states.
|
||||||
|
* **Detailed metrics** expose each device’s metadata via labels.
|
||||||
|
* **Use case**: feed into Prometheus for scraping, monitoring, alerting, or charting dashboard views.
|
||||||
|
|
||||||
|
### Prometheus Scraping Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'netalertx'
|
||||||
|
metrics_path: /metrics
|
||||||
|
scheme: http
|
||||||
|
scrape_interval: 60s
|
||||||
|
static_configs:
|
||||||
|
- targets: ['<server_ip>:<GRAPHQL_PORT>']
|
||||||
|
authorization:
|
||||||
|
type: Bearer
|
||||||
|
credentials: <API_TOKEN>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Grafana template
|
||||||
|
|
||||||
|
Grafana template sample: [Download json](./samples/API/Grafana_Dashboard.json)
|
||||||
|
|
||||||
## API Endpoint: /log files
|
## API Endpoint: /log files
|
||||||
|
|
||||||
This API endpoint retrieves files from the `/app/log` folder.
|
This API endpoint retrieves files from the `/app/log` folder.
|
||||||
|
|||||||
111
docs/DEVICE_HEURISTICS.md
Executable file
111
docs/DEVICE_HEURISTICS.md
Executable file
@@ -0,0 +1,111 @@
|
|||||||
|
# Device Heuristics: Icon and Type Guessing
|
||||||
|
|
||||||
|
This module is responsible for inferring the most likely **device type** and **icon** based on minimal identifying data like MAC address, vendor, IP, or device name.
|
||||||
|
|
||||||
|
It does this using a set of heuristics defined in an external JSON rules file, which it evaluates **in priority order**.
|
||||||
|
|
||||||
|
>[!NOTE]
|
||||||
|
> You can find the full source code of the heuristics module in the `device_heuristics.py` file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## JSON Rule Format
|
||||||
|
|
||||||
|
Rules are defined in a file called `device_heuristics_rules.json` (located under `/back`), structured like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"dev_type": "Phone",
|
||||||
|
"icon_html": "<i class=\"fa-brands fa-apple\"></i>",
|
||||||
|
"matching_pattern": [
|
||||||
|
{ "mac_prefix": "001A79", "vendor": "Apple" }
|
||||||
|
],
|
||||||
|
"name_pattern": ["iphone", "pixel"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
>[!NOTE]
|
||||||
|
> Feel free to raise a PR in case you'd like to add any rules into the `device_heuristics_rules.json` file. Please place new rules into the correct position and consider the priority of already available rules.
|
||||||
|
|
||||||
|
### Supported fields:
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| ------------------ | -------------------- | --------------------------------------------------------------- |
|
||||||
|
| `dev_type` | `string` | Type to assign if rule matches (e.g. `"Gateway"`, `"Phone"`) |
|
||||||
|
| `icon_html` | `string` | Icon (HTML string) to assign if rule matches. Encoded to base64 at load time. |
|
||||||
|
| `matching_pattern` | `array` | List of `{ mac_prefix, vendor }` objects for first strict and then loose matching |
|
||||||
|
| `name_pattern` | `array` *(optional)* | List of lowercase substrings (used with regex) |
|
||||||
|
| `ip_pattern` | `array` *(optional)* | Regex patterns to match IPs |
|
||||||
|
|
||||||
|
**Order in this array defines priority** — rules are checked top-down and short-circuit on first match.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Matching Flow (in Priority Order)
|
||||||
|
|
||||||
|
The function `guess_device_attributes(...)` runs a series of matching functions in strict order:
|
||||||
|
|
||||||
|
1. MAC + Vendor → `match_mac_and_vendor()`
|
||||||
|
2. Vendor only → `match_vendor()`
|
||||||
|
3. Name pattern → `match_name()`
|
||||||
|
4. IP pattern → `match_ip()`
|
||||||
|
5. Final fallback → defaults defined in the `NEWDEV_devIcon` and `NEWDEV_devType` settings.
|
||||||
|
|
||||||
|
### Use of default values
|
||||||
|
|
||||||
|
The guessing process runs for every device **as long as the current type or icon still matches the default values**. Even if earlier heuristics return a match, the system continues evaluating additional clues — like name or IP — to try and replace placeholders.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Still considered a match attempt if current values are defaults
|
||||||
|
if (not type_ or type_ == default_type) or (not icon or icon == default_icon):
|
||||||
|
type_, icon = match_ip(ip, default_type, default_icon)
|
||||||
|
```
|
||||||
|
|
||||||
|
In other words: if the type or icon is still `"unknown"` (or matches the default), the system assumes the match isn’t final — and keeps looking. It stops only when both values are non-default (defaults are defined in the `NEWDEV_devIcon` and `NEWDEV_devType` settings).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Match Behavior (per function)
|
||||||
|
|
||||||
|
These functions are executed in the following order:
|
||||||
|
|
||||||
|
### `match_mac_and_vendor(mac_clean, vendor, ...)`
|
||||||
|
|
||||||
|
* Looks for MAC prefix **and** vendor substring match
|
||||||
|
* Most precise
|
||||||
|
* Stops as soon as a match is found
|
||||||
|
|
||||||
|
### `match_vendor(vendor, ...)`
|
||||||
|
|
||||||
|
* Falls back to substring match on vendor only
|
||||||
|
* Ignores rules where `mac_prefix` is present (ensures this is really a fallback)
|
||||||
|
|
||||||
|
### `match_name(name, ...)`
|
||||||
|
|
||||||
|
* Lowercase name is compared against all `name_pattern` values using regex
|
||||||
|
* Good for user-assigned labels (e.g. "AP Office", "iPhone")
|
||||||
|
|
||||||
|
### `match_ip(ip, ...)`
|
||||||
|
|
||||||
|
* If IP is present and matches regex patterns under any rule, it returns that type/icon
|
||||||
|
* Usually used for gateways or local IP ranges
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
|
||||||
|
* Each rule can define an `icon_html`, which is converted to a `icon_base64` on load
|
||||||
|
* If missing, it falls back to the passed-in `default_icon` (`NEWDEV_devIcon` setting)
|
||||||
|
* If a match is found but icon is still blank, default is used
|
||||||
|
|
||||||
|
**TL;DR:** Type and icon must both be matched. If only one is matched, the other falls back to the default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority Mechanics
|
||||||
|
|
||||||
|
* JSON rules are evaluated **top-to-bottom**
|
||||||
|
* Matching is **first-hit wins** — no scoring, no weights
|
||||||
|
* Rules that are more specific (e.g. exact MAC prefixes) should be listed earlier
|
||||||
1110
docs/samples/API/Grafana_Dashboard.json
Executable file
1110
docs/samples/API/Grafana_Dashboard.json
Executable file
File diff suppressed because it is too large
Load Diff
@@ -387,6 +387,16 @@ body
|
|||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plugin-content #tabs-location .nav-tabs-custom > .nav-tabs > li
|
||||||
|
{
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-content .left-nav
|
||||||
|
{
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
.pa-small-box-2 .inner h3 {
|
.pa-small-box-2 .inner h3 {
|
||||||
margin-left: 0em;
|
margin-left: 0em;
|
||||||
margin-bottom: 1.3em;
|
margin-bottom: 1.3em;
|
||||||
@@ -1411,6 +1421,7 @@ input[readonly] {
|
|||||||
.iconPreview svg{
|
.iconPreview svg{
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
max-width: 20px;
|
max-width: 20px;
|
||||||
|
margin-bottom: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1489,7 +1500,7 @@ input[readonly] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#tableDevicesBox td svg, #tableDevicesBox td i{
|
#tableDevicesBox td svg, #tableDevicesBox td i{
|
||||||
height: 1.5em !important;
|
height: 1em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#TileCards .tile .inner
|
#TileCards .tile .inner
|
||||||
@@ -1649,6 +1660,21 @@ input[readonly] {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-badge a
|
||||||
|
{
|
||||||
|
color: #fff !important;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.custom-badge
|
||||||
|
{
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-style: solid;
|
||||||
|
padding: 0 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
#deviceDetailsEdit .form-control
|
#deviceDetailsEdit .form-control
|
||||||
{
|
{
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
@@ -2114,16 +2140,16 @@ input[readonly] {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pa_semitransparent-panel {
|
.nax_semitransparent-panel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
opacity: 0.8;
|
opacity: 0.5;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pa_spinner {
|
.nax_spinner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100px;
|
top: 100px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|||||||
@@ -744,7 +744,7 @@ table.dataTable tbody tr.selected, table.dataTable tbody tr .selected
|
|||||||
top: 0.01em;
|
top: 0.01em;
|
||||||
font-size: 3.25em;
|
font-size: 3.25em;
|
||||||
}
|
}
|
||||||
.pa_semitransparent-panel{
|
.nax_semitransparent-panel{
|
||||||
background-color: #000 !important;
|
background-color: #000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
--color-yellow: #f39c12;
|
--color-yellow: #f39c12;
|
||||||
--color-red: #dd4b39;
|
--color-red: #dd4b39;
|
||||||
--color-gray: #8c8c8c;
|
--color-gray: #8c8c8c;
|
||||||
|
--color-white: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -746,7 +747,7 @@
|
|||||||
top: 0.01em;
|
top: 0.01em;
|
||||||
font-size: 3.25em;
|
font-size: 3.25em;
|
||||||
}
|
}
|
||||||
.pa_semitransparent-panel{
|
.nax_semitransparent-panel{
|
||||||
background-color: #000 !important;
|
background-color: #000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,5 +794,5 @@
|
|||||||
|
|
||||||
.btn:hover
|
.btn:hover
|
||||||
{
|
{
|
||||||
color: var(--color-gray);
|
color: var(--color-white);
|
||||||
}
|
}
|
||||||
@@ -443,6 +443,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// init first time
|
// init first time
|
||||||
initNmapButtons();
|
// -----------------------------------------------------------
|
||||||
initCopyFromDevice();
|
var toolsPageInitialized = false;
|
||||||
|
|
||||||
|
function initDeviceToolsPage()
|
||||||
|
{
|
||||||
|
// Only proceed if .panTools is visible
|
||||||
|
if (!$('#panTools:visible').length) {
|
||||||
|
return; // exit early if nothing is visible
|
||||||
|
}
|
||||||
|
|
||||||
|
// init page once
|
||||||
|
if (toolsPageInitialized) return;
|
||||||
|
toolsPageInitialized = true;
|
||||||
|
|
||||||
|
initNmapButtons();
|
||||||
|
initCopyFromDevice();
|
||||||
|
|
||||||
|
hideSpinner();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Recurring function to monitor the URL and reinitialize if needed
|
||||||
|
function deviceToolsPageUpdater() {
|
||||||
|
initDeviceToolsPage();
|
||||||
|
|
||||||
|
// Run updater again after delay
|
||||||
|
setTimeout(deviceToolsPageUpdater, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// start updater
|
||||||
|
deviceToolsPageUpdater();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -503,36 +503,36 @@ function collectFilters() {
|
|||||||
function mapColumnIndexToFieldName(index, tableColumnVisible) {
|
function mapColumnIndexToFieldName(index, tableColumnVisible) {
|
||||||
// the order is important, don't change it!
|
// the order is important, don't change it!
|
||||||
const columnNames = [
|
const columnNames = [
|
||||||
"devName",
|
"devName", // 0
|
||||||
"devOwner",
|
"devOwner", // 1
|
||||||
"devType",
|
"devType", // 2
|
||||||
"devIcon",
|
"devIcon", // 3
|
||||||
"devFavorite",
|
"devFavorite", // 4
|
||||||
"devGroup",
|
"devGroup", // 5
|
||||||
"devFirstConnection",
|
"devFirstConnection", // 6
|
||||||
"devLastConnection",
|
"devLastConnection", // 7
|
||||||
"devLastIP",
|
"devLastIP", // 8
|
||||||
"devIsRandomMac", // resolved on the fly
|
"devIsRandomMac", // 9 resolved on the fly
|
||||||
"devStatus", // resolved on the fly
|
"devStatus", // 10 resolved on the fly
|
||||||
"devMac",
|
"devMac", // 11
|
||||||
"devIpLong", //formatIPlong(device.devLastIP) || "", // IP orderable
|
"devIpLong", // 12 formatIPlong(device.devLastIP) || "", // IP orderable
|
||||||
"rowid",
|
"rowid", // 13
|
||||||
"devParentMAC",
|
"devParentMAC", // 14
|
||||||
"devParentChildrenCount", // resolved on the fly
|
"devParentChildrenCount", // 15 resolved on the fly
|
||||||
"devLocation",
|
"devLocation", // 16
|
||||||
"devVendor",
|
"devVendor", // 17
|
||||||
"devParentPort",
|
"devParentPort", // 18
|
||||||
"devGUID",
|
"devGUID", // 19
|
||||||
"devSyncHubNode",
|
"devSyncHubNode", // 20
|
||||||
"devSite",
|
"devSite", // 21
|
||||||
"devSSID",
|
"devSSID", // 22
|
||||||
"devSourcePlugin",
|
"devSourcePlugin", // 23
|
||||||
"devPresentLastScan",
|
"devPresentLastScan", // 24
|
||||||
"devAlertDown",
|
"devAlertDown", // 25
|
||||||
"devCustomProps",
|
"devCustomProps", // 26
|
||||||
"devFQDN",
|
"devFQDN", // 27
|
||||||
"devParentRelType",
|
"devParentRelType", // 28
|
||||||
"devReqNicsOnline"
|
"devReqNicsOnline" // 29
|
||||||
];
|
];
|
||||||
|
|
||||||
// console.log("OrderBy: " + columnNames[tableColumnOrder[index]]);
|
// console.log("OrderBy: " + columnNames[tableColumnOrder[index]]);
|
||||||
@@ -899,6 +899,28 @@ function initializeDatatable (status) {
|
|||||||
}
|
}
|
||||||
} },
|
} },
|
||||||
|
|
||||||
|
// Parent Mac
|
||||||
|
{targets: [mapIndx(14)],
|
||||||
|
'createdCell': function (td, cellData, rowData, row, col) {
|
||||||
|
if (!isValidMac(cellData)) {
|
||||||
|
$(td).html('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
id: cellData, // MAC address
|
||||||
|
text: cellData // Optional display text (you could use a name or something else)
|
||||||
|
};
|
||||||
|
|
||||||
|
spanWrap = $(`<span class="custom-badge text-white"></span>`)
|
||||||
|
|
||||||
|
$(td).html(spanWrap);
|
||||||
|
|
||||||
|
const chipHtml = renderDeviceLink(data, spanWrap, true); // pass the td as container
|
||||||
|
|
||||||
|
$(spanWrap).append(chipHtml);
|
||||||
|
}
|
||||||
|
},
|
||||||
// Status color
|
// Status color
|
||||||
{targets: [mapIndx(10)],
|
{targets: [mapIndx(10)],
|
||||||
'createdCell': function (td, cellData, rowData, row, col) {
|
'createdCell': function (td, cellData, rowData, row, col) {
|
||||||
|
|||||||
@@ -715,45 +715,7 @@ function initSelect2() {
|
|||||||
{
|
{
|
||||||
var selectEl = $(this).select2({
|
var selectEl = $(this).select2({
|
||||||
templateSelection: function (data, container) {
|
templateSelection: function (data, container) {
|
||||||
if (!data.id) return data.text; // default for placeholder etc.
|
return $(renderDeviceLink(data, container));
|
||||||
|
|
||||||
const device = getDevDataByMac(data.id);
|
|
||||||
|
|
||||||
const badge = getStatusBadgeParts(
|
|
||||||
device.devPresentLastScan,
|
|
||||||
device.devAlertDown,
|
|
||||||
device.devMac
|
|
||||||
)
|
|
||||||
|
|
||||||
$(container).addClass(badge.cssClass);
|
|
||||||
|
|
||||||
// Custom HTML
|
|
||||||
const html = $(`
|
|
||||||
<a href="${badge.url}" target="_blank">
|
|
||||||
<span class="custom-chip hover-node-info"
|
|
||||||
data-name="${device.devName}"
|
|
||||||
data-ip="${device.devLastIP}"
|
|
||||||
data-mac="${device.devMac}"
|
|
||||||
data-vendor="${device.devVendor}"
|
|
||||||
data-type="${device.devType}"
|
|
||||||
data-lastseen="${device.devLastConnection}"
|
|
||||||
data-firstseen="${device.devFirstConnection}"
|
|
||||||
data-relationship="${device.devParentRelType}"
|
|
||||||
data-status="${device.devStatus}"
|
|
||||||
data-present="${device.devPresentLastScan}"
|
|
||||||
data-alert="${device.devAlertDown}"
|
|
||||||
data-icon="${device.devIcon}"
|
|
||||||
>
|
|
||||||
<span class="iconPreview">${atob(device.devIcon)}</span>
|
|
||||||
${data.text}
|
|
||||||
<span>
|
|
||||||
(${badge.iconHtml})
|
|
||||||
</span
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
`);
|
|
||||||
|
|
||||||
return html;
|
|
||||||
},
|
},
|
||||||
escapeMarkup: function (m) {
|
escapeMarkup: function (m) {
|
||||||
return m; // Allow HTML
|
return m; // Allow HTML
|
||||||
@@ -817,6 +779,50 @@ function initSelect2() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------
|
||||||
|
// Render a device link with hover-over functionality
|
||||||
|
function renderDeviceLink(data, container, useName = false) {
|
||||||
|
if (!data.id) return data.text; // default placeholder etc.
|
||||||
|
|
||||||
|
const device = getDevDataByMac(data.id);
|
||||||
|
|
||||||
|
const badge = getStatusBadgeParts(
|
||||||
|
device.devPresentLastScan,
|
||||||
|
device.devAlertDown,
|
||||||
|
device.devMac
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add badge class and hover-info class to container
|
||||||
|
$(container)
|
||||||
|
.addClass(`${badge.cssClass} hover-node-info`)
|
||||||
|
.attr({
|
||||||
|
'data-name': device.devName,
|
||||||
|
'data-ip': device.devLastIP,
|
||||||
|
'data-mac': device.devMac,
|
||||||
|
'data-vendor': device.devVendor,
|
||||||
|
'data-type': device.devType,
|
||||||
|
'data-lastseen': device.devLastConnection,
|
||||||
|
'data-firstseen': device.devFirstConnection,
|
||||||
|
'data-relationship': device.devParentRelType,
|
||||||
|
'data-status': device.devStatus,
|
||||||
|
'data-present': device.devPresentLastScan,
|
||||||
|
'data-alert': device.devAlertDown,
|
||||||
|
'data-icon': device.devIcon
|
||||||
|
});
|
||||||
|
|
||||||
|
return `
|
||||||
|
<a href="${badge.url}" target="_blank">
|
||||||
|
<span class="custom-chip">
|
||||||
|
<span class="iconPreview">${atob(device.devIcon)}</span>
|
||||||
|
${useName ? device.devName : data.text}
|
||||||
|
<span>
|
||||||
|
(${badge.iconHtml})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
// Display device info on hover (attach only once)
|
// Display device info on hover (attach only once)
|
||||||
function initHoverNodeInfo() {
|
function initHoverNodeInfo() {
|
||||||
|
|||||||
@@ -185,6 +185,12 @@ $db->close();
|
|||||||
</div>
|
</div>
|
||||||
<div class="db_tools_table_cell_b"><?= lang('Maintenance_Tool_del_ActHistory_text');?></div>
|
<div class="db_tools_table_cell_b"><?= lang('Maintenance_Tool_del_ActHistory_text');?></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="db_info_table_row">
|
||||||
|
<div class="db_tools_table_cell_a" >
|
||||||
|
<button type="button" class="btn btn-default pa-btn pa-btn-delete bg-red dbtools-button" id="btnRestartServer" onclick="askRestartBackend()"><?= lang('Maint_RestartServer');?></button>
|
||||||
|
</div>
|
||||||
|
<div class="db_tools_table_cell_b"><?= lang('Maint_Restart_Server_noti_text');?></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -139,8 +139,8 @@
|
|||||||
<body class="hold-transition fixed <?php echo $pia_skin_selected;?> theme-<?php echo $UI_THEME;?> sidebar-mini" onLoad="update_servertime();" >
|
<body class="hold-transition fixed <?php echo $pia_skin_selected;?> theme-<?php echo $UI_THEME;?> sidebar-mini" onLoad="update_servertime();" >
|
||||||
|
|
||||||
<div id="loadingSpinner">
|
<div id="loadingSpinner">
|
||||||
<div class="pa_semitransparent-panel"></div>
|
<div class="nax_semitransparent-panel"></div>
|
||||||
<div class="panel panel-default pa_spinner">
|
<div class="panel panel-default nax_spinner">
|
||||||
<table>
|
<table>
|
||||||
<td id="loadingSpinnerText" width="130px" ></td>
|
<td id="loadingSpinnerText" width="130px" ></td>
|
||||||
<td><i class="fa-solid fa-spinner fa-spin-pulse"></i></td>
|
<td><i class="fa-solid fa-spinner fa-spin-pulse"></i></td>
|
||||||
@@ -436,8 +436,24 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<!-- system info menu item -->
|
<!-- system info menu item -->
|
||||||
<li class=" <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('systeminfo.php') ) ){ echo 'active'; } ?>">
|
<li class=" treeview <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('systeminfo.php') ) ){ echo 'active menu-open'; } ?>">
|
||||||
<a href="systeminfo.php"><i class="fa fa-fw fa-info-circle"></i> <span><?= lang('Navigation_SystemInfo');?></span></a>
|
<a href="#">
|
||||||
|
<i class="fa fa-fw fa-info-circle"></i> <span><?= lang('Navigation_SystemInfo');?></span>
|
||||||
|
<span class="pull-right-container">
|
||||||
|
<i class="fa fa-angle-left pull-right"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<ul class="treeview-menu " style="display: <?php if (in_array (basename($_SERVER['SCRIPT_NAME']), array('systeminfo.php') ) ){ echo 'block'; } else {echo 'none';} ?>;">
|
||||||
|
<li>
|
||||||
|
<a href="systeminfo.php#panServer" onclick="setCache('activeSysinfoTab','tabServer');initializeTabs()"><?= lang('Systeminfo_System');?></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="systeminfo.php#panNetwork" onclick="setCache('activeSysinfoTab','tabNetwork');initializeTabs()"><?= lang('Systeminfo_Network');?></a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="systeminfo.php#panStorage" onclick="setCache('activeSysinfoTab','tabStorage');initializeTabs()"><?= lang('Systeminfo_Storage');?></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
@@ -450,24 +466,6 @@
|
|||||||
|
|
||||||
<script defer>
|
<script defer>
|
||||||
|
|
||||||
// Generate work-in-progress icons
|
|
||||||
function workInProgress() {
|
|
||||||
|
|
||||||
if($(".work-in-progress").length > 0 && $(".work-in-progress").html().trim() == "")
|
|
||||||
{
|
|
||||||
$(".work-in-progress").append(`
|
|
||||||
<a href="https://github.com/jokob-sk/NetAlertX/issues" target="_blank">
|
|
||||||
<b class="pointer" title="${getString("Gen_Work_In_Progress")}">🦺</b>
|
|
||||||
</a>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//--------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
//--------------------------------------------------------------
|
|
||||||
|
|
||||||
function toggleFullscreen() {
|
function toggleFullscreen() {
|
||||||
|
|
||||||
if (document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
@@ -485,6 +483,5 @@ function workInProgress() {
|
|||||||
|
|
||||||
// Update server state in the header
|
// Update server state in the header
|
||||||
updateState()
|
updateState()
|
||||||
workInProgress()
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -301,7 +301,7 @@
|
|||||||
"Gen_Cancel": "Annuler",
|
"Gen_Cancel": "Annuler",
|
||||||
"Gen_Change": "Changement",
|
"Gen_Change": "Changement",
|
||||||
"Gen_Copy": "Lancer",
|
"Gen_Copy": "Lancer",
|
||||||
"Gen_CopyToClipboard": "",
|
"Gen_CopyToClipboard": "Copier vers le presse-papier",
|
||||||
"Gen_DataUpdatedUITakesTime": "OK - cela peut prendre du temps à l'interface pour se mettre à jour si un scan est en cours.",
|
"Gen_DataUpdatedUITakesTime": "OK - cela peut prendre du temps à l'interface pour se mettre à jour si un scan est en cours.",
|
||||||
"Gen_Delete": "Supprimer",
|
"Gen_Delete": "Supprimer",
|
||||||
"Gen_DeleteAll": "Supprimer tous",
|
"Gen_DeleteAll": "Supprimer tous",
|
||||||
|
|||||||
@@ -301,7 +301,7 @@
|
|||||||
"Gen_Cancel": "Annulla",
|
"Gen_Cancel": "Annulla",
|
||||||
"Gen_Change": "Modifica",
|
"Gen_Change": "Modifica",
|
||||||
"Gen_Copy": "Esegui",
|
"Gen_Copy": "Esegui",
|
||||||
"Gen_CopyToClipboard": "",
|
"Gen_CopyToClipboard": "Copia negli appunti",
|
||||||
"Gen_DataUpdatedUITakesTime": "OK: l'aggiornamento dell'interfaccia utente potrebbe richiedere del tempo se è in esecuzione una scansione.",
|
"Gen_DataUpdatedUITakesTime": "OK: l'aggiornamento dell'interfaccia utente potrebbe richiedere del tempo se è in esecuzione una scansione.",
|
||||||
"Gen_Delete": "Elimina",
|
"Gen_Delete": "Elimina",
|
||||||
"Gen_DeleteAll": "Elimina tutti",
|
"Gen_DeleteAll": "Elimina tutti",
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
// show spinning icon
|
// show spinning icon
|
||||||
showSpinner()
|
showSpinner()
|
||||||
|
|
||||||
//var selectedTab = 'tabServer';
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Page ------------------------------------------------------------------ -->
|
<!-- Page ------------------------------------------------------------------ -->
|
||||||
|
|||||||
@@ -9,11 +9,33 @@
|
|||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
function getExternalIp() {
|
||||||
|
$ch = curl_init('https://api64.ipify.org?format=json');
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
if (curl_errno($ch)) {
|
||||||
|
curl_close($ch);
|
||||||
|
return 'ERROR: ' . curl_error($ch);
|
||||||
|
}
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
if (isset($data['ip'])) {
|
||||||
|
return htmlspecialchars($data['ip']);
|
||||||
|
}
|
||||||
|
return 'ERROR: Invalid response';
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// Network
|
// Network
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
||||||
//Network stats
|
//Network stats
|
||||||
|
// Server IP
|
||||||
|
|
||||||
|
$externalIp = getExternalIp();
|
||||||
|
|
||||||
// Check Server name
|
// Check Server name
|
||||||
if (!empty(gethostname())) { $network_NAME = gethostname(); } else { $network_NAME = lang('Systeminfo_Network_Server_Name_String'); }
|
if (!empty(gethostname())) { $network_NAME = gethostname(); } else { $network_NAME = lang('Systeminfo_Network_Server_Name_String'); }
|
||||||
// Check HTTPS
|
// Check HTTPS
|
||||||
@@ -100,7 +122,7 @@ echo '<div class="box box-solid">
|
|||||||
<div class="box-body">
|
<div class="box-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_IP') . '</div>
|
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_IP') . '</div>
|
||||||
<div class="col-sm-9 sysinfo_network_b">' . shell_exec("curl https://ifconfig.co") . '</div>
|
<div class="col-sm-9 sysinfo_network_b" id="external-ip">' .$externalIp. '</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_IP_Connection') . '</div>
|
<div class="col-sm-3 sysinfo_network_a">' . lang('Systeminfo_Network_IP_Connection') . '</div>
|
||||||
@@ -169,7 +191,7 @@ echo '<div class="box box-solid">
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
|
|
||||||
<!-- DataTable initialization -->
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -242,8 +264,6 @@ function fetchUsedIps(callback) {
|
|||||||
function renderAvailableIpsTable(allIps, usedIps) {
|
function renderAvailableIpsTable(allIps, usedIps) {
|
||||||
const availableIps = allIps.filter(row => !usedIps.includes(row.ip));
|
const availableIps = allIps.filter(row => !usedIps.includes(row.ip));
|
||||||
|
|
||||||
console.log(allIps);
|
|
||||||
console.log(usedIps);
|
|
||||||
console.log(availableIps);
|
console.log(availableIps);
|
||||||
|
|
||||||
$('#availableIpsTable').DataTable({
|
$('#availableIpsTable').DataTable({
|
||||||
@@ -274,19 +294,22 @@ function renderAvailableIpsTable(allIps, usedIps) {
|
|||||||
|
|
||||||
// INIT
|
// INIT
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
|
// available IPs
|
||||||
fetchUsedIps(usedIps => {
|
fetchUsedIps(usedIps => {
|
||||||
const allIps = inferNetworkRange(usedIps);
|
const allIps = inferNetworkRange(usedIps);
|
||||||
renderAvailableIpsTable(allIps, usedIps);
|
renderAvailableIpsTable(allIps, usedIps);
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
// Available IPs datatable
|
||||||
$('#networkTable').DataTable({
|
$('#networkTable').DataTable({
|
||||||
searching: true,
|
searching: true,
|
||||||
order: [[0, "desc"]],
|
order: [[0, "desc"]],
|
||||||
initComplete: function(settings, json) {
|
initComplete: function(settings, json) {
|
||||||
hideSpinner(); // Called after the DataTable is fully initialized
|
hideSpinner(); // Called after the DataTable is fully initialized
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,5 +30,5 @@ source myenv/bin/activate
|
|||||||
update-alternatives --install /usr/bin/python python /usr/bin/python3 10
|
update-alternatives --install /usr/bin/python python /usr/bin/python3 10
|
||||||
|
|
||||||
# install packages thru pip3
|
# install packages thru pip3
|
||||||
pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git
|
pip3 install openwrt-luci-rpc asusrouter asyncio aiohttp graphene flask flask-cors tplink-omada-client wakeonlan pycryptodome requests paho-mqtt scapy cron-converter pytz json2table dhcp-leases pyunifi speedtest-cli chardet python-nmap dnspython librouteros yattag git+https://github.com/foreign-sub/aiofreepybox.git
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ nav:
|
|||||||
- Database: DATABASE.md
|
- Database: DATABASE.md
|
||||||
- Settings: SETTINGS_SYSTEM.md
|
- Settings: SETTINGS_SYSTEM.md
|
||||||
- Versions: VERSIONS.md
|
- Versions: VERSIONS.md
|
||||||
|
- Icon and Type guessing: DEVICE_HEURISTICS.md
|
||||||
- Integrations:
|
- Integrations:
|
||||||
- Webhook Secret: WEBHOOK_SECRET.md
|
- Webhook Secret: WEBHOOK_SECRET.md
|
||||||
- API: API.md
|
- API: API.md
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from models.user_events_queue_instance import UserEventsQueueInstance
|
|||||||
from messaging.in_app import write_notification
|
from messaging.in_app import write_notification
|
||||||
|
|
||||||
# Import the start_server function
|
# Import the start_server function
|
||||||
from graphql_server.graphql_server_start import start_server
|
from api_server.api_server_start import start_server
|
||||||
|
|
||||||
apiEndpoints = []
|
apiEndpoints = []
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import threading
|
import threading
|
||||||
from flask import Flask, request, jsonify
|
from flask import Flask, request, jsonify, Response
|
||||||
|
from flask_cors import CORS
|
||||||
from .graphql_schema import devicesSchema
|
from .graphql_schema import devicesSchema
|
||||||
|
from .prometheus_metrics import getMetricStats
|
||||||
from graphene import Schema
|
from graphene import Schema
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
@@ -15,9 +17,11 @@ from messaging.in_app import write_notification
|
|||||||
|
|
||||||
# Flask application
|
# Flask application
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
CORS(app, resources={r"/metrics": {"origins": "*"}}, supports_credentials=True, allow_headers=["Authorization"])
|
||||||
|
|
||||||
# Retrieve API token and port
|
# --------------------------
|
||||||
graphql_port_value = get_setting_value("GRAPHQL_PORT")
|
# GraphQL Endpoints
|
||||||
|
# --------------------------
|
||||||
|
|
||||||
# Endpoint used when accessed via browser
|
# Endpoint used when accessed via browser
|
||||||
@app.route("/graphql", methods=["GET"])
|
@app.route("/graphql", methods=["GET"])
|
||||||
@@ -29,10 +33,7 @@ def graphql_debug():
|
|||||||
@app.route("/graphql", methods=["POST"])
|
@app.route("/graphql", methods=["POST"])
|
||||||
def graphql_endpoint():
|
def graphql_endpoint():
|
||||||
# Check for API token in headers
|
# Check for API token in headers
|
||||||
incoming_header_token = request.headers.get("Authorization")
|
if not is_authorized():
|
||||||
api_token_value = get_setting_value("API_TOKEN")
|
|
||||||
|
|
||||||
if incoming_header_token != f"Bearer {api_token_value}":
|
|
||||||
msg = '[graphql_server] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct.'
|
msg = '[graphql_server] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct.'
|
||||||
mylog('verbose', [msg])
|
mylog('verbose', [msg])
|
||||||
return jsonify({"error": msg}), 401
|
return jsonify({"error": msg}), 401
|
||||||
@@ -47,6 +48,32 @@ def graphql_endpoint():
|
|||||||
# Return the result as JSON
|
# Return the result as JSON
|
||||||
return jsonify(result.data)
|
return jsonify(result.data)
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Prometheus /metrics Endpoint
|
||||||
|
# --------------------------
|
||||||
|
|
||||||
|
@app.route("/metrics")
|
||||||
|
def metrics():
|
||||||
|
|
||||||
|
# Check for API token in headers
|
||||||
|
if not is_authorized():
|
||||||
|
msg = '[metrics] Unauthorized access attempt - make sure your GRAPHQL_PORT and API_TOKEN settings are correct.'
|
||||||
|
mylog('verbose', [msg])
|
||||||
|
return jsonify({"error": msg}), 401
|
||||||
|
|
||||||
|
|
||||||
|
# Return Prometheus metrics as plain text
|
||||||
|
return Response(getMetricStats(), mimetype="text/plain")
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# Background Server Start
|
||||||
|
# --------------------------
|
||||||
|
def is_authorized():
|
||||||
|
token = request.headers.get("Authorization")
|
||||||
|
return token == f"Bearer {get_setting_value('API_TOKEN')}"
|
||||||
|
|
||||||
|
|
||||||
def start_server(graphql_port, app_state):
|
def start_server(graphql_port, app_state):
|
||||||
"""Start the GraphQL server in a background thread."""
|
"""Start the GraphQL server in a background thread."""
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ class Query(ObjectType):
|
|||||||
searchable_fields = [
|
searchable_fields = [
|
||||||
"devName", "devMac", "devOwner", "devType", "devVendor", "devLastIP",
|
"devName", "devMac", "devOwner", "devType", "devVendor", "devLastIP",
|
||||||
"devGroup", "devComments", "devLocation", "devStatus", "devSSID",
|
"devGroup", "devComments", "devLocation", "devStatus", "devSSID",
|
||||||
"devSite", "devSourcePlugin", "devSyncHubNode", "devFQDN", "devParentRelType"
|
"devSite", "devSourcePlugin", "devSyncHubNode", "devFQDN", "devParentRelType", "devParentMAC"
|
||||||
]
|
]
|
||||||
|
|
||||||
search_term = options.search.lower()
|
search_term = options.search.lower()
|
||||||
76
server/api_server/prometheus_metrics.py
Executable file
76
server/api_server/prometheus_metrics.py
Executable file
@@ -0,0 +1,76 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Register NetAlertX directories
|
||||||
|
INSTALL_PATH = "/app"
|
||||||
|
sys.path.extend([f"{INSTALL_PATH}/server"])
|
||||||
|
|
||||||
|
from logger import mylog
|
||||||
|
from const import apiPath
|
||||||
|
from helper import is_random_mac, get_number_of_children, format_ip_long, get_setting_value
|
||||||
|
|
||||||
|
def escape_label_value(val):
|
||||||
|
"""
|
||||||
|
Escape special characters for Prometheus labels.
|
||||||
|
"""
|
||||||
|
return str(val).replace('\\', '\\\\').replace('\n', '\\n').replace('"', '\\"')
|
||||||
|
|
||||||
|
# Define a base URL with the user's home directory
|
||||||
|
folder = apiPath
|
||||||
|
|
||||||
|
def getMetricStats():
|
||||||
|
output = []
|
||||||
|
|
||||||
|
# 1. Dashboard totals
|
||||||
|
try:
|
||||||
|
with open(folder + 'table_devices_tiles.json', 'r') as f:
|
||||||
|
tiles_data = json.load(f)["data"]
|
||||||
|
|
||||||
|
if isinstance(tiles_data, list) and tiles_data:
|
||||||
|
totals = tiles_data[0]
|
||||||
|
output.append(f'netalertx_connected_devices {totals.get("connected", 0)}')
|
||||||
|
output.append(f'netalertx_offline_devices {totals.get("offline", 0)}')
|
||||||
|
output.append(f'netalertx_down_devices {totals.get("down", 0)}')
|
||||||
|
output.append(f'netalertx_new_devices {totals.get("new", 0)}')
|
||||||
|
output.append(f'netalertx_archived_devices {totals.get("archived", 0)}')
|
||||||
|
output.append(f'netalertx_favorite_devices {totals.get("favorites", 0)}')
|
||||||
|
output.append(f'netalertx_my_devices {totals.get("my_devices", 0)}')
|
||||||
|
else:
|
||||||
|
output.append("# Unexpected format in table_devices_tiles.json")
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||||
|
mylog('none', f'[metrics] Error loading tiles data: {e}')
|
||||||
|
output.append(f"# Error loading tiles data: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
output.append(f"# General error loading dashboard totals: {e}")
|
||||||
|
|
||||||
|
# 2. Device-level metrics
|
||||||
|
try:
|
||||||
|
with open(folder + 'table_devices.json', 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
devices = data.get("data", [])
|
||||||
|
|
||||||
|
for row in devices:
|
||||||
|
name = escape_label_value(row.get("devName", "unknown"))
|
||||||
|
mac = escape_label_value(row.get("devMac", "unknown"))
|
||||||
|
ip = escape_label_value(row.get("devLastIP", "unknown"))
|
||||||
|
vendor = escape_label_value(row.get("devVendor", "unknown"))
|
||||||
|
first_conn = escape_label_value(row.get("devFirstConnection", "unknown"))
|
||||||
|
last_conn = escape_label_value(row.get("devLastConnection", "unknown"))
|
||||||
|
dev_type = escape_label_value(row.get("devType", "unknown"))
|
||||||
|
raw_status = row.get("devStatus", "Unknown")
|
||||||
|
dev_status = raw_status.replace("-", "").capitalize()
|
||||||
|
|
||||||
|
output.append(
|
||||||
|
f'netalertx_device_status{{device="{name}", mac="{mac}", ip="{ip}", vendor="{vendor}", '
|
||||||
|
f'first_connection="{first_conn}", last_connection="{last_conn}", dev_type="{dev_type}", '
|
||||||
|
f'device_status="{dev_status}"}} 1'
|
||||||
|
)
|
||||||
|
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||||
|
mylog('none', f'[metrics] Error loading devices data: {e}')
|
||||||
|
output.append(f"# Error loading devices data: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
output.append(f"# General error processing device metrics: {e}")
|
||||||
|
|
||||||
|
return "\n".join(output) + "\n"
|
||||||
@@ -667,7 +667,10 @@ def checkNewVersion():
|
|||||||
buildTimestamp = int(f.read().strip())
|
buildTimestamp = int(f.read().strip())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get("https://api.github.com/repos/jokob-sk/NetAlertX/releases")
|
response = requests.get(
|
||||||
|
"https://api.github.com/repos/jokob-sk/NetAlertX/releases",
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
response.raise_for_status() # Raise an exception for HTTP errors
|
response.raise_for_status() # Raise an exception for HTTP errors
|
||||||
text = response.text
|
text = response.text
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ def importConfigs (db, all_plugins):
|
|||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
# ccd(key, default, config_dir, name, inputtype, options, group, events=[], desc = "", regex = "", setJsonMetadata = {}, overrideTemplate = {})
|
# ccd(key, default, config_dir, name, inputtype, options, group, events=[], desc = "", regex = "", setJsonMetadata = {}, overrideTemplate = {})
|
||||||
|
|
||||||
conf.LOADED_PLUGINS = ccd('LOADED_PLUGINS', [] , c_d, 'Loaded plugins', '{"dataType":"array", "elements": [{"elementType" : "select", "elementOptions" : [{"multiple":"true", "ordeable": "true"}] ,"transformers": []}]}', '[]', 'General')
|
conf.LOADED_PLUGINS = ccd('LOADED_PLUGINS', [] , c_d, 'Loaded plugins', '{"dataType":"array","elements":[{"elementType":"select","elementOptions":[{"multiple":"true","ordeable":"true"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-12"},{"onClick":"selectChange(this)"},{"getStringKey":"Gen_Change"}],"transformers":[]}]}', '[]', 'General')
|
||||||
conf.DISCOVER_PLUGINS = ccd('DISCOVER_PLUGINS', True , c_d, 'Discover plugins', """{"dataType": "boolean","elements": [{"elementType": "input","elementOptions": [{ "type": "checkbox" }],"transformers": []}]}""", '[]', 'General')
|
conf.DISCOVER_PLUGINS = ccd('DISCOVER_PLUGINS', True , c_d, 'Discover plugins', """{"dataType": "boolean","elements": [{"elementType": "input","elementOptions": [{ "type": "checkbox" }],"transformers": []}]}""", '[]', 'General')
|
||||||
conf.SCAN_SUBNETS = ccd('SCAN_SUBNETS', ['192.168.1.0/24 --interface=eth1', '192.168.1.0/24 --interface=eth0'] , c_d, 'Subnets to scan', '''{"dataType": "array","elements": [{"elementType": "input","elementOptions": [{"placeholder": "192.168.1.0/24 --interface=eth1"},{"suffix": "_in"},{"cssClasses": "col-sm-10"},{"prefillValue": "null"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": ["_in"]},{"separator": ""},{"cssClasses": "col-xs-12"},{"onClick": "addList(this, false)"},{"getStringKey": "Gen_Add"}],"transformers": []},{"elementType": "select","elementHasInputValue": 1,"elementOptions": [{"multiple": "true"},{"readonly": "true"},{"editable": "true"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": []},{"separator": ""},{"cssClasses": "col-xs-6"},{"onClick": "removeAllOptions(this)"},{"getStringKey": "Gen_Remove_All"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": []},{"separator": ""},{"cssClasses": "col-xs-6"},{"onClick": "removeFromList(this)"},{"getStringKey": "Gen_Remove_Last"}],"transformers": []}]}''', '[]', 'General')
|
conf.SCAN_SUBNETS = ccd('SCAN_SUBNETS', ['192.168.1.0/24 --interface=eth1', '192.168.1.0/24 --interface=eth0'] , c_d, 'Subnets to scan', '''{"dataType": "array","elements": [{"elementType": "input","elementOptions": [{"placeholder": "192.168.1.0/24 --interface=eth1"},{"suffix": "_in"},{"cssClasses": "col-sm-10"},{"prefillValue": "null"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": ["_in"]},{"separator": ""},{"cssClasses": "col-xs-12"},{"onClick": "addList(this, false)"},{"getStringKey": "Gen_Add"}],"transformers": []},{"elementType": "select","elementHasInputValue": 1,"elementOptions": [{"multiple": "true"},{"readonly": "true"},{"editable": "true"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": []},{"separator": ""},{"cssClasses": "col-xs-6"},{"onClick": "removeAllOptions(this)"},{"getStringKey": "Gen_Remove_All"}],"transformers": []},{"elementType": "button","elementOptions": [{"sourceSuffixes": []},{"separator": ""},{"cssClasses": "col-xs-6"},{"onClick": "removeFromList(this)"},{"getStringKey": "Gen_Remove_Last"}],"transformers": []}]}''', '[]', 'General')
|
||||||
conf.LOG_LEVEL = ccd('LOG_LEVEL', 'verbose' , c_d, 'Log verboseness', '{"dataType":"string", "elements": [{"elementType" : "select", "elementOptions" : [] ,"transformers": []}]}', "['none', 'minimal', 'verbose', 'debug', 'trace']", 'General')
|
conf.LOG_LEVEL = ccd('LOG_LEVEL', 'verbose' , c_d, 'Log verboseness', '{"dataType":"string", "elements": [{"elementType" : "select", "elementOptions" : [] ,"transformers": []}]}', "['none', 'minimal', 'verbose', 'debug', 'trace']", 'General')
|
||||||
@@ -171,7 +171,7 @@ def importConfigs (db, all_plugins):
|
|||||||
conf.REFRESH_FQDN = ccd('REFRESH_FQDN', False , c_d, 'Refresh FQDN', """{"dataType": "boolean","elements": [{"elementType": "input","elementOptions": [{ "type": "checkbox" }],"transformers": []}]}""", '[]', 'General')
|
conf.REFRESH_FQDN = ccd('REFRESH_FQDN', False , c_d, 'Refresh FQDN', """{"dataType": "boolean","elements": [{"elementType": "input","elementOptions": [{ "type": "checkbox" }],"transformers": []}]}""", '[]', 'General')
|
||||||
conf.API_CUSTOM_SQL = ccd('API_CUSTOM_SQL', 'SELECT * FROM Devices WHERE devPresentLastScan = 0' , c_d, 'Custom endpoint', '{"dataType":"string", "elements": [{"elementType" : "input", "elementOptions" : [] ,"transformers": []}]}', '[]', 'General')
|
conf.API_CUSTOM_SQL = ccd('API_CUSTOM_SQL', 'SELECT * FROM Devices WHERE devPresentLastScan = 0' , c_d, 'Custom endpoint', '{"dataType":"string", "elements": [{"elementType" : "input", "elementOptions" : [] ,"transformers": []}]}', '[]', 'General')
|
||||||
conf.VERSION = ccd('VERSION', '' , c_d, 'Version', '{"dataType":"string", "elements": [{"elementType" : "input", "elementOptions" : [{ "readonly": "true" }] ,"transformers": []}]}', '', 'General')
|
conf.VERSION = ccd('VERSION', '' , c_d, 'Version', '{"dataType":"string", "elements": [{"elementType" : "input", "elementOptions" : [{ "readonly": "true" }] ,"transformers": []}]}', '', 'General')
|
||||||
conf.NETWORK_DEVICE_TYPES = ccd('NETWORK_DEVICE_TYPES', ['AP', 'Gateway', 'Firewall', 'Hypervisor', 'Powerline', 'Switch', 'WLAN', 'PLC', 'Router','USB LAN Adapter', 'USB WIFI Adapter', 'Internet'] , c_d, 'Network device types', '{"dataType":"array","elements":[{"elementType":"input","elementOptions":[{"placeholder":"Enter value"},{"suffix":"_in"},{"cssClasses":"col-sm-10"},{"prefillValue":"null"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":["_in"]},{"separator":""},{"cssClasses":"col-xs-12"},{"onClick":"addList(this,false)"},{"getStringKey":"Gen_Add"}],"transformers":[]},{"elementType":"select", "elementHasInputValue":1,"elementOptions":[{"multiple":"true"},{"readonly":"true"},{"editable":"true"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeAllOptions(this)"},{"getStringKey":"Gen_Remove_All"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeFromList(this)"},{"getStringKey":"Gen_Remove_Last"}],"transformers":[]}]}', '[]', 'General')
|
conf.NETWORK_DEVICE_TYPES = ccd('NETWORK_DEVICE_TYPES', ['AP', 'Access Point', 'Gateway', 'Firewall', 'Hypervisor', 'Powerline', 'Switch', 'WLAN', 'PLC', 'Router','USB LAN Adapter', 'USB WIFI Adapter', 'Internet'] , c_d, 'Network device types', '{"dataType":"array","elements":[{"elementType":"input","elementOptions":[{"placeholder":"Enter value"},{"suffix":"_in"},{"cssClasses":"col-sm-10"},{"prefillValue":"null"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":["_in"]},{"separator":""},{"cssClasses":"col-xs-12"},{"onClick":"addList(this,false)"},{"getStringKey":"Gen_Add"}],"transformers":[]},{"elementType":"select", "elementHasInputValue":1,"elementOptions":[{"multiple":"true"},{"readonly":"true"},{"editable":"true"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeAllOptions(this)"},{"getStringKey":"Gen_Remove_All"}],"transformers":[]},{"elementType":"button","elementOptions":[{"sourceSuffixes":[]},{"separator":""},{"cssClasses":"col-xs-6"},{"onClick":"removeFromList(this)"},{"getStringKey":"Gen_Remove_Last"}],"transformers":[]}]}', '[]', 'General')
|
||||||
conf.GRAPHQL_PORT = ccd('GRAPHQL_PORT', 20212 , c_d, 'GraphQL port', '{"dataType":"integer", "elements": [{"elementType" : "input", "elementOptions" : [{"type": "number"}] ,"transformers": []}]}', '[]', 'General')
|
conf.GRAPHQL_PORT = ccd('GRAPHQL_PORT', 20212 , c_d, 'GraphQL port', '{"dataType":"integer", "elements": [{"elementType" : "input", "elementOptions" : [{"type": "number"}] ,"transformers": []}]}', '[]', 'General')
|
||||||
conf.API_TOKEN = ccd('API_TOKEN', 't_' + generate_random_string(20) , c_d, 'API token', '{"dataType": "string","elements": [{"elementType": "input","elementHasInputValue": 1,"elementOptions": [{ "cssClasses": "col-xs-12" }],"transformers": []},{"elementType": "button","elementOptions": [{ "getStringKey": "Gen_Generate" },{ "customParams": "API_TOKEN" },{ "onClick": "generateApiToken(this, 20)" },{ "cssClasses": "col-xs-12" }],"transformers": []}]}', '[]', 'General')
|
conf.API_TOKEN = ccd('API_TOKEN', 't_' + generate_random_string(20) , c_d, 'API token', '{"dataType": "string","elements": [{"elementType": "input","elementHasInputValue": 1,"elementOptions": [{ "cssClasses": "col-xs-12" }],"transformers": []},{"elementType": "button","elementOptions": [{ "getStringKey": "Gen_Generate" },{ "customParams": "API_TOKEN" },{ "onClick": "generateApiToken(this, 20)" },{ "cssClasses": "col-xs-12" }],"transformers": []}]}', '[]', 'General')
|
||||||
|
|
||||||
|
|||||||
@@ -447,8 +447,8 @@ def create_new_devices (db):
|
|||||||
cur_MAC, cur_Name, cur_Vendor, cur_ScanMethod, cur_IP, cur_SyncHubNodeName, cur_NetworkNodeMAC, cur_PORT, cur_NetworkSite, cur_SSID, cur_Type = row
|
cur_MAC, cur_Name, cur_Vendor, cur_ScanMethod, cur_IP, cur_SyncHubNodeName, cur_NetworkNodeMAC, cur_PORT, cur_NetworkSite, cur_SSID, cur_Type = row
|
||||||
|
|
||||||
# Handle NoneType
|
# Handle NoneType
|
||||||
cur_Name = cur_Name.strip() if cur_Name else '(unknown)'
|
cur_Name = str(cur_Name).strip() if cur_Name else '(unknown)'
|
||||||
cur_Type = cur_Type.strip() if cur_Type else get_setting_value("NEWDEV_devType")
|
cur_Type = str(cur_Type).strip() if cur_Type else get_setting_value("NEWDEV_devType")
|
||||||
cur_NetworkNodeMAC = cur_NetworkNodeMAC.strip() if cur_NetworkNodeMAC else ''
|
cur_NetworkNodeMAC = cur_NetworkNodeMAC.strip() if cur_NetworkNodeMAC else ''
|
||||||
cur_NetworkNodeMAC = cur_NetworkNodeMAC if cur_NetworkNodeMAC and cur_MAC != "Internet" else (get_setting_value("NEWDEV_devParentMAC") if cur_MAC != "Internet" else "null")
|
cur_NetworkNodeMAC = cur_NetworkNodeMAC if cur_NetworkNodeMAC and cur_MAC != "Internet" else (get_setting_value("NEWDEV_devParentMAC") if cur_MAC != "Internet" else "null")
|
||||||
cur_SyncHubNodeName = cur_SyncHubNodeName if cur_SyncHubNodeName and cur_SyncHubNodeName != "null" else (get_setting_value("SYNC_node_name"))
|
cur_SyncHubNodeName = cur_SyncHubNodeName if cur_SyncHubNodeName and cur_SyncHubNodeName != "null" else (get_setting_value("SYNC_node_name"))
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
from pathlib import Path
|
||||||
from typing import Optional, List, Tuple, Dict
|
from typing import Optional, List, Tuple, Dict
|
||||||
|
|
||||||
# Register NetAlertX directories
|
# Register NetAlertX directories
|
||||||
@@ -11,57 +14,167 @@ from const import *
|
|||||||
from logger import mylog
|
from logger import mylog
|
||||||
from helper import timeNowTZ, get_setting_value
|
from helper import timeNowTZ, get_setting_value
|
||||||
|
|
||||||
|
# Load MAC/device-type/icon rules from external file
|
||||||
|
MAC_TYPE_ICON_PATH = Path(f"{INSTALL_PATH}/back/device_heuristics_rules.json")
|
||||||
|
try:
|
||||||
|
with open(MAC_TYPE_ICON_PATH, "r", encoding="utf-8") as f:
|
||||||
|
MAC_TYPE_ICON_RULES = json.load(f)
|
||||||
|
# Precompute base64-encoded icon_html once for each rule
|
||||||
|
for rule in MAC_TYPE_ICON_RULES:
|
||||||
|
icon_html = rule.get("icon_html", "")
|
||||||
|
if icon_html:
|
||||||
|
# encode icon_html to base64 string
|
||||||
|
b64_bytes = base64.b64encode(icon_html.encode("utf-8"))
|
||||||
|
rule["icon_base64"] = b64_bytes.decode("utf-8")
|
||||||
|
else:
|
||||||
|
rule["icon_base64"] = ""
|
||||||
|
except Exception as e:
|
||||||
|
MAC_TYPE_ICON_RULES = []
|
||||||
|
mylog('none', f"[guess_device_attributes] Failed to load device_heuristics_rules.json: {e}")
|
||||||
|
|
||||||
|
# -----------------------------------------
|
||||||
|
# Match device type and base64-encoded icon using MAC prefix and vendor patterns.
|
||||||
|
def match_mac_and_vendor(
|
||||||
|
mac_clean: str,
|
||||||
|
vendor: str,
|
||||||
|
default_type: str,
|
||||||
|
default_icon: str
|
||||||
|
) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Match device type and base64-encoded icon using MAC prefix and vendor patterns.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mac_clean: Cleaned MAC address (uppercase, no colons).
|
||||||
|
vendor: Normalized vendor name (lowercase).
|
||||||
|
default_type: Fallback device type.
|
||||||
|
default_icon: Fallback base64 icon.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple containing (device_type, base64_icon)
|
||||||
|
"""
|
||||||
|
for rule in MAC_TYPE_ICON_RULES:
|
||||||
|
dev_type = rule.get("dev_type")
|
||||||
|
base64_icon = rule.get("icon_base64", "")
|
||||||
|
patterns = rule.get("matching_pattern", [])
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
mac_prefix = pattern.get("mac_prefix", "").upper()
|
||||||
|
vendor_pattern = pattern.get("vendor", "").lower()
|
||||||
|
|
||||||
|
if mac_clean.startswith(mac_prefix):
|
||||||
|
if not vendor_pattern or vendor_pattern in vendor:
|
||||||
|
|
||||||
|
mylog('debug', f"[guess_device_attributes] Matched via MAC+Vendor")
|
||||||
|
|
||||||
|
type_ = dev_type
|
||||||
|
icon = base64_icon or default_icon
|
||||||
|
return type_, icon
|
||||||
|
|
||||||
|
return default_type, default_icon
|
||||||
|
|
||||||
|
# ---------------------------------------------------
|
||||||
|
# Match device type and base64-encoded icon using vendor patterns.
|
||||||
|
def match_vendor(
|
||||||
|
vendor: str,
|
||||||
|
default_type: str,
|
||||||
|
default_icon: str
|
||||||
|
) -> Tuple[str, str]:
|
||||||
|
|
||||||
|
vendor_lc = vendor.lower()
|
||||||
|
|
||||||
|
for rule in MAC_TYPE_ICON_RULES:
|
||||||
|
dev_type = rule.get("dev_type")
|
||||||
|
base64_icon = rule.get("icon_base64", "")
|
||||||
|
patterns = rule.get("matching_pattern", [])
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
# Only apply fallback when no MAC prefix is specified
|
||||||
|
mac_prefix = pattern.get("mac_prefix", "")
|
||||||
|
vendor_pattern = pattern.get("vendor", "").lower()
|
||||||
|
|
||||||
|
if vendor_pattern and vendor_pattern in vendor_lc:
|
||||||
|
|
||||||
|
mylog('debug', f"[guess_device_attributes] Matched via Vendor")
|
||||||
|
|
||||||
|
icon = base64_icon or default_icon
|
||||||
|
|
||||||
|
return dev_type, icon
|
||||||
|
|
||||||
|
return default_type, default_icon
|
||||||
|
|
||||||
|
# ---------------------------------------------------
|
||||||
|
# Match device type and base64-encoded icon using name patterns.
|
||||||
|
def match_name(
|
||||||
|
name: str,
|
||||||
|
default_type: str,
|
||||||
|
default_icon: str
|
||||||
|
) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Match device type and base64-encoded icon using name patterns from global MAC_TYPE_ICON_RULES.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Normalized device name (lowercase).
|
||||||
|
default_type: Fallback device type.
|
||||||
|
default_icon: Fallback base64 icon.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple containing (device_type, base64_icon)
|
||||||
|
"""
|
||||||
|
name_lower = name.lower() if name else ""
|
||||||
|
|
||||||
|
for rule in MAC_TYPE_ICON_RULES:
|
||||||
|
dev_type = rule.get("dev_type")
|
||||||
|
base64_icon = rule.get("icon_base64", "")
|
||||||
|
name_patterns = rule.get("name_pattern", [])
|
||||||
|
|
||||||
|
for pattern in name_patterns:
|
||||||
|
# Use regex search to allow pattern substrings
|
||||||
|
if re.search(pattern, name_lower, re.IGNORECASE):
|
||||||
|
|
||||||
|
mylog('debug', f"[guess_device_attributes] Matched via Name")
|
||||||
|
|
||||||
|
type_ = dev_type
|
||||||
|
icon = base64_icon or default_icon
|
||||||
|
return type_, icon
|
||||||
|
|
||||||
|
return default_type, default_icon
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Base64 encoded HTML strings for FontAwesome icons, now with an extended icons dictionary for broader device coverage
|
#
|
||||||
ICONS = {
|
def match_ip(
|
||||||
"globe": "PGkgY2xhc3M9ImZhcyBmYS1nbG9iZSI+PC9pPg==", # Internet or global network
|
ip: str,
|
||||||
"phone": "PGkgY2xhc3M9ImZhcyBmYS1tb2JpbGUtYWx0Ij48L2k+", # Smartphone
|
default_type: str,
|
||||||
"laptop": "PGkgY2xhc3M9ImZhIGZhLWxhcHRvcCI+PC9pPg==", # Laptop
|
default_icon: str
|
||||||
"printer": "PGkgY2xhc3M9ImZhIGZhLXByaW50ZXIiPjwvaT4=", # Printer
|
) -> Tuple[str, str]:
|
||||||
"router": "PGkgY2xhc3M9ImZhcyBmYS1yYW5kb20iPjwvaT4=", # Router or network switch
|
"""
|
||||||
"tv": "PGkgY2xhc3M9ImZhIGZhLXR2Ij48L2k+", # Television
|
Match device type and base64-encoded icon using IP regex patterns from global JSON.
|
||||||
"desktop": "PGkgY2xhc3M9ImZhIGZhLWRlc2t0b3AiPjwvaT4=", # Desktop PC
|
|
||||||
"tablet": "PGkgY2xhc3M9ImZhIGZhLXRhYmxldCI+PC9pPg==", # Tablet
|
|
||||||
"watch": "PGkgY2xhc3M9ImZhcyBmYS1jbG9jayI+PC9pPg==", # Fallback to clock since smartwatch is nonfree in FontAwesome
|
|
||||||
"camera": "PGkgY2xhc3M9ImZhIGZhLWNhbWVyYSI+PC9pPg==", # Camera or webcam
|
|
||||||
"home": "PGkgY2xhc3M9ImZhIGZhLWhvbWUiPjwvaT4=", # Smart home device
|
|
||||||
"apple": "PGkgY2xhc3M9ImZhYiBmYS1hcHBsZSI+PC9pPg==", # Apple device
|
|
||||||
"ethernet": "PGkgY2xhc3M9ImZhcyBmYS1uZXR3b3JrLXdpcmVkIj48L2k+", # Free alternative for ethernet icon in FontAwesome
|
|
||||||
"google": "PGkgY2xhc3M9ImZhYiBmYS1nb29nbGUiPjwvaT4=", # Google device
|
|
||||||
"raspberry": "PGkgY2xhc3M9ImZhYiBmYS1yYXNwYmVycnktcGkiPjwvaT4=", # Raspberry Pi
|
|
||||||
"microchip": "PGkgY2xhc3M9ImZhcyBmYS1taWNyb2NoaXAiPjwvaT4=", # IoT or embedded device
|
|
||||||
"server": "PGkgY2xhc3M9ImZhcyBmYS1zZXJ2ZXIiPjwvaT4=", # Server
|
|
||||||
"gamepad": "PGkgY2xhc3M9ImZhcyBmYS1nYW1lcGFkIj48L2k+", # Gaming console
|
|
||||||
"lightbulb": "PGkgY2xhc3M9ImZhcyBmYS1saWdodGJ1bGIiPjwvaT4=", # Smart light
|
|
||||||
"speaker": "PGkgY2xhc3M9ImZhcyBmYS12b2x1bWUtdXAiPjwvaT4=", # Free speaker alt icon for smart speakers in FontAwesome
|
|
||||||
"lock": "PGkgY2xhc3M9ImZhcyBmYS1sb2NrIj48L2k+", # Security device
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extended device types for comprehensive classification
|
Args:
|
||||||
DEVICE_TYPES = {
|
ip: Device IP address as string.
|
||||||
"Internet": "Internet Gateway",
|
default_type: Fallback device type.
|
||||||
"Phone": "Smartphone",
|
default_icon: Fallback base64 icon.
|
||||||
"Laptop": "Laptop",
|
|
||||||
"Printer": "Printer",
|
|
||||||
"Router": "Router",
|
|
||||||
"TV": "Television",
|
|
||||||
"Desktop": "Desktop PC",
|
|
||||||
"Tablet": "Tablet",
|
|
||||||
"Smartwatch": "Smartwatch",
|
|
||||||
"Camera": "Camera",
|
|
||||||
"SmartHome": "Smart Home Device",
|
|
||||||
"Server": "Server",
|
|
||||||
"GamingConsole": "Gaming Console",
|
|
||||||
"IoT": "IoT Device",
|
|
||||||
"NetworkSwitch": "Network Switch",
|
|
||||||
"AccessPoint": "Access Point",
|
|
||||||
"SmartLight": "Smart Light",
|
|
||||||
"SmartSpeaker": "Smart Speaker",
|
|
||||||
"SecurityDevice": "Security Device",
|
|
||||||
"Unknown": "Unknown Device",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple containing (device_type, base64_icon)
|
||||||
|
"""
|
||||||
|
if not ip:
|
||||||
|
return default_type, default_icon
|
||||||
|
|
||||||
|
for rule in MAC_TYPE_ICON_RULES:
|
||||||
|
ip_patterns = rule.get("ip_pattern", [])
|
||||||
|
dev_type = rule.get("dev_type")
|
||||||
|
base64_icon = rule.get("icon_base64", "")
|
||||||
|
|
||||||
|
for pattern in ip_patterns:
|
||||||
|
if re.match(pattern, ip):
|
||||||
|
|
||||||
|
mylog('debug', f"[guess_device_attributes] Matched via IP")
|
||||||
|
|
||||||
|
type_ = dev_type
|
||||||
|
icon = base64_icon or default_icon
|
||||||
|
return type_, icon
|
||||||
|
|
||||||
|
return default_type, default_icon
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Guess device attributes such as type of device and associated device icon
|
# Guess device attributes such as type of device and associated device icon
|
||||||
@@ -72,197 +185,46 @@ def guess_device_attributes(
|
|||||||
name: Optional[str],
|
name: Optional[str],
|
||||||
default_icon: str,
|
default_icon: str,
|
||||||
default_type: str
|
default_type: str
|
||||||
) -> Tuple[str, str]:
|
) -> Tuple[str, str]:
|
||||||
"""
|
|
||||||
Guess the appropriate FontAwesome icon and device type based on device attributes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
vendor: Device vendor name.
|
|
||||||
mac: Device MAC address.
|
|
||||||
ip: Device IP address.
|
|
||||||
name: Device name.
|
|
||||||
default_icon: Default icon to return if no match is found.
|
|
||||||
default_type: Default type to return if no match is found.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple[str, str]: A tuple containing the guessed icon (Base64-encoded HTML string)
|
|
||||||
and the guessed device type (string).
|
|
||||||
"""
|
|
||||||
mylog('debug', f"[guess_device_attributes] Guessing attributes for (vendor|mac|ip|name): ('{vendor}'|'{mac}'|'{ip}'|'{name}')")
|
mylog('debug', f"[guess_device_attributes] Guessing attributes for (vendor|mac|ip|name): ('{vendor}'|'{mac}'|'{ip}'|'{name}')")
|
||||||
# Normalize inputs
|
|
||||||
|
# --- Normalize inputs ---
|
||||||
vendor = str(vendor).lower().strip() if vendor else "unknown"
|
vendor = str(vendor).lower().strip() if vendor else "unknown"
|
||||||
mac = str(mac).upper().strip() if mac else "00:00:00:00:00:00"
|
mac = str(mac).upper().strip() if mac else "00:00:00:00:00:00"
|
||||||
ip = str(ip).strip() if ip else "169.254.0.0" # APIPA address for unknown IPs per RFC 3927
|
ip = str(ip).strip() if ip else "169.254.0.0"
|
||||||
name = str(name).lower().strip() if name else "(unknown)"
|
name = str(name).lower().strip() if name else "(unknown)"
|
||||||
|
mac_clean = mac.replace(':', '').replace('-', '').upper()
|
||||||
|
|
||||||
# --- Icon Guessing Logic ---
|
# # Internet shortcut
|
||||||
if mac == "INTERNET":
|
# if mac == "INTERNET":
|
||||||
icon = ICONS.get("globe", default_icon)
|
# return ICONS.get("globe", default_icon), DEVICE_TYPES.get("Internet", default_type)
|
||||||
else:
|
|
||||||
# Vendor-based icon guessing
|
|
||||||
icon_vendor_patterns = {
|
|
||||||
"apple": "apple",
|
|
||||||
"samsung|motorola|xiaomi|huawei": "phone",
|
|
||||||
"dell|lenovo|asus|acer": "laptop",
|
|
||||||
"hp|epson|canon|brother": "printer",
|
|
||||||
"cisco|ubiquiti|netgear|tp-link|d-link|mikrotik": "router",
|
|
||||||
"lg|samsung electronics|sony|vizio": "tv",
|
|
||||||
"raspberry pi": "raspberry",
|
|
||||||
"google": "google",
|
|
||||||
"espressif|particle": "microchip",
|
|
||||||
"intel|amd": "desktop",
|
|
||||||
"amazon": "speaker",
|
|
||||||
"philips hue|lifx": "lightbulb",
|
|
||||||
"aruba|meraki": "ethernet",
|
|
||||||
"qnap|synology": "server",
|
|
||||||
"nintendo|sony interactive|microsoft": "gamepad",
|
|
||||||
"ring|blink|arlo": "camera",
|
|
||||||
"nest": "home",
|
|
||||||
}
|
|
||||||
for pattern, icon_key in icon_vendor_patterns.items():
|
|
||||||
if re.search(pattern, vendor, re.IGNORECASE):
|
|
||||||
icon = ICONS.get(icon_key, default_icon)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# MAC-based icon guessing
|
|
||||||
mac_clean = mac.replace(':', '').replace('-', '').upper()
|
|
||||||
icon_mac_patterns = {
|
|
||||||
"001A79|B0BE83|BC926B": "apple",
|
|
||||||
"001B63|BC4C4C": "tablet",
|
|
||||||
"74ACB9|002468": "ethernet",
|
|
||||||
"B827EB": "raspberry",
|
|
||||||
"001422|001874": "desktop",
|
|
||||||
"001CBF|002186": "server",
|
|
||||||
}
|
|
||||||
for pattern_str, icon_key in icon_mac_patterns.items():
|
|
||||||
patterns = [p.replace(':', '').replace('-', '').upper() for p in pattern_str.split('|')]
|
|
||||||
if any(mac_clean.startswith(p) for p in patterns):
|
|
||||||
icon = ICONS.get(icon_key, default_icon)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Name-based icon guessing
|
|
||||||
icon_name_patterns = {
|
|
||||||
"iphone|ipad|macbook|imac": "apple",
|
|
||||||
"pixel|galaxy|redmi": "phone",
|
|
||||||
"laptop|notebook": "laptop",
|
|
||||||
"printer|print": "printer",
|
|
||||||
"router|gateway|ap|access[ -]?point": "router",
|
|
||||||
"tv|television|smarttv": "tv",
|
|
||||||
"desktop|pc|computer": "desktop",
|
|
||||||
"tablet|pad": "tablet",
|
|
||||||
"watch|wear": "watch",
|
|
||||||
"camera|cam|webcam": "camera",
|
|
||||||
"echo|alexa|dot": "speaker",
|
|
||||||
"hue|lifx|bulb": "lightbulb",
|
|
||||||
"server|nas": "server",
|
|
||||||
"playstation|xbox|switch": "gamepad",
|
|
||||||
"raspberry|pi": "raspberry",
|
|
||||||
"google|chromecast|nest": "google",
|
|
||||||
"doorbell|lock|security": "lock",
|
|
||||||
}
|
|
||||||
for pattern, icon_key in icon_name_patterns.items():
|
|
||||||
if re.search(pattern, name, re.IGNORECASE):
|
|
||||||
icon = ICONS.get(icon_key, default_icon)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# IP-based icon guessing
|
|
||||||
icon_ip_patterns = {
|
|
||||||
r"^192\.168\.[0-1]\.1$": "router",
|
|
||||||
r"^10\.0\.0\.1$": "router",
|
|
||||||
r"^192\.168\.[0-1]\.[2-9]$": "desktop",
|
|
||||||
r"^192\.168\.[0-1]\.1\d{2}$": "phone",
|
|
||||||
}
|
|
||||||
for pattern, icon_key in icon_ip_patterns.items():
|
|
||||||
if re.match(pattern, ip):
|
|
||||||
icon = ICONS.get(icon_key, default_icon)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
icon = default_icon
|
|
||||||
|
|
||||||
# --- Type Guessing Logic ---
|
type_ = None
|
||||||
if mac == "INTERNET":
|
icon = None
|
||||||
type_ = DEVICE_TYPES.get("Internet", default_type)
|
|
||||||
else:
|
|
||||||
# Vendor-based type guessing
|
|
||||||
type_vendor_patterns = {
|
|
||||||
"apple|samsung|motorola|xiaomi|huawei": "Phone",
|
|
||||||
"dell|lenovo|asus|acer|hp": "Laptop",
|
|
||||||
"epson|canon|brother": "Printer",
|
|
||||||
"cisco|ubiquiti|netgear|tp-link|d-link|mikrotik|aruba|meraki": "Router",
|
|
||||||
"lg|samsung electronics|sony|vizio": "TV",
|
|
||||||
"raspberry pi": "IoT",
|
|
||||||
"google|nest": "SmartHome",
|
|
||||||
"espressif|particle": "IoT",
|
|
||||||
"intel|amd": "Desktop",
|
|
||||||
"amazon": "SmartSpeaker",
|
|
||||||
"philips hue|lifx": "SmartLight",
|
|
||||||
"qnap|synology": "Server",
|
|
||||||
"nintendo|sony interactive|microsoft": "GamingConsole",
|
|
||||||
"ring|blink|arlo": "Camera",
|
|
||||||
}
|
|
||||||
for pattern, type_key in type_vendor_patterns.items():
|
|
||||||
if re.search(pattern, vendor, re.IGNORECASE):
|
|
||||||
type_ = DEVICE_TYPES.get(type_key, default_type)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# MAC-based type guessing
|
|
||||||
mac_clean = mac.replace(':', '').replace('-', '').upper()
|
|
||||||
type_mac_patterns = {
|
|
||||||
"00:1A:79|B0:BE:83|BC:92:6B": "Phone",
|
|
||||||
"00:1B:63|BC:4C:4C": "Tablet",
|
|
||||||
"74:AC:B9|00:24:68": "AccessPoint",
|
|
||||||
"B8:27:EB": "IoT",
|
|
||||||
"00:14:22|00:18:74": "Desktop",
|
|
||||||
"00:1C:BF|00:21:86": "Server",
|
|
||||||
}
|
|
||||||
for pattern_str, type_key in type_mac_patterns.items():
|
|
||||||
patterns = [p.replace(':', '').replace('-', '').upper() for p in pattern_str.split('|')]
|
|
||||||
if any(mac_clean.startswith(p) for p in patterns):
|
|
||||||
type_ = DEVICE_TYPES.get(type_key, default_type)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Name-based type guessing
|
|
||||||
type_name_patterns = {
|
|
||||||
"iphone|ipad": "Phone",
|
|
||||||
"macbook|imac": "Laptop",
|
|
||||||
"pixel|galaxy|redmi": "Phone",
|
|
||||||
"laptop|notebook": "Laptop",
|
|
||||||
"printer|print": "Printer",
|
|
||||||
"router|gateway|ap|access[ -]?point": "Router",
|
|
||||||
"tv|television|smarttv": "TV",
|
|
||||||
"desktop|pc|computer": "Desktop",
|
|
||||||
"tablet|pad": "Tablet",
|
|
||||||
"watch|wear": "Smartwatch",
|
|
||||||
"camera|cam|webcam": "Camera",
|
|
||||||
"echo|alexa|dot": "SmartSpeaker",
|
|
||||||
"hue|lifx|bulb": "SmartLight",
|
|
||||||
"server|nas": "Server",
|
|
||||||
"playstation|xbox|switch": "GamingConsole",
|
|
||||||
"raspberry|pi": "IoT",
|
|
||||||
"google|chromecast|nest": "SmartHome",
|
|
||||||
"doorbell|lock|security": "SecurityDevice",
|
|
||||||
}
|
|
||||||
for pattern, type_key in type_name_patterns.items():
|
|
||||||
if re.search(pattern, name, re.IGNORECASE):
|
|
||||||
type_ = DEVICE_TYPES.get(type_key, default_type)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# IP-based type guessing
|
|
||||||
type_ip_patterns = {
|
|
||||||
r"^192\.168\.[0-1]\.1$": "Router",
|
|
||||||
r"^10\.0\.0\.1$": "Router",
|
|
||||||
r"^192\.168\.[0-1]\.[2-9]$": "Desktop",
|
|
||||||
r"^192\.168\.[0-1]\.1\d{2}$": "Phone",
|
|
||||||
}
|
|
||||||
for pattern, type_key in type_ip_patterns.items():
|
|
||||||
if re.match(pattern, ip):
|
|
||||||
type_ = DEVICE_TYPES.get(type_key, default_type)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
type_ = default_type
|
|
||||||
|
|
||||||
|
# --- Strict MAC + vendor rule matching from external file ---
|
||||||
|
type_, icon = match_mac_and_vendor(mac_clean, vendor, default_type, default_icon)
|
||||||
|
|
||||||
|
# --- Loose Vendor-based fallback ---
|
||||||
|
if not type_ or type_ == default_type:
|
||||||
|
type_, icon = match_vendor(vendor, default_type, default_icon)
|
||||||
|
|
||||||
|
# --- Loose Name-based fallback ---
|
||||||
|
if not type_ or type_ == default_type:
|
||||||
|
type_, icon = match_name(name, default_type, default_icon)
|
||||||
|
|
||||||
|
# --- Loose IP-based fallback ---
|
||||||
|
if (not type_ or type_ == default_type) or (not icon or icon == default_icon):
|
||||||
|
type_, icon = match_ip(ip, default_type, default_icon)
|
||||||
|
|
||||||
|
# Final fallbacks
|
||||||
|
type_ = type_ or default_type
|
||||||
|
icon = icon or default_icon
|
||||||
|
|
||||||
|
mylog('debug', f"[guess_device_attributes] Guessed attributes (icon|type_): ('{icon}'|'{type_}')")
|
||||||
return icon, type_
|
return icon, type_
|
||||||
|
|
||||||
|
|
||||||
# Deprecated functions with redirects (To be removed once all calls for these have been adjusted to use the updated function)
|
# Deprecated functions with redirects (To be removed once all calls for these have been adjusted to use the updated function)
|
||||||
def guess_icon(
|
def guess_icon(
|
||||||
vendor: Optional[str],
|
vendor: Optional[str],
|
||||||
@@ -308,7 +270,7 @@ def guess_type(
|
|||||||
default: Default type to return if no match is found.
|
default: Default type to return if no match is found.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Device type from DEVICE_TYPES dictionary.
|
str: Device type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_, type_ = guess_device_attributes(vendor, mac, ip, name, "unknown_icon", default)
|
_, type_ = guess_device_attributes(vendor, mac, ip, name, "unknown_icon", default)
|
||||||
|
|||||||
Reference in New Issue
Block a user