Compare commits

...

38 Commits

Author SHA1 Message Date
Deleted User
ab45f28027 Translated using Weblate (Ukrainian)
Some checks are pending
✅ Code checks / docker-tests (push) Waiting to run
✅ Code checks / check-url-paths (push) Waiting to run
✅ Code checks / lint (push) Waiting to run
🐳 👩‍💻 docker dev / docker_dev (push) Waiting to run
📘 Deploy MkDocs / deploy (push) Waiting to run
Currently translated at 96.5% (778 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/uk/
2026-04-09 00:10:16 +00:00
Adam Stańczyk
03891dd78d Translated using Weblate (Polish)
Currently translated at 83.7% (675 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/pl/
2026-04-09 00:10:14 +00:00
mid
9e605a593d Translated using Weblate (Japanese)
Currently translated at 99.7% (804 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ja/
2026-04-09 00:10:12 +00:00
Deleted User
8230b2380c Translated using Weblate (German)
Currently translated at 80.5% (649 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/de/
2026-04-09 00:10:09 +00:00
Deleted User
0c6f5b223f Translated using Weblate (Spanish)
Currently translated at 97.1% (783 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/es/
2026-04-09 00:10:07 +00:00
Ptsa Daniel
924d70d267 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 97.3% (785 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/zh_Hans/
2026-04-09 00:10:05 +00:00
GoldBull3t
33c3e2fef7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 50.2% (405 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/pt_BR/
2026-04-09 00:10:02 +00:00
HAMAD ABDULLA
940cd503d3 Translated using Weblate (Arabic)
Currently translated at 83.9% (677 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ar/
2026-04-09 00:10:00 +00:00
Massimo Pissarello
0e862845f4 Translated using Weblate (Italian)
Currently translated at 100.0% (806 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/it/
2026-04-09 00:09:58 +00:00
Bekir Kayra Çiğdem
68e5b3c877 Translated using Weblate (Turkish)
Currently translated at 55.8% (450 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/tr/
2026-04-09 00:09:56 +00:00
Safeguard
7827e24aae Translated using Weblate (Russian)
Currently translated at 99.7% (804 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ru/
2026-04-09 00:09:52 +00:00
Sylvain Pichon
3ff128e1f5 Translated using Weblate (French)
Currently translated at 99.5% (802 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/fr/
2026-04-09 00:09:50 +00:00
Jokob @NetAlertX
ef0d521e2b Merge pull request #1599 from netalertx/next_release
Fix set_alias 404 handling: simplify failure check to prevent silent regression
2026-04-09 10:08:44 +10:00
Jokob @NetAlertX
16e2249d0d Merge pull request #1600 from netalertx/copilot/fix-api-error-response
Some checks are pending
🐳 ⚠ docker-unsafe from next_release branch / docker_dev_unsafe (push) Waiting to run
Fix set-alias endpoint: return HTTP 200 with normalized `error` key on failure
2026-04-09 09:58:36 +10:00
copilot-swe-agent[bot]
a6d3b85614 Fix set-alias endpoint: return HTTP 200 with normalized error key on failure
Agent-Logs-Url: https://github.com/netalertx/NetAlertX/sessions/05ab18a3-4ac2-492d-bb80-67a1cc089bd9

Co-authored-by: jokob-sk <96159884+jokob-sk@users.noreply.github.com>
2026-04-08 23:28:50 +00:00
copilot-swe-agent[bot]
6eaa477ed3 Initial plan 2026-04-08 23:23:52 +00:00
copilot-swe-agent[bot]
e3a4c62d5f Fix set_alias 404 check: simplify to match generic updateDeviceColumn pattern
Agent-Logs-Url: https://github.com/netalertx/NetAlertX/sessions/661c66ce-45e8-4f96-b51d-1bb0b918c669

Co-authored-by: jokob-sk <96159884+jokob-sk@users.noreply.github.com>
2026-04-08 23:05:54 +00:00
Jokob @NetAlertX
da8b694a49 Update API documentation and schemas: clarify lock/unlock behavior for device fields and enhance error handling in device alias update 2026-04-08 22:38:43 +00:00
Jokob @NetAlertX
28e6d62ccb Update Device API documentation: clarify full-replace and partial-update semantics for device fields #1597 2026-04-08 22:34:08 +00:00
Jokob @NetAlertX
b530a6e635 Enhance API documentation and schemas: add new device field locking/unlocking endpoints and expand allowed column names for updates #1598 2026-04-08 21:57:58 +00:00
jokob-sk
548d237c38 DOCS: contributing
Some checks failed
✅ Code checks / check-url-paths (push) Has been cancelled
✅ Code checks / lint (push) Has been cancelled
✅ Code checks / docker-tests (push) Has been cancelled
🐳 👩‍💻 docker dev / docker_dev (push) Has been cancelled
📘 Deploy MkDocs / deploy (push) Has been cancelled
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-04-08 15:25:13 +10:00
jokob-sk
b96b170bed Merge branch 'main' of github.com:netalertx/NetAlertX
Some checks failed
✅ Code checks / check-url-paths (push) Has been cancelled
✅ Code checks / lint (push) Has been cancelled
✅ Code checks / docker-tests (push) Has been cancelled
🐳 👩‍💻 docker dev / docker_dev (push) Has been cancelled
📘 Deploy MkDocs / deploy (push) Has been cancelled
2026-04-08 09:41:26 +10:00
jokob-sk
da9c5f1bc7 DOCS: typos #1596
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-04-08 09:41:19 +10:00
anton garcias
2e8b793b44 Translated using Weblate (Catalan)
Some checks failed
✅ Code checks / check-url-paths (push) Has been cancelled
✅ Code checks / lint (push) Has been cancelled
✅ Code checks / docker-tests (push) Has been cancelled
🐳 👩‍💻 docker dev / docker_dev (push) Has been cancelled
📘 Deploy MkDocs / deploy (push) Has been cancelled
Currently translated at 99.8% (805 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/ca/
2026-04-07 18:54:29 +02:00
António Oliveira
3c8fd72049 Translated using Weblate (Portuguese (Portugal))
Some checks failed
✅ Code checks / check-url-paths (push) Has been cancelled
✅ Code checks / lint (push) Has been cancelled
✅ Code checks / docker-tests (push) Has been cancelled
🐳 👩‍💻 docker dev / docker_dev (push) Has been cancelled
📘 Deploy MkDocs / deploy (push) Has been cancelled
Currently translated at 73.5% (593 of 806 strings)

Translation: NetAlertX/core
Translate-URL: https://hosted.weblate.org/projects/pialert/core/pt_PT/
2026-04-07 05:34:53 +02:00
Jokob @NetAlertX
ae089f5ad9 Merge pull request #1592 from sebingel/fritzbox-plugin
Some checks failed
✅ Code checks / check-url-paths (push) Has been cancelled
✅ Code checks / lint (push) Has been cancelled
✅ Code checks / docker-tests (push) Has been cancelled
🐳 👩‍💻 docker dev / docker_dev (push) Has been cancelled
📘 Deploy MkDocs / deploy (push) Has been cancelled
feat(plugins): Add Fritz!Box device scanner plugin via TR-064 protocol
2026-04-06 21:03:35 +10:00
sebingel
4b6203a1d0 Add Fritz!Box plugin entry to docs/PLUGINS.md
The FRITZBOX plugin was not listed in the central plugin registry at
docs/PLUGINS.md. Requested by reviewer jokob-sk in PR #1592.

Changes:
- Add FRITZBOX entry to the Available Plugins table (docs/PLUGINS.md:60)
  Inserted alphabetically between FREEBOX and ICMP, with type 🔍
  (device_scanner) and a link to the plugin directory.
2026-04-06 10:47:10 +00:00
sebingel
13f840b9f2 Refactor Fritz!Box guest WiFi MAC generation to use string_to_fake_mac
The previous implementation derived the guest WiFi device MAC using a
custom MD5 hash of the Fritz!Box hardware MAC, producing a
locally-administered address with a 02: prefix. This was inconsistent
with the project-wide convention of using string_to_fake_mac() from
crypto_utils, which produces a fa:ce: prefixed address and is used by
all other plugins (nmap_dev_scan, adguard_import, pihole_api_scan, etc.).

A naive switch to string_to_fake_mac(host) would have introduced a
stability problem: if the user reconfigures FRITZBOX_HOST from an IP
address (e.g. 192.168.178.1) to a hostname (e.g. fritz.box), the fake
MAC would change and the guest device would re-appear as a new unknown
device in NetAlertX. The Fritz!Box hardware MAC is a stable identifier
that does not change with the configured host string.

Requested by reviewer jokob-sk in PR #1592.

Changes:
- Remove import hashlib (fritzbox.py:3) — no longer needed

- Add import string_to_fake_mac from utils.crypto_utils (fritzbox.py:15)

- Replace custom MD5-based MAC derivation in create_guest_wifi_device()
  with string_to_fake_mac(normalize_mac(fritzbox_mac)) (fritzbox.py:178)
  The Fritz!Box hardware MAC is fetched via TR-064 as before, but is now
  passed to the shared project utility instead of a custom hash.

- Add host parameter to create_guest_wifi_device(fc, host) (fritzbox.py:169)
  Used as fallback input to string_to_fake_mac() if the hardware MAC
  cannot be retrieved.

- Update call site in main() to pass host (fritzbox.py:224)

The guest WiFi device MAC is now stable across host configuration changes
and consistent with the fa:ce: prefix convention used across the project.
2026-04-06 10:37:51 +00:00
sebingel
ca9a0ef5ce Update Fritz!Box plugin README metadata
Requested by reviewer jokob-sk in PR #1592.

Changes:
- Replace generic author "NetAlertX Community" with @sebingel
  (README.md:204)

- Update release date from January 2026 to April 2026
  (README.md:205)

- Remove license field from version section (README.md:206)
  Project license is defined at repository level and does not need
  to be repeated in individual plugin READMEs.

- Update repository link from jokob-sk/NetAlertX to netalertx/NetAlertX
  (README.md:211)
  The project was transferred to the netalertx organisation; the
  canonical URL is now github.com/netalertx/NetAlertX.
2026-04-06 10:28:30 +00:00
sebingel
bc66575f91 Add fritzconnection to install-specific requirements files
The fritzconnection dependency was added to the top-level requirements.txt
when the Fritz!Box plugin was introduced, but the install-specific files
for Proxmox and Ubuntu 24 were not updated. Without the entry in these
files, fresh installations via the install scripts would not install the
dependency.

Requested by reviewer jokob-sk in PR #1592.

Changes:
- Add fritzconnection>=1.15.1 to install/proxmox/requirements.txt
- Add fritzconnection>=1.15.1 to install/ubuntu24/requirements.txt

All three requirements files now declare the fritzconnection dependency
consistently.
2026-04-06 10:20:54 +00:00
sebingel
ee42d8b56a Refactor Fritz!Box plugin: move imports to module level
The fritzconnection imports were originally placed inside the function
bodies as a defensive pattern: by catching ImportError locally,
get_fritzbox_connection() and get_connected_devices() could each return
None or an empty list with a user-friendly log message instead of
crashing at import time. This kept the plugin runnable even when the
dependency was missing.

Requested by reviewer jokob-sk in PR #1592: move all imports to the top
of the module, treating fritzconnection as a required dependency that is
assumed to be installed via requirements.txt.

Changes:
- Add top-level imports for FritzConnection and FritzHosts
  (fritzbox.py:16-17)

- Remove inline import and ImportError handler from
  get_fritzbox_connection() (fritzbox.py:48, 64-67)

- Remove inline import and ImportError handler from
  get_connected_devices() (fritzbox.py:79, 133-134)

Functional behavior of the plugin is unchanged.
2026-04-06 10:20:37 +00:00
jokob-sk
ae2dc92318 LANG: Finnish
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-04-06 19:15:05 +10:00
sebingel
706ef1a8a1 Fix incorrect connection status field mapping in Fritz!Box README
The device information table in README.md incorrectly stated that the
Connection Status field ("Active"/"Inactive") maps to devVendor in the
devices table. In reality, watchedValue2 has no mapped_to_column entry
in config.json, meaning the value is stored only in the plugin's own
Plugins_FRITZBOX table and never promoted to the Devices table. A user
following the documentation to filter or display Connection Status via
devVendor would find no data there.

Changes:
- Correct the "Mapped To" column for Connection Status (README.md:86)
  Changed from "`devVendor` (shown as vendor field)" to "Plugin table
  only (not mapped to device fields)" to accurately reflect config.json
  behavior.

Users now have a correct expectation: Connection Status is visible in
the Fritz!Box plugin view but not in standard device columns. No
functional code was changed.
2026-04-06 07:49:16 +00:00
sebingel
1d4fd09444 Fix robustness issues in Fritz!Box plugin before PR
Two independent reliability problems were identified during PR readiness
review. First, FritzConnection had no explicit timeout, meaning an
unreachable or slow Fritz!Box would block the plugin process indefinitely
until the OS TCP timeout fired (typically 2+ minutes), making the 60s
RUN_TIMEOUT in config.json ineffective. Second, hashlib.md5() called
without usedforsecurity=False raises ValueError on FIPS-enforced systems
(common in enterprise Docker hosts), silently breaking the guest WiFi
synthetic device feature for those users.

Changes:
- Add timeout=10 to FritzConnection(...) call (fritzbox.py:57)
  The fritzconnection library accepts a timeout parameter directly in
  __init__; it applies per individual HTTP request to the Fritz!Box,
  bounding each TR-064 call including the initial connection handshake.

- Add usedforsecurity=False to hashlib.md5() call (fritzbox.py:191)
  The MD5 hash is used only for deterministic MAC derivation (not for
  any security purpose), so the flag is semantically correct and lifts
  the FIPS restriction without changing the computed value.

- Update test assertion to include timeout=10 (test_fritzbox.py:307)
  assert_called_once_with checks the exact call signature; the test
  expectation must match the updated production code.

The plugin now fails fast on unreachable Fritz!Box (within 10s per
request) and works correctly on FIPS-enabled hosts. Default behavior
for standard deployments is unchanged.
2026-04-06 07:48:59 +00:00
sebingel
0648e8217c Add full i18n for Fritz!Box plugin description and settings
The Fritz!Box plugin config.json only contained English (en_us) strings
for all translatable fields. NetAlertX supports 21 languages and shows
the plugin description and all setting labels in the user's chosen
language. Without translations, every non-English user sees raw English
text for the plugin card description, setting names, and setting
explanations regardless of their language preference.

Changes:
- front/plugins/fritzbox/config.json: added 20 translations for the
  top-level plugin `description` field (all 21 supported languages)

- front/plugins/fritzbox/config.json: added translations for `name` and
  `description` fields in all 14 settings (RUN, RUN_SCHD, HOST, PORT,
  USER, PASS, USE_TLS, REPORT_GUEST, GUEST_SERVICE, ACTIVE_ONLY, CMD,
  RUN_TIMEOUT, SET_ALWAYS, SET_EMPTY)

  Selectively translated by field type:
  - 12 settings: 21 languages for both name and description
  - HOST (name "Fritz!Box Host") and PORT (name "TR-064 Port"): name
    kept as en_us only — these are language-neutral proper names and
    standard identifiers; description translated in all 21 languages

  Technical terms left untranslated in all languages: Fritz!Box, TR-064,
  HTTPS, HTTP, WLANConfiguration, and all code identifiers referenced
  in descriptions (schedule, NEWDEV, Source = USER, Source = LOCKED)

Total: 544 localized strings added across 21 languages (ar_ar, ca_ca,
cs_cz, de_de, es_es, fa_fa, fr_fr, id_id, it_it, ja_jp, nb_no, pl_pl,
pt_br, pt_pt, ru_ru, sv_sv, tr_tr, uk_ua, vi_vn, zh_cn).

Users in all supported languages now see the plugin description card and
every setting label in their own language. The existing en_us fallback
mechanism ensures forward compatibility with any future languages added
to the project.
2026-04-06 07:34:16 +00:00
sebingel
5839853f69 Add Fritz!Box device scanner plugin via TR-064 protocol
NetAlertX had no native support for discovering devices connected to
Fritz!Box routers. Users relying on Fritz!Box as their primary home
router had to use generic network scanning (ARP/ICMP), missing
Fritz!Box-specific details like interface type (WiFi/LAN) and
connection status per device.

Changes:
- Add plugin implementation (front/plugins/fritzbox/fritzbox.py)
  Queries all hosts via FritzHosts TR-064 service, normalizes MACs,
  maps interface types (802.11→WiFi, Ethernet→LAN), and writes results
  to CurrentScan via Plugin_Objects. Supports filtering to active-only
  devices and optional guest WiFi monitoring via a synthetic AP device
  with a deterministic locally-administered MAC (02:xx derived from
  Fritz!Box MAC via MD5).

- Add plugin configuration (front/plugins/fritzbox/config.json)
  Defines plugin_type "device_scanner" with settings for host, port,
  credentials, guest WiFi reporting, and active-only filtering.
  Maps scan columns to CurrentScan fields (scanMac, scanLastIP, scanName,
  scanType). Default schedule: every 5 minutes.

- Add plugin documentation (front/plugins/fritzbox/README.md)
  Covers TR-064 protocol basics, quick setup guide, all settings with
  defaults, troubleshooting for common issues (connection refused, auth
  failures, no devices found), and technical details.

- Add fritzconnection>=1.15.1 dependency (requirements.txt)
  Required Python library for TR-064 communication with Fritz!Box.

- Add test suite (test/plugins/test_fritzbox.py:1-298)
  298 lines covering get_connected_devices (active filtering, MAC
  normalization, interface mapping, error resilience), check_guest_wifi_status
  (service detection, SSID-based guest detection, fallback behavior), and
  create_guest_wifi_device (deterministic MAC generation, locally-administered
  bit, fallback MAC, regression anchor with precomputed hash).

Users can now scan Fritz!Box-connected devices natively, seeing per-device
connection status and interface type directly in NetAlertX. Guest WiFi
monitoring provides visibility into guest network state. The plugin
defaults to HTTPS on port 49443 with active-only filtering enabled.
2026-04-06 07:34:14 +00:00
jokob-sk
83de79bf94 Merge branch 'main' of github.com:netalertx/NetAlertX
Some checks failed
✅ Code checks / check-url-paths (push) Has been cancelled
✅ Code checks / lint (push) Has been cancelled
✅ Code checks / docker-tests (push) Has been cancelled
🐳 👩‍💻 docker dev / docker_dev (push) Has been cancelled
📘 Deploy MkDocs / deploy (push) Has been cancelled
2026-04-06 12:27:59 +10:00
jokob-sk
92f1508d33 DOCS: growtd
Signed-off-by: jokob-sk <jokob.sk@gmail.com>
2026-04-06 12:27:46 +10:00
31 changed files with 4941 additions and 118 deletions

View File

@@ -15,6 +15,22 @@ Before opening a new issue:
- [Check Common Issues & Debug Tips](https://docs.netalertx.com/DEBUG_TIPS#common-issues)
- [Search Closed Issues](https://github.com/netalertx/NetAlertX/issues?q=is%3Aissue+is%3Aclosed)
---
## Contributing without coding knowledge
Writing code is not the only way how you can contribute to the project. Here are some ideas how you can help out otherwise:
- Help improving the documentation (text, typos, use cases, screenshots - all this makes things better)
- Share your favorite project on socials (see [Growth ideas](./GROWTH.md))
- Write a blog post, or a how-to set-up guide
- Write how it helped you to achieve something fun or interesting to simplify your personal or work life
- Help with the translation effort
- Running a nightly or dev build to test for bugs before they make it to the production release
- Report bugs and features with sufficient detail and care (super important to ease everyone's maintenance burden and minimize back-and-forth)
- running a projects test suite
---
## Use of AI

142
GROWTH.md Normal file
View File

@@ -0,0 +1,142 @@
# 📈 NetAlertX Growth Playbook
If you like NetAlertX and want to help it grow, this is how.
This isnt about spam or "growth hacks." Its just getting the tool in front of people who already have the problem (manual network docs, blind spots, messy networks).
---
## 🎯 Who this is for
* **Homelabbers** → Proxmox, Unraid, Raspberry Pi setups
* **Self-hosters** → moving away from cloud / want local-first
* **Sysadmins / MSPs** → need visibility + fast onboarding
---
## 💬 Where to post (and how)
### Reddit (main channel)
Reddit works *if* you dont sound like marketing.
**Subreddits**
* r/homelab
* r/selfhosted
* r/sysadmin
* r/networking
**What works**
* "Show r/homelab" / "I built this"
* Problem → solution
* Real screenshots or short GIFs
**Example**
> Tired of keeping network diagrams up to date?
> I deployed something that does it automatically.
**Avoid**
* Over-explaining
* Buzzwords
* "Check out my project!!!" energy
**Timing (rough guide)**
* TueThu mornings (US time)
---
### Hacker News (Show HN)
Good fit because NetAlertX is:
* local-first
* privacy-focused
* actually useful
**Title**
> Show HN: NetAlertX network documentation that updates itself
Be ready to answer:
* stack (Python / PHP / JS)
* how discovery works
* privacy / local scanning
---
### Awesome Lists
Low effort, long-term visibility.
Target:
* awesome-selfhosted
* awesome-homelab
Keep it simple:
> NetAlertX automated network discovery and documentation with alerting
---
## 🎥 Content / creators
If you or someone else makes videos, these angles work:
* **First 5 minutes** → "it already found everything"
* **Before vs after** → manual vs NetAlertX
* **Docker setup** → show how easy it is
No need for overproduction—real usage > polished demos.
---
## ⚖️ Positioning (keep it simple)
When explaining NetAlertX, this is the mental model:
| | Manual Docs | Basic Scanners | NetAlertX |
| ------- | ----------------- | -------------- | ------------------------- |
| Updates | Manual / outdated | On demand | **Automatic** |
| Alerts | None | Limited | **New / unknown devices** |
| History | None | None | **Device timeline** |
| Effort | High | Medium | **Set & forget** |
---
## 🧑‍💻 MSP / professional angle
If youre using it in real environments:
* Faster client onboarding
* Better visibility
* Less "what changed?" guessing
Thats the value—keep it grounded. And share your experience 🙏
---
## 🛠 Easy ways to help
* Star the repo
* Post a screenshot of your network
* Mention it when someone asks "how do you track devices?" or "how do you detect presence?"
* Write a quick "how I use it" post
---
## Final note
Authenticity matters more than reach.
If it feels like marketing, people ignore it.
If it feels like "this solved my problem," people pay attention.
Thanks a lot in advance!
- jokob

View File

@@ -50,6 +50,10 @@ Manage a **single device** by its MAC address. Operations include retrieval, upd
* **POST** `/device/<mac>`
Create or update a device record.
> ⚠️ **Full-replace (PUT) semantics.** Every editable field is written on each call. Any field omitted from the payload is reset to its default (empty string or `0`). This matches how the frontend edit form works — it always sends the complete device state.
>
> To update a **single field** without affecting others, use [`POST /device/<mac>/update-column`](#7-update-a-single-column) instead.
**Request Body**:
```json
@@ -62,8 +66,8 @@ Manage a **single device** by its MAC address. Operations include retrieval, upd
**Behavior**:
* If `createNew=true`creates a new device
* Otherwise → updates existing device fields
* If `createNew=true`inserts a new device row
* Otherwise → **replaces all editable fields** on the existing device
**Response**:
@@ -163,7 +167,13 @@ Manage a **single device** by its MAC address. Operations include retrieval, upd
## 7. Update a Single Column
* **POST** `/device/<mac>/update-column`
Update one specific column for a device.
Update exactly one field for a device without touching any other fields.
> ✅ **Partial-update (PATCH) semantics.** Only the specified column is written. All other fields are left unchanged. Use this for automation, integrations, and any workflow that needs to update a single attribute.
>
> To replace all fields at once (e.g. saving from the edit form), use [`POST /device/<mac>`](#2-update-device-fields).
Allowed `columnName` values: `devName`, `devOwner`, `devType`, `devVendor`, `devGroup`, `devLocation`, `devComments`, `devIcon`, `devFavorite`, `devAlertEvents`, `devAlertDown`, `devCanSleep`, `devSkipRepeated`, `devReqNicsOnline`, `devForceStatus`, `devParentMAC`, `devParentPort`, `devParentRelType`, `devSSID`, `devSite`, `devVlan`, `devStaticIP`, `devIsNew`, `devIsArchived`, `devCustomProps`.
**Request Body**:
@@ -190,6 +200,108 @@ Manage a **single device** by its MAC address. Operations include retrieval, upd
---
## 8. Lock / Unlock a Device Field
* **POST** `/device/<mac>/field/lock`
Lock a field to prevent plugin overwrites, or unlock it to allow overwrites again.
**Request Body**:
```json
{
"fieldName": "devName",
"lock": true
}
```
| Field | Type | Required | Description |
|---|---|---|---|
| `fieldName` | string | ✅ | Field to lock/unlock (e.g. `devName`, `devVendor`) |
| `lock` | boolean | ❌ | `true` to lock, `false` to unlock (default when omitted) |
**Response** (success):
```json
{
"success": true,
"fieldName": "devName",
"locked": true,
"message": "Field devName locked"
}
```
**Error Responses**:
* Field does not support locking → HTTP 400
* Unauthorized → HTTP 403
---
## 9. Unlock / Clear Device Fields (Bulk)
* **POST** `/devices/fields/unlock`
Unlock fields (clear `LOCKED`/`USER` sources) for one device, a list of devices, or all devices.
**Request Body**:
```json
{
"mac": "AA:BB:CC:DD:EE:FF",
"fields": ["devName", "devVendor"],
"clearAll": false
}
```
| Field | Type | Required | Description |
|---|---|---|---|
| `mac` | string or array | ❌ | Single MAC, list of MACs, or omit for all devices |
| `fields` | array of strings | ❌ | Fields to unlock. Omit to unlock all tracked fields |
| `clearAll` | boolean | ❌ | `true` clears all sources; `false` (default) clears only `LOCKED`/`USER` |
**Response** (success):
```json
{
"success": true
}
```
**Error Responses**:
* `fields` is not a list → HTTP 400
* Unauthorized → HTTP 403
---
## 10. Set Device Alias
* **POST** `/device/<mac>/set-alias`
Convenience wrapper to update the device display name (`devName`).
**Request Body**:
```json
{
"alias": "My Router"
}
```
**Response** (success):
```json
{
"success": true
}
```
**Error Responses**:
* Missing `alias` → HTTP 400
* Device not found → HTTP 404
* Unauthorized → HTTP 403
---
## Example `curl` Requests
**Get Device Details**:
@@ -233,3 +345,30 @@ curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/device/AA:BB:CC:DD:EE:FF/update-
--data '{"columnName":"devName","columnValue":"Updated Device"}'
```
**Lock a Field**:
```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/device/AA:BB:CC:DD:EE:FF/field/lock" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"fieldName":"devName","lock":true}'
```
**Unlock Fields (all devices)**:
```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/devices/fields/unlock" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"fields":["devName","devVendor"]}'
```
**Set Device Alias**:
```bash
curl -X POST "http://<server_ip>:<GRAPHQL_PORT>/device/AA:BB:CC:DD:EE:FF/set-alias" \
-H "Authorization: Bearer <API_TOKEN>" \
-H "Content-Type: application/json" \
--data '{"alias":"My Router"}'
```

View File

@@ -57,6 +57,7 @@ Device-detecting plugins insert values into the `CurrentScan` database table. T
| `DHCPSRVS` | [dhcp_servers](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/dhcp_servers/) | ♻ | DHCP servers | | |
| `DIGSCAN` | [dig_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/dig_scan/) | 🆎 | Dig (DNS) Name resolution | | |
| `FREEBOX` | [freebox](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/freebox/) | 🔍/♻/🆎 | Pull data and names from Freebox/Iliadbox | | |
| `FRITZBOX` | [fritzbox](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/fritzbox/) | 🔍 | Fritz!Box device scanner via TR-064 | | |
| `ICMP` | [icmp_scan](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/icmp_scan/) | ♻ | ICMP (ping) status checker | | |
| `INTRNT` | [internet_ip](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/internet_ip/) | 🔍 | Internet IP scanner | | |
| `INTRSPD` | [internet_speedtest](https://github.com/netalertx/NetAlertX/tree/main/front/plugins/internet_speedtest/) | ♻ | Internet speed test | | |

View File

@@ -126,7 +126,7 @@ function isAppInitialized() {
lang_shouldBeCompletedCalls = getLangCode() == 'en_us' ? 1 : 2;
// check if each ajax call completed succesfully
// check if each ajax call completed successfully
for (const call_name of completedCalls_final) {
if (getCache(CACHE_KEYS.initFlag(call_name)) != "true") {
_isAppInitLog(`[isAppInitialized] waiting on ${call_name} (value: ${getCache(CACHE_KEYS.initFlag(call_name))})`);
@@ -268,7 +268,7 @@ setTimeout(() => {
// page refresh if configured
const refreshTime = getSetting("UI_REFRESH");
if (refreshTime && refreshTime !== "0" && refreshTime !== "") {
console.log("Refreshing page becasue UI_REFRESH setting enabled.");
console.log("Refreshing page because UI_REFRESH setting enabled.");
newTimerRefreshData(clearCache, parseInt(refreshTime)*1000);
}

View File

@@ -590,6 +590,7 @@ function addOptionFromModalInput() {
* A MAC is considered fake if it starts with:
* - "FA:CE" (new synthetic devices)
* - "00:1A" (legacy placeholder devices)
* - "02:" (legacy placeholder devices)
*
* The check is case-insensitive.
*
@@ -600,8 +601,8 @@ function isFakeMac(macAddress) {
// Normalize to lowercase for consistent comparison
macAddress = macAddress.toLowerCase();
// Check if MAC starts with FA:CE or 00:1a
return macAddress.startsWith("fa:ce") || macAddress.startsWith("00:1a");
// Check if MAC starts with FA:CE or 00:1a or 02:
return macAddress.startsWith("fa:ce") || macAddress.startsWith("00:1a") || macAddress.startsWith("02:");
}

View File

@@ -203,17 +203,17 @@
"Device_MultiEdit_MassActions": "Accions massives:",
"Device_MultiEdit_No_Devices": "Cap dispositiu seleccionat.",
"Device_MultiEdit_Tooltip": "Atenció. Si feu clic a això s'aplicarà el valor de l'esquerra a tots els dispositius seleccionats a dalt.",
"Device_NextScan_Imminent": "",
"Device_NextScan_In": "",
"Device_NoData_Help": "",
"Device_NoData_Scanning": "",
"Device_NoData_Title": "",
"Device_NoMatch_Title": "",
"Device_NextScan_Imminent": "Imminent...",
"Device_NextScan_In": "Proper scan en prop de ",
"Device_NoData_Help": "Si no surten dispositius desprès del scan, comproveu la configuracio de SCAN_SUBNETS i <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">documentacio</a>.",
"Device_NoData_Scanning": "Esperant el primer scan - això pot trigar alguns minuts desprès de la configuració inicial.",
"Device_NoData_Title": "Encara no s'han trobat dispositius",
"Device_NoMatch_Title": "No hi ha cap dispositiu que compleixi el filtre",
"Device_Save_Failed": "Problemes guardant el dispositiu",
"Device_Save_Unauthorized": "Token invàlid - No autoritzat",
"Device_Saved_Success": "S'ha guardat el dispositiu",
"Device_Saved_Unexpected": "Actualització de dispositiu ha retornat una resposta no esperada",
"Device_Scanning": "",
"Device_Scanning": "Escanejant...",
"Device_Searchbox": "Cerca",
"Device_Shortcut_AllDevices": "Els meus dispositius",
"Device_Shortcut_AllNodes": "Tots els nodes",
@@ -232,7 +232,7 @@
"Device_TableHead_FQDN": "FQDN",
"Device_TableHead_Favorite": "Favorit",
"Device_TableHead_FirstSession": "Primera Sessió",
"Device_TableHead_Flapping": "",
"Device_TableHead_Flapping": "Flapping",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Grup",
"Device_TableHead_IPv4": "IPv4",
@@ -323,7 +323,7 @@
"Gen_AddDevice": "Afegir dispositiu",
"Gen_Add_All": "Afegeix tot",
"Gen_All_Devices": "Tots els dispositius",
"Gen_Archived": "",
"Gen_Archived": "Arxivat",
"Gen_AreYouSure": "Estàs segur?",
"Gen_Backup": "Executar Backup",
"Gen_Cancel": "Cancel·lar",
@@ -334,16 +334,16 @@
"Gen_Delete": "Esborrar",
"Gen_DeleteAll": "Esborrar tot",
"Gen_Description": "Descripció",
"Gen_Down": "",
"Gen_Down": "Baix",
"Gen_Error": "Error",
"Gen_Filter": "Filtrar",
"Gen_Flapping": "",
"Gen_Flapping": "Flapping",
"Gen_Generate": "Generar",
"Gen_InvalidMac": "Mac address invàlida.",
"Gen_Invalid_Value": "S'ha introduït un valor incorrecte",
"Gen_LockedDB": "ERROR - DB podria estar bloquejada - Fes servir F12 Eines desenvolupament -> Consola o provar-ho més tard.",
"Gen_NetworkMask": "Màscara de xarxa",
"Gen_New": "",
"Gen_New": "Nou",
"Gen_Offline": "Fora de línia",
"Gen_Okay": "Ok",
"Gen_Online": "En línia",
@@ -361,7 +361,7 @@
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "Seleccioneu la vista prèvia",
"Gen_Selected_Devices": "Dispositius seleccionats:",
"Gen_Sleeping": "",
"Gen_Sleeping": "Dormint",
"Gen_Subnet": "Subxarxa",
"Gen_Switch": "Switch",
"Gen_Upd": "Actualitzat correctament",
@@ -591,8 +591,8 @@
"PIALERT_WEB_PROTECTION_name": "Activa l'accés",
"PLUGINS_KEEP_HIST_description": "Quantes entrades de Plugins s'han de mantenir a la història (per Plugin, no per dispositiu).",
"PLUGINS_KEEP_HIST_name": "Història dels Plugins",
"PRAGMA_JOURNAL_SIZE_LIMIT_description": "",
"PRAGMA_JOURNAL_SIZE_LIMIT_name": "",
"PRAGMA_JOURNAL_SIZE_LIMIT_description": "SQLite WAL (Write-Ahead Log) mida màxima en MB abans de desencadenar punts de verificació automàtics. Els valors més baixos (10-20 MB) redueixen l'ús del disc / emmagatzematge, però augmenten l'ús de la CPU durant les exploracions. Els valors més alts (50-100 MB) redueixen els pics de CPU durant les operacions, però poden utilitzar més memòria RAM i espai de disc. Default <code>50 MB</code> saldos ambdós. Útil per a sistemes formats per recursos com dispositius NAS amb targetes SD. Preguntes Freqüents - FAQ.",
"PRAGMA_JOURNAL_SIZE_LIMIT_name": "Límit de mida WAL (MB)",
"Plugins_DeleteAll": "Elimina tot (s'ignoraran els filtres)",
"Plugins_Filters_Mac": "Filtre de MAC",
"Plugins_History": "Historial d'Esdeveniments",

View File

@@ -400,11 +400,11 @@
"Login_Toggle_Info_headline": "Password Information",
"Maint_PurgeLog": "Purge log",
"Maint_RestartServer": "Restart server",
"Maint_Restart_Server_noti_text": "Are you sure you want to restart the backend server? This may casue app inconsistency. Backup your setup first. <br/> <br/> Note: This may take a few minutes.",
"Maint_Restart_Server_noti_text": "Are you sure you want to restart the backend server? This may cause app inconsistency. Backup your setup first. <br/> <br/> Note: This may take a few minutes.",
"Maintenance_InitCheck": "Init check",
"Maintenance_InitCheck_Checking": "Checking…",
"Maintenance_InitCheck_QuickSetupGuide": "Make sure you followed the <a href=\"https://docs.netalertx.com/INITIAL_SETUP/\" target=\"_blank\">quick setup guide</a>.",
"Maintenance_InitCheck_Success": "Application initialized succesfully!",
"Maintenance_InitCheck_Success": "Application initialized successfully!",
"Maintenance_ReCheck": "Retry check",
"Maintenance_Running_Version": "Installed version",
"Maintenance_Status": "Status",

View File

@@ -0,0 +1,808 @@
{
"API_CUSTOM_SQL_description": "",
"API_CUSTOM_SQL_name": "",
"API_TOKEN_description": "",
"API_TOKEN_name": "",
"API_display_name": "",
"API_icon": "",
"About_Design": "",
"About_Exit": "",
"About_Title": "",
"AppEvents_AppEventProcessed": "",
"AppEvents_DateTimeCreated": "",
"AppEvents_Extra": "",
"AppEvents_GUID": "",
"AppEvents_Helper1": "",
"AppEvents_Helper2": "",
"AppEvents_Helper3": "",
"AppEvents_ObjectForeignKey": "",
"AppEvents_ObjectIndex": "",
"AppEvents_ObjectIsArchived": "",
"AppEvents_ObjectIsNew": "",
"AppEvents_ObjectPlugin": "",
"AppEvents_ObjectPrimaryID": "",
"AppEvents_ObjectSecondaryID": "",
"AppEvents_ObjectStatus": "",
"AppEvents_ObjectStatusColumn": "",
"AppEvents_ObjectType": "",
"AppEvents_Plugin": "",
"AppEvents_Type": "",
"BACKEND_API_URL_description": "",
"BACKEND_API_URL_name": "",
"BackDevDetail_Actions_Ask_Run": "",
"BackDevDetail_Actions_Not_Registered": "",
"BackDevDetail_Actions_Title_Run": "",
"BackDevDetail_Copy_Ask": "",
"BackDevDetail_Copy_Title": "",
"BackDevDetail_Tools_WOL_error": "",
"BackDevDetail_Tools_WOL_okay": "",
"BackDevices_Arpscan_disabled": "",
"BackDevices_Arpscan_enabled": "",
"BackDevices_Backup_CopError": "",
"BackDevices_Backup_Failed": "",
"BackDevices_Backup_okay": "",
"BackDevices_DBTools_DelDevError_a": "",
"BackDevices_DBTools_DelDevError_b": "",
"BackDevices_DBTools_DelDev_a": "",
"BackDevices_DBTools_DelDev_b": "",
"BackDevices_DBTools_DelEvents": "",
"BackDevices_DBTools_DelEventsError": "",
"BackDevices_DBTools_ImportCSV": "",
"BackDevices_DBTools_ImportCSVError": "",
"BackDevices_DBTools_ImportCSVMissing": "",
"BackDevices_DBTools_Purge": "",
"BackDevices_DBTools_UpdDev": "",
"BackDevices_DBTools_UpdDevError": "",
"BackDevices_DBTools_Upgrade": "",
"BackDevices_DBTools_UpgradeError": "",
"BackDevices_Device_UpdDevError": "",
"BackDevices_Restore_CopError": "",
"BackDevices_Restore_Failed": "",
"BackDevices_Restore_okay": "",
"BackDevices_darkmode_disabled": "",
"BackDevices_darkmode_enabled": "",
"CLEAR_NEW_FLAG_description": "",
"CLEAR_NEW_FLAG_name": "",
"CustProps_cant_remove": "",
"DAYS_TO_KEEP_EVENTS_description": "",
"DAYS_TO_KEEP_EVENTS_name": "",
"DISCOVER_PLUGINS_description": "",
"DISCOVER_PLUGINS_name": "",
"DevDetail_Children_Title": "",
"DevDetail_Copy_Device_Title": "",
"DevDetail_Copy_Device_Tooltip": "",
"DevDetail_CustomProperties_Title": "",
"DevDetail_CustomProps_reset_info": "",
"DevDetail_DisplayFields_Title": "",
"DevDetail_EveandAl_AlertAllEvents": "",
"DevDetail_EveandAl_AlertDown": "",
"DevDetail_EveandAl_Archived": "",
"DevDetail_EveandAl_NewDevice": "",
"DevDetail_EveandAl_NewDevice_Tooltip": "",
"DevDetail_EveandAl_RandomMAC": "",
"DevDetail_EveandAl_ScanCycle": "",
"DevDetail_EveandAl_ScanCycle_a": "",
"DevDetail_EveandAl_ScanCycle_z": "",
"DevDetail_EveandAl_Skip": "",
"DevDetail_EveandAl_Title": "",
"DevDetail_Events_CheckBox": "",
"DevDetail_GoToNetworkNode": "",
"DevDetail_Icon": "",
"DevDetail_Icon_Descr": "",
"DevDetail_Loading": "",
"DevDetail_MainInfo_Comments": "",
"DevDetail_MainInfo_Favorite": "",
"DevDetail_MainInfo_Group": "",
"DevDetail_MainInfo_Location": "",
"DevDetail_MainInfo_Name": "",
"DevDetail_MainInfo_Network": "",
"DevDetail_MainInfo_Network_Port": "",
"DevDetail_MainInfo_Network_Site": "",
"DevDetail_MainInfo_Network_Title": "",
"DevDetail_MainInfo_Owner": "",
"DevDetail_MainInfo_SSID": "",
"DevDetail_MainInfo_Title": "",
"DevDetail_MainInfo_Type": "",
"DevDetail_MainInfo_Vendor": "",
"DevDetail_MainInfo_mac": "",
"DevDetail_NavToChildNode": "",
"DevDetail_Network_Node_hover": "",
"DevDetail_Network_Port_hover": "",
"DevDetail_Nmap_Scans": "",
"DevDetail_Nmap_Scans_desc": "",
"DevDetail_Nmap_buttonDefault": "",
"DevDetail_Nmap_buttonDefault_text": "",
"DevDetail_Nmap_buttonDetail": "",
"DevDetail_Nmap_buttonDetail_text": "",
"DevDetail_Nmap_buttonFast": "",
"DevDetail_Nmap_buttonFast_text": "",
"DevDetail_Nmap_buttonSkipDiscovery": "",
"DevDetail_Nmap_buttonSkipDiscovery_text": "",
"DevDetail_Nmap_resultsLink": "",
"DevDetail_Owner_hover": "",
"DevDetail_Periodselect_All": "",
"DevDetail_Periodselect_LastMonth": "",
"DevDetail_Periodselect_LastWeek": "",
"DevDetail_Periodselect_LastYear": "",
"DevDetail_Periodselect_today": "",
"DevDetail_Run_Actions_Title": "",
"DevDetail_Run_Actions_Tooltip": "",
"DevDetail_SessionInfo_FirstSession": "",
"DevDetail_SessionInfo_LastIP": "",
"DevDetail_SessionInfo_LastSession": "",
"DevDetail_SessionInfo_StaticIP": "",
"DevDetail_SessionInfo_Status": "",
"DevDetail_SessionInfo_Title": "",
"DevDetail_SessionTable_Additionalinfo": "",
"DevDetail_SessionTable_Connection": "",
"DevDetail_SessionTable_Disconnection": "",
"DevDetail_SessionTable_Duration": "",
"DevDetail_SessionTable_IP": "",
"DevDetail_SessionTable_Order": "",
"DevDetail_Shortcut_CurrentStatus": "",
"DevDetail_Shortcut_DownAlerts": "",
"DevDetail_Shortcut_Presence": "",
"DevDetail_Shortcut_Sessions": "",
"DevDetail_Tab_Details": "",
"DevDetail_Tab_Events": "",
"DevDetail_Tab_EventsTableDate": "",
"DevDetail_Tab_EventsTableEvent": "",
"DevDetail_Tab_EventsTableIP": "",
"DevDetail_Tab_EventsTableInfo": "",
"DevDetail_Tab_Nmap": "",
"DevDetail_Tab_NmapEmpty": "",
"DevDetail_Tab_NmapTableExtra": "",
"DevDetail_Tab_NmapTableHeader": "",
"DevDetail_Tab_NmapTableIndex": "",
"DevDetail_Tab_NmapTablePort": "",
"DevDetail_Tab_NmapTableService": "",
"DevDetail_Tab_NmapTableState": "",
"DevDetail_Tab_NmapTableText": "",
"DevDetail_Tab_NmapTableTime": "",
"DevDetail_Tab_Plugins": "",
"DevDetail_Tab_Presence": "",
"DevDetail_Tab_Sessions": "",
"DevDetail_Tab_Tools": "",
"DevDetail_Tab_Tools_Internet_Info_Description": "",
"DevDetail_Tab_Tools_Internet_Info_Error": "",
"DevDetail_Tab_Tools_Internet_Info_Start": "",
"DevDetail_Tab_Tools_Internet_Info_Title": "",
"DevDetail_Tab_Tools_Nslookup_Description": "",
"DevDetail_Tab_Tools_Nslookup_Error": "",
"DevDetail_Tab_Tools_Nslookup_Start": "",
"DevDetail_Tab_Tools_Nslookup_Title": "",
"DevDetail_Tab_Tools_Speedtest_Description": "",
"DevDetail_Tab_Tools_Speedtest_Start": "",
"DevDetail_Tab_Tools_Speedtest_Title": "",
"DevDetail_Tab_Tools_Traceroute_Description": "",
"DevDetail_Tab_Tools_Traceroute_Error": "",
"DevDetail_Tab_Tools_Traceroute_Start": "",
"DevDetail_Tab_Tools_Traceroute_Title": "",
"DevDetail_Tools_WOL": "",
"DevDetail_Tools_WOL_noti": "",
"DevDetail_Tools_WOL_noti_text": "",
"DevDetail_Type_hover": "",
"DevDetail_Vendor_hover": "",
"DevDetail_WOL_Title": "",
"DevDetail_button_AddIcon": "",
"DevDetail_button_AddIcon_Help": "",
"DevDetail_button_AddIcon_Tooltip": "",
"DevDetail_button_Delete": "",
"DevDetail_button_DeleteEvents": "",
"DevDetail_button_DeleteEvents_Warning": "",
"DevDetail_button_Delete_ask": "",
"DevDetail_button_OverwriteIcons": "",
"DevDetail_button_OverwriteIcons_Tooltip": "",
"DevDetail_button_OverwriteIcons_Warning": "",
"DevDetail_button_Reset": "",
"DevDetail_button_Save": "",
"DeviceEdit_ValidMacIp": "",
"Device_MultiEdit": "",
"Device_MultiEdit_Backup": "",
"Device_MultiEdit_Fields": "",
"Device_MultiEdit_MassActions": "",
"Device_MultiEdit_No_Devices": "",
"Device_MultiEdit_Tooltip": "",
"Device_NextScan_Imminent": "",
"Device_NextScan_In": "",
"Device_NoData_Help": "",
"Device_NoData_Scanning": "",
"Device_NoData_Title": "",
"Device_NoMatch_Title": "",
"Device_Save_Failed": "",
"Device_Save_Unauthorized": "",
"Device_Saved_Success": "",
"Device_Saved_Unexpected": "",
"Device_Scanning": "",
"Device_Searchbox": "",
"Device_Shortcut_AllDevices": "",
"Device_Shortcut_AllNodes": "",
"Device_Shortcut_Archived": "",
"Device_Shortcut_Connected": "",
"Device_Shortcut_Devices": "",
"Device_Shortcut_DownAlerts": "",
"Device_Shortcut_DownOnly": "",
"Device_Shortcut_Favorites": "",
"Device_Shortcut_NewDevices": "",
"Device_Shortcut_OnlineChart": "",
"Device_Shortcut_Unstable": "",
"Device_TableHead_AlertDown": "",
"Device_TableHead_Connected_Devices": "",
"Device_TableHead_CustomProps": "",
"Device_TableHead_FQDN": "",
"Device_TableHead_Favorite": "",
"Device_TableHead_FirstSession": "",
"Device_TableHead_Flapping": "",
"Device_TableHead_GUID": "",
"Device_TableHead_Group": "",
"Device_TableHead_IPv4": "",
"Device_TableHead_IPv6": "",
"Device_TableHead_Icon": "",
"Device_TableHead_LastIP": "",
"Device_TableHead_LastIPOrder": "",
"Device_TableHead_LastSession": "",
"Device_TableHead_Location": "",
"Device_TableHead_MAC": "",
"Device_TableHead_MAC_full": "",
"Device_TableHead_Name": "",
"Device_TableHead_NetworkSite": "",
"Device_TableHead_Owner": "",
"Device_TableHead_ParentRelType": "",
"Device_TableHead_Parent_MAC": "",
"Device_TableHead_Port": "",
"Device_TableHead_PresentLastScan": "",
"Device_TableHead_ReqNicsOnline": "",
"Device_TableHead_RowID": "",
"Device_TableHead_Rowid": "",
"Device_TableHead_SSID": "",
"Device_TableHead_SourcePlugin": "",
"Device_TableHead_Status": "",
"Device_TableHead_SyncHubNodeName": "",
"Device_TableHead_Type": "",
"Device_TableHead_Vendor": "",
"Device_TableHead_Vlan": "",
"Device_Table_Not_Network_Device": "",
"Device_Table_info": "",
"Device_Table_nav_next": "",
"Device_Table_nav_prev": "",
"Device_Tablelenght": "",
"Device_Tablelenght_all": "",
"Device_Title": "",
"Devices_Filters": "",
"ENABLE_PLUGINS_description": "",
"ENABLE_PLUGINS_name": "",
"ENCRYPTION_KEY_description": "",
"ENCRYPTION_KEY_name": "",
"Email_display_name": "",
"Email_icon": "",
"Events_Loading": "",
"Events_Periodselect_All": "",
"Events_Periodselect_LastMonth": "",
"Events_Periodselect_LastWeek": "",
"Events_Periodselect_LastYear": "",
"Events_Periodselect_today": "",
"Events_Searchbox": "",
"Events_Shortcut_AllEvents": "",
"Events_Shortcut_DownAlerts": "",
"Events_Shortcut_Events": "",
"Events_Shortcut_MissSessions": "",
"Events_Shortcut_NewDevices": "",
"Events_Shortcut_Sessions": "",
"Events_Shortcut_VoidSessions": "",
"Events_TableHead_AdditionalInfo": "",
"Events_TableHead_Connection": "",
"Events_TableHead_Date": "",
"Events_TableHead_Device": "",
"Events_TableHead_Disconnection": "",
"Events_TableHead_Duration": "",
"Events_TableHead_DurationOrder": "",
"Events_TableHead_EventType": "",
"Events_TableHead_IP": "",
"Events_TableHead_IPOrder": "",
"Events_TableHead_Order": "",
"Events_TableHead_Owner": "",
"Events_TableHead_PendingAlert": "",
"Events_Table_info": "",
"Events_Table_nav_next": "",
"Events_Table_nav_prev": "",
"Events_Tablelenght": "",
"Events_Tablelenght_all": "",
"Events_Title": "",
"FakeMAC_hover": "",
"FieldLock_Error": "",
"FieldLock_Lock_Tooltip": "",
"FieldLock_Locked": "",
"FieldLock_SaveBeforeLocking": "",
"FieldLock_Source_Label": "",
"FieldLock_Unlock_Tooltip": "",
"FieldLock_Unlocked": "",
"GRAPHQL_PORT_description": "",
"GRAPHQL_PORT_name": "",
"Gen_Action": "",
"Gen_Add": "",
"Gen_AddDevice": "",
"Gen_Add_All": "",
"Gen_All_Devices": "",
"Gen_Archived": "",
"Gen_AreYouSure": "",
"Gen_Backup": "",
"Gen_Cancel": "",
"Gen_Change": "",
"Gen_Copy": "",
"Gen_CopyToClipboard": "",
"Gen_DataUpdatedUITakesTime": "",
"Gen_Delete": "",
"Gen_DeleteAll": "",
"Gen_Description": "",
"Gen_Down": "",
"Gen_Error": "",
"Gen_Filter": "",
"Gen_Flapping": "",
"Gen_Generate": "",
"Gen_InvalidMac": "",
"Gen_Invalid_Value": "",
"Gen_LockedDB": "",
"Gen_NetworkMask": "",
"Gen_New": "",
"Gen_Offline": "",
"Gen_Okay": "",
"Gen_Online": "",
"Gen_Purge": "",
"Gen_ReadDocs": "",
"Gen_Remove_All": "",
"Gen_Remove_Last": "",
"Gen_Reset": "",
"Gen_Restore": "",
"Gen_Run": "",
"Gen_Save": "",
"Gen_Saved": "",
"Gen_Search": "",
"Gen_Select": "",
"Gen_SelectIcon": "",
"Gen_SelectToPreview": "",
"Gen_Selected_Devices": "",
"Gen_Sleeping": "",
"Gen_Subnet": "",
"Gen_Switch": "",
"Gen_Upd": "",
"Gen_Upd_Fail": "",
"Gen_Update": "",
"Gen_Update_Value": "",
"Gen_ValidIcon": "",
"Gen_Warning": "",
"Gen_Work_In_Progress": "",
"Gen_create_new_device": "",
"Gen_create_new_device_info": "",
"General_display_name": "",
"General_icon": "",
"HRS_TO_KEEP_NEWDEV_description": "",
"HRS_TO_KEEP_NEWDEV_name": "",
"HRS_TO_KEEP_OFFDEV_description": "",
"HRS_TO_KEEP_OFFDEV_name": "",
"LOADED_PLUGINS_description": "",
"LOADED_PLUGINS_name": "",
"LOG_LEVEL_description": "",
"LOG_LEVEL_name": "",
"Loading": "",
"Login_Box": "",
"Login_Default_PWD": "",
"Login_Info": "",
"Login_Psw-box": "",
"Login_Psw_alert": "",
"Login_Psw_folder": "",
"Login_Psw_new": "",
"Login_Psw_run": "",
"Login_Remember": "",
"Login_Remember_small": "",
"Login_Submit": "",
"Login_Toggle_Alert_headline": "",
"Login_Toggle_Info": "",
"Login_Toggle_Info_headline": "",
"Maint_PurgeLog": "",
"Maint_RestartServer": "",
"Maint_Restart_Server_noti_text": "",
"Maintenance_InitCheck": "",
"Maintenance_InitCheck_Checking": "",
"Maintenance_InitCheck_QuickSetupGuide": "",
"Maintenance_InitCheck_Success": "",
"Maintenance_ReCheck": "",
"Maintenance_Running_Version": "",
"Maintenance_Status": "",
"Maintenance_Title": "",
"Maintenance_Tool_DownloadConfig": "",
"Maintenance_Tool_DownloadConfig_text": "",
"Maintenance_Tool_DownloadWorkflows": "",
"Maintenance_Tool_DownloadWorkflows_text": "",
"Maintenance_Tool_ExportCSV": "",
"Maintenance_Tool_ExportCSV_noti": "",
"Maintenance_Tool_ExportCSV_noti_text": "",
"Maintenance_Tool_ExportCSV_text": "",
"Maintenance_Tool_ImportCSV": "",
"Maintenance_Tool_ImportCSV_noti": "",
"Maintenance_Tool_ImportCSV_noti_text": "",
"Maintenance_Tool_ImportCSV_text": "",
"Maintenance_Tool_ImportConfig_noti": "",
"Maintenance_Tool_ImportPastedCSV": "",
"Maintenance_Tool_ImportPastedCSV_noti_text": "",
"Maintenance_Tool_ImportPastedCSV_text": "",
"Maintenance_Tool_ImportPastedConfig": "",
"Maintenance_Tool_ImportPastedConfig_noti_text": "",
"Maintenance_Tool_ImportPastedConfig_text": "",
"Maintenance_Tool_UnlockFields": "",
"Maintenance_Tool_UnlockFields_noti": "",
"Maintenance_Tool_UnlockFields_noti_text": "",
"Maintenance_Tool_UnlockFields_text": "",
"Maintenance_Tool_arpscansw": "",
"Maintenance_Tool_arpscansw_noti": "",
"Maintenance_Tool_arpscansw_noti_text": "",
"Maintenance_Tool_arpscansw_text": "",
"Maintenance_Tool_backup": "",
"Maintenance_Tool_backup_noti": "",
"Maintenance_Tool_backup_noti_text": "",
"Maintenance_Tool_backup_text": "",
"Maintenance_Tool_check_visible": "",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_darkmode": "",
"Maintenance_Tool_darkmode_noti": "",
"Maintenance_Tool_darkmode_noti_text": "",
"Maintenance_Tool_darkmode_text": "",
"Maintenance_Tool_del_ActHistory": "",
"Maintenance_Tool_del_ActHistory_noti": "",
"Maintenance_Tool_del_ActHistory_noti_text": "",
"Maintenance_Tool_del_ActHistory_text": "",
"Maintenance_Tool_del_alldev": "",
"Maintenance_Tool_del_alldev_noti": "",
"Maintenance_Tool_del_alldev_noti_text": "",
"Maintenance_Tool_del_alldev_text": "",
"Maintenance_Tool_del_allevents": "",
"Maintenance_Tool_del_allevents30": "",
"Maintenance_Tool_del_allevents30_noti": "",
"Maintenance_Tool_del_allevents30_noti_text": "",
"Maintenance_Tool_del_allevents30_text": "",
"Maintenance_Tool_del_allevents_noti": "",
"Maintenance_Tool_del_allevents_noti_text": "",
"Maintenance_Tool_del_allevents_text": "",
"Maintenance_Tool_del_empty_macs": "",
"Maintenance_Tool_del_empty_macs_noti": "",
"Maintenance_Tool_del_empty_macs_noti_text": "",
"Maintenance_Tool_del_empty_macs_text": "",
"Maintenance_Tool_del_selecteddev": "",
"Maintenance_Tool_del_selecteddev_text": "",
"Maintenance_Tool_del_unknowndev": "",
"Maintenance_Tool_del_unknowndev_noti": "",
"Maintenance_Tool_del_unknowndev_noti_text": "",
"Maintenance_Tool_del_unknowndev_text": "",
"Maintenance_Tool_del_unlockFields_selecteddev_text": "",
"Maintenance_Tool_displayed_columns_text": "",
"Maintenance_Tool_drag_me": "",
"Maintenance_Tool_order_columns_text": "",
"Maintenance_Tool_purgebackup": "",
"Maintenance_Tool_purgebackup_noti": "",
"Maintenance_Tool_purgebackup_noti_text": "",
"Maintenance_Tool_purgebackup_text": "",
"Maintenance_Tool_restore": "",
"Maintenance_Tool_restore_noti": "",
"Maintenance_Tool_restore_noti_text": "",
"Maintenance_Tool_restore_text": "",
"Maintenance_Tool_unlockFields_selecteddev": "",
"Maintenance_Tool_unlockFields_selecteddev_noti": "",
"Maintenance_Tool_upgrade_database_noti": "",
"Maintenance_Tool_upgrade_database_noti_text": "",
"Maintenance_Tool_upgrade_database_text": "",
"Maintenance_Tools_Tab_BackupRestore": "",
"Maintenance_Tools_Tab_Logging": "",
"Maintenance_Tools_Tab_Settings": "",
"Maintenance_Tools_Tab_Tools": "",
"Maintenance_Tools_Tab_UISettings": "",
"Maintenance_arp_status": "",
"Maintenance_arp_status_off": "",
"Maintenance_arp_status_on": "",
"Maintenance_built_on": "",
"Maintenance_current_version": "",
"Maintenance_database_backup": "",
"Maintenance_database_backup_found": "",
"Maintenance_database_backup_total": "",
"Maintenance_database_lastmod": "",
"Maintenance_database_path": "",
"Maintenance_database_rows": "",
"Maintenance_database_size": "",
"Maintenance_lang_selector_apply": "",
"Maintenance_lang_selector_empty": "",
"Maintenance_lang_selector_lable": "",
"Maintenance_lang_selector_text": "",
"Maintenance_new_version": "",
"Maintenance_themeselector_apply": "",
"Maintenance_themeselector_empty": "",
"Maintenance_themeselector_lable": "",
"Maintenance_themeselector_text": "",
"Maintenance_version": "",
"NETWORK_DEVICE_TYPES_description": "",
"NETWORK_DEVICE_TYPES_name": "",
"Navigation_About": "",
"Navigation_AppEvents": "",
"Navigation_Devices": "",
"Navigation_Donations": "",
"Navigation_Events": "",
"Navigation_Integrations": "",
"Navigation_Maintenance": "",
"Navigation_Monitoring": "",
"Navigation_Network": "",
"Navigation_Notifications": "",
"Navigation_Plugins": "",
"Navigation_Presence": "",
"Navigation_Report": "",
"Navigation_Settings": "",
"Navigation_SystemInfo": "",
"Navigation_Workflows": "",
"Network_Assign": "",
"Network_Cant_Assign": "",
"Network_Cant_Assign_No_Node_Selected": "",
"Network_Configuration_Error": "",
"Network_Connected": "",
"Network_Devices": "",
"Network_ManageAdd": "",
"Network_ManageAdd_Name": "",
"Network_ManageAdd_Name_text": "",
"Network_ManageAdd_Port": "",
"Network_ManageAdd_Port_text": "",
"Network_ManageAdd_Submit": "",
"Network_ManageAdd_Type": "",
"Network_ManageAdd_Type_text": "",
"Network_ManageAssign": "",
"Network_ManageDel": "",
"Network_ManageDel_Name": "",
"Network_ManageDel_Name_text": "",
"Network_ManageDel_Submit": "",
"Network_ManageDevices": "",
"Network_ManageEdit": "",
"Network_ManageEdit_ID": "",
"Network_ManageEdit_ID_text": "",
"Network_ManageEdit_Name": "",
"Network_ManageEdit_Name_text": "",
"Network_ManageEdit_Port": "",
"Network_ManageEdit_Port_text": "",
"Network_ManageEdit_Submit": "",
"Network_ManageEdit_Type": "",
"Network_ManageEdit_Type_text": "",
"Network_ManageLeaf": "",
"Network_ManageUnassign": "",
"Network_NoAssignedDevices": "",
"Network_NoDevices": "",
"Network_Node": "",
"Network_Node_Name": "",
"Network_Parent": "",
"Network_Root": "",
"Network_Root_Not_Configured": "",
"Network_Root_Unconfigurable": "",
"Network_ShowArchived": "",
"Network_ShowOffline": "",
"Network_Table_Hostname": "",
"Network_Table_IP": "",
"Network_Table_State": "",
"Network_Title": "",
"Network_UnassignedDevices": "",
"Notifications_All": "",
"Notifications_Mark_All_Read": "",
"PIALERT_WEB_PASSWORD_description": "",
"PIALERT_WEB_PASSWORD_name": "",
"PIALERT_WEB_PROTECTION_description": "",
"PIALERT_WEB_PROTECTION_name": "",
"PLUGINS_KEEP_HIST_description": "",
"PLUGINS_KEEP_HIST_name": "",
"PRAGMA_JOURNAL_SIZE_LIMIT_description": "",
"PRAGMA_JOURNAL_SIZE_LIMIT_name": "",
"Plugins_DeleteAll": "",
"Plugins_Filters_Mac": "",
"Plugins_History": "",
"Plugins_Obj_DeleteListed": "",
"Plugins_Objects": "",
"Plugins_Out_of": "",
"Plugins_Unprocessed_Events": "",
"Plugins_no_control": "",
"Presence_CalHead_day": "",
"Presence_CalHead_lang": "",
"Presence_CalHead_month": "",
"Presence_CalHead_quarter": "",
"Presence_CalHead_week": "",
"Presence_CalHead_year": "",
"Presence_CallHead_Devices": "",
"Presence_Key_OnlineNow": "",
"Presence_Key_OnlineNow_desc": "",
"Presence_Key_OnlinePast": "",
"Presence_Key_OnlinePastMiss": "",
"Presence_Key_OnlinePastMiss_desc": "",
"Presence_Key_OnlinePast_desc": "",
"Presence_Loading": "",
"Presence_Shortcut_AllDevices": "",
"Presence_Shortcut_Archived": "",
"Presence_Shortcut_Connected": "",
"Presence_Shortcut_Devices": "",
"Presence_Shortcut_DownAlerts": "",
"Presence_Shortcut_Favorites": "",
"Presence_Shortcut_NewDevices": "",
"Presence_Title": "",
"REFRESH_FQDN_description": "",
"REFRESH_FQDN_name": "",
"REPORT_DASHBOARD_URL_description": "",
"REPORT_DASHBOARD_URL_name": "",
"REPORT_ERROR": "",
"REPORT_MAIL_description": "",
"REPORT_MAIL_name": "",
"REPORT_TITLE": "",
"RandomMAC_hover": "",
"Reports_Sent_Log": "",
"SCAN_SUBNETS_description": "",
"SCAN_SUBNETS_name": "",
"SYSTEM_TITLE": "",
"Setting_Override": "",
"Setting_Override_Description": "",
"Settings_Metadata_Toggle": "",
"Settings_Show_Description": "",
"Settings_device_Scanners_desync": "",
"Settings_device_Scanners_desync_popup": "",
"Speedtest_Results": "",
"Systeminfo_AvailableIps": "",
"Systeminfo_CPU": "",
"Systeminfo_CPU_Cores": "",
"Systeminfo_CPU_Name": "",
"Systeminfo_CPU_Speed": "",
"Systeminfo_CPU_Temp": "",
"Systeminfo_CPU_Vendor": "",
"Systeminfo_Client_Resolution": "",
"Systeminfo_Client_User_Agent": "",
"Systeminfo_General": "",
"Systeminfo_General_Date": "",
"Systeminfo_General_Date2": "",
"Systeminfo_General_Full_Date": "",
"Systeminfo_General_TimeZone": "",
"Systeminfo_Memory": "",
"Systeminfo_Memory_Total_Memory": "",
"Systeminfo_Memory_Usage": "",
"Systeminfo_Memory_Usage_Percent": "",
"Systeminfo_Motherboard": "",
"Systeminfo_Motherboard_BIOS": "",
"Systeminfo_Motherboard_BIOS_Date": "",
"Systeminfo_Motherboard_BIOS_Vendor": "",
"Systeminfo_Motherboard_Manufactured": "",
"Systeminfo_Motherboard_Name": "",
"Systeminfo_Motherboard_Revision": "",
"Systeminfo_Network": "",
"Systeminfo_Network_Accept_Encoding": "",
"Systeminfo_Network_Accept_Language": "",
"Systeminfo_Network_Connection_Port": "",
"Systeminfo_Network_HTTP_Host": "",
"Systeminfo_Network_HTTP_Referer": "",
"Systeminfo_Network_HTTP_Referer_String": "",
"Systeminfo_Network_Hardware": "",
"Systeminfo_Network_Hardware_Interface_Mask": "",
"Systeminfo_Network_Hardware_Interface_Name": "",
"Systeminfo_Network_Hardware_Interface_RX": "",
"Systeminfo_Network_Hardware_Interface_TX": "",
"Systeminfo_Network_IP": "",
"Systeminfo_Network_IP_Connection": "",
"Systeminfo_Network_IP_Server": "",
"Systeminfo_Network_MIME": "",
"Systeminfo_Network_Request_Method": "",
"Systeminfo_Network_Request_Time": "",
"Systeminfo_Network_Request_URI": "",
"Systeminfo_Network_Secure_Connection": "",
"Systeminfo_Network_Secure_Connection_String": "",
"Systeminfo_Network_Server_Name": "",
"Systeminfo_Network_Server_Name_String": "",
"Systeminfo_Network_Server_Query": "",
"Systeminfo_Network_Server_Query_String": "",
"Systeminfo_Network_Server_Version": "",
"Systeminfo_Services": "",
"Systeminfo_Services_Description": "",
"Systeminfo_Services_Name": "",
"Systeminfo_Storage": "",
"Systeminfo_Storage_Device": "",
"Systeminfo_Storage_Mount": "",
"Systeminfo_Storage_Size": "",
"Systeminfo_Storage_Type": "",
"Systeminfo_Storage_Usage": "",
"Systeminfo_Storage_Usage_Free": "",
"Systeminfo_Storage_Usage_Mount": "",
"Systeminfo_Storage_Usage_Total": "",
"Systeminfo_Storage_Usage_Used": "",
"Systeminfo_System": "",
"Systeminfo_System_AVG": "",
"Systeminfo_System_Architecture": "",
"Systeminfo_System_Kernel": "",
"Systeminfo_System_OSVersion": "",
"Systeminfo_System_Running_Processes": "",
"Systeminfo_System_System": "",
"Systeminfo_System_Uname": "",
"Systeminfo_System_Uptime": "",
"Systeminfo_This_Client": "",
"Systeminfo_USB_Devices": "",
"TICKER_MIGRATE_TO_NETALERTX": "",
"TIMEZONE_description": "",
"TIMEZONE_name": "",
"UI_DEV_SECTIONS_description": "",
"UI_DEV_SECTIONS_name": "",
"UI_ICONS_description": "",
"UI_ICONS_name": "",
"UI_LANG_description": "",
"UI_LANG_name": "",
"UI_MY_DEVICES_description": "",
"UI_MY_DEVICES_name": "",
"UI_NOT_RANDOM_MAC_description": "",
"UI_NOT_RANDOM_MAC_name": "",
"UI_PRESENCE_description": "",
"UI_PRESENCE_name": "",
"UI_REFRESH_description": "",
"UI_REFRESH_name": "",
"VERSION_description": "",
"VERSION_name": "",
"WF_Action_Add": "",
"WF_Action_field": "",
"WF_Action_type": "",
"WF_Action_value": "",
"WF_Actions": "",
"WF_Add": "",
"WF_Add_Condition": "",
"WF_Add_Group": "",
"WF_Condition_field": "",
"WF_Condition_operator": "",
"WF_Condition_value": "",
"WF_Conditions": "",
"WF_Conditions_logic_rules": "",
"WF_Duplicate": "",
"WF_Enabled": "",
"WF_Export": "",
"WF_Export_Copy": "",
"WF_Import": "",
"WF_Import_Copy": "",
"WF_Name": "",
"WF_Remove": "",
"WF_Remove_Copy": "",
"WF_Save": "",
"WF_Trigger": "",
"WF_Trigger_event_type": "",
"WF_Trigger_type": "",
"add_icon_event_tooltip": "",
"add_option_event_tooltip": "",
"copy_icons_event_tooltip": "",
"devices_old": "",
"general_event_description": "",
"general_event_title": "",
"go_to_device_event_tooltip": "",
"go_to_node_event_tooltip": "",
"new_version_available": "",
"report_guid": "",
"report_guid_missing": "",
"report_select_format": "",
"report_time": "",
"run_event_tooltip": "",
"select_icon_event_tooltip": "",
"settings_core_icon": "",
"settings_core_label": "",
"settings_device_scanners": "",
"settings_device_scanners_icon": "",
"settings_device_scanners_info": "",
"settings_device_scanners_label": "",
"settings_enabled": "",
"settings_enabled_icon": "",
"settings_expand_all": "",
"settings_imported": "",
"settings_imported_label": "",
"settings_missing": "",
"settings_missing_block": "",
"settings_old": "",
"settings_other_scanners": "",
"settings_other_scanners_icon": "",
"settings_other_scanners_label": "",
"settings_publishers": "",
"settings_publishers_icon": "",
"settings_publishers_info": "",
"settings_publishers_label": "",
"settings_readonly": "",
"settings_saved": "",
"settings_system_icon": "",
"settings_system_label": "",
"settings_update_item_warning": "",
"test_event_tooltip": ""
}

View File

@@ -10,6 +10,7 @@
{ "code": "fa_fa", "display": "Farsi (fa_fa)" },
{ "code": "id_id", "display": "Indonesian (id_id)" },
{ "code": "fr_fr", "display": "French (fr_fr)" },
{ "code": "fi_fi", "display": "Finnish (fi_fi)" },
{ "code": "he_il", "display": "Hebrew (he_il)" },
{ "code": "it_it", "display": "Italian (it_it)" },
{ "code": "ja_jp", "display": "Japanese (ja_jp)" },

View File

@@ -27,8 +27,8 @@
"AppEvents_ObjectType": "Tipo de Objeto",
"AppEvents_Plugin": "Plugin",
"AppEvents_Type": "Tipo",
"BACKEND_API_URL_description": "",
"BACKEND_API_URL_name": "",
"BACKEND_API_URL_description": "Usado para permitir que o frontend comunique com o backend. Por padrão isto é definido para <code>/server</code> e geralmente não deve ser mudado.",
"BACKEND_API_URL_name": "URL do API do backend",
"BackDevDetail_Actions_Ask_Run": "Deseja executar esta ação?",
"BackDevDetail_Actions_Not_Registered": "Ação não registada: ",
"BackDevDetail_Actions_Title_Run": "Executar ação",
@@ -98,10 +98,10 @@
"DevDetail_MainInfo_Network": "<i class=\"fa fa-server\"> </i> Node (MAC)",
"DevDetail_MainInfo_Network_Port": "<i class=\"fa fa-ethernet\"></i>Porta",
"DevDetail_MainInfo_Network_Site": "Site",
"DevDetail_MainInfo_Network_Title": "Rede",
"DevDetail_MainInfo_Network_Title": "Detalhes de Rede",
"DevDetail_MainInfo_Owner": "Proprietário",
"DevDetail_MainInfo_SSID": "SSID",
"DevDetail_MainInfo_Title": "Informações principais",
"DevDetail_MainInfo_Title": "Informação de Dispositivo",
"DevDetail_MainInfo_Type": "Tipo",
"DevDetail_MainInfo_Vendor": "Fornecedor",
"DevDetail_MainInfo_mac": "MAC",
@@ -139,7 +139,7 @@
"DevDetail_SessionTable_Duration": "Duração",
"DevDetail_SessionTable_IP": "IP",
"DevDetail_SessionTable_Order": "Ordem",
"DevDetail_Shortcut_CurrentStatus": "Estado atual",
"DevDetail_Shortcut_CurrentStatus": "Estado",
"DevDetail_Shortcut_DownAlerts": "Alertas para baixo",
"DevDetail_Shortcut_Presence": "Presença",
"DevDetail_Shortcut_Sessions": "Sessões",
@@ -198,22 +198,22 @@
"DevDetail_button_Save": "Gravar",
"DeviceEdit_ValidMacIp": "Insira um endereço <b>Mac</b> e <b>IP</b> válidos.",
"Device_MultiEdit": "Edição múltipla",
"Device_MultiEdit_Backup": "",
"Device_MultiEdit_Backup": "Cuidado, introduzir valores errados abaixo pode quebrar a sua configuração. Por favor configure a sua base de dados ou configurações de Dispositivos primeiro (<a href=\"#\" onclick=\"ExportCSV()\">clique para transferir <i class=\"fa-solid fa-download fa-bounce\"></i></a>). Leia como recuperar Dispositivos a partir deste ficheiro na <a href=\"https://docs.netalertx.com/BACKUPS#scenario-2-corrupted-database\" target=\"_blank\">Documentação de cópias de segurança</a>. De maneira a aplicar as suas mudanças clique no ícone <b>Guardar<i class=\"fa-solid fa-save\"></i></b> em cada campo que deseja atualizar.",
"Device_MultiEdit_Fields": "Editar campos:",
"Device_MultiEdit_MassActions": "Ações em massa:",
"Device_MultiEdit_No_Devices": "Nenhum dispositivo selecionado.",
"Device_MultiEdit_Tooltip": "Cuidadoso. Clicar aqui aplicará o valor à esquerda a todos os dispositivos selecionados acima.",
"Device_NextScan_Imminent": "",
"Device_NextScan_In": "",
"Device_NoData_Help": "",
"Device_NoData_Scanning": "",
"Device_NoData_Title": "",
"Device_NoMatch_Title": "",
"Device_Save_Failed": "",
"Device_Save_Unauthorized": "",
"Device_Saved_Success": "",
"Device_Saved_Unexpected": "",
"Device_Scanning": "",
"Device_NextScan_Imminent": "Iminente...",
"Device_NextScan_In": "Próxima análise em aproximadamente ",
"Device_NoData_Help": "Se dispositivos não aparecem após a análise, verifique a sua definição SCAN_SUBNETS e a <a href=\"https://docs.netalertx.com/SUBNETS\" target=\"_blank\">documentação</a>.",
"Device_NoData_Scanning": "Espere primeiro pela análise - isto pode levar vários minutos após a primeira configuração inicial.",
"Device_NoData_Title": "Nenhum dispositivo encontrado ainda",
"Device_NoMatch_Title": "Nenhum dispositivo corresponde ao filtro atual",
"Device_Save_Failed": "Falha ao guardar dispositivo",
"Device_Save_Unauthorized": "Não autorizado - token de API inválido",
"Device_Saved_Success": "Dispositivo guardado com sucesso",
"Device_Saved_Unexpected": "Atualização de dispositivo devolveu uma resposta inesperada",
"Device_Scanning": "Analisando...",
"Device_Searchbox": "Procurar",
"Device_Shortcut_AllDevices": "Os meus dispositivos",
"Device_Shortcut_AllNodes": "Todos os Nodes",
@@ -225,18 +225,18 @@
"Device_Shortcut_Favorites": "Favoritos",
"Device_Shortcut_NewDevices": "Novo dispostivo",
"Device_Shortcut_OnlineChart": "Presença do dispositivo",
"Device_Shortcut_Unstable": "",
"Device_Shortcut_Unstable": "Instável",
"Device_TableHead_AlertDown": "Alerta em baixo",
"Device_TableHead_Connected_Devices": "Conexões",
"Device_TableHead_CustomProps": "Propriedades / Ações",
"Device_TableHead_FQDN": "FQDN",
"Device_TableHead_Favorite": "Favorito",
"Device_TableHead_FirstSession": "Primeira sessão",
"Device_TableHead_Flapping": "",
"Device_TableHead_Flapping": "Flapping",
"Device_TableHead_GUID": "GUID",
"Device_TableHead_Group": "Grupo",
"Device_TableHead_IPv4": "",
"Device_TableHead_IPv6": "",
"Device_TableHead_IPv4": "IPv4",
"Device_TableHead_IPv6": "IPv6",
"Device_TableHead_Icon": "Ícone",
"Device_TableHead_LastIP": "Último IP",
"Device_TableHead_LastIPOrder": "Último pedido de IP",
@@ -260,7 +260,7 @@
"Device_TableHead_SyncHubNodeName": "Nó de sincronização",
"Device_TableHead_Type": "Tipo",
"Device_TableHead_Vendor": "Fornecedor",
"Device_TableHead_Vlan": "",
"Device_TableHead_Vlan": "VLAN",
"Device_Table_Not_Network_Device": "Não configurado como um dispositivo de rede",
"Device_Table_info": "A mostrar _START_ to _END_ of _TOTAL_ entradas",
"Device_Table_nav_next": "Próximo",
@@ -308,14 +308,14 @@
"Events_Tablelenght": "Mostrar entradas do _MENU_",
"Events_Tablelenght_all": "Todos",
"Events_Title": "Eventos",
"FakeMAC_hover": "",
"FieldLock_Error": "",
"FieldLock_Lock_Tooltip": "",
"FieldLock_Locked": "",
"FieldLock_SaveBeforeLocking": "",
"FieldLock_Source_Label": "",
"FieldLock_Unlock_Tooltip": "",
"FieldLock_Unlocked": "",
"FakeMAC_hover": "Este dispositivo tem um endereço MAC falso/alterado",
"FieldLock_Error": "Erro ao atualizar o estado de bloqueio do campo",
"FieldLock_Lock_Tooltip": "Bloquear campo (evita sobrescrita de plugins)",
"FieldLock_Locked": "Campo bloqueado",
"FieldLock_SaveBeforeLocking": "Guarde as suas mudanças antes de bloquear",
"FieldLock_Source_Label": "Fonte: ",
"FieldLock_Unlock_Tooltip": "Desbloquear campo (permite sobrescritas de plugins)",
"FieldLock_Unlocked": "Campo desbloqueado",
"GRAPHQL_PORT_description": "O número da porta do servidor GraphQL. Certifique-se de que a porta seja exclusiva em todas as suas aplicações neste host e nas instâncias do NetAlertX.",
"GRAPHQL_PORT_name": "Porta GraphQL",
"Gen_Action": "Ação",
@@ -323,7 +323,7 @@
"Gen_AddDevice": "Adicionar dispositivo",
"Gen_Add_All": "Adicionar todos",
"Gen_All_Devices": "Todos os dispostivos",
"Gen_Archived": "",
"Gen_Archived": "Arquivado",
"Gen_AreYouSure": "Tem certeza?",
"Gen_Backup": "Executar backup",
"Gen_Cancel": "Cancelar",
@@ -334,16 +334,16 @@
"Gen_Delete": "Apagar",
"Gen_DeleteAll": "Apagar todos",
"Gen_Description": "Descrição",
"Gen_Down": "",
"Gen_Down": "Em Baixo",
"Gen_Error": "Erro",
"Gen_Filter": "Filtro",
"Gen_Flapping": "",
"Gen_Flapping": "Flapping",
"Gen_Generate": "Gerar",
"Gen_InvalidMac": "Endereço MAC Inválido.",
"Gen_Invalid_Value": "",
"Gen_Invalid_Value": "Um valor inválido foi inserido",
"Gen_LockedDB": "ERRO - A base de dados pode estar bloqueada - Verifique F12 Ferramentas de desenvolvimento -> Console ou tente mais tarde.",
"Gen_NetworkMask": "Máscara de Rede",
"Gen_New": "",
"Gen_New": "Novo",
"Gen_Offline": "Offline",
"Gen_Okay": "Ok",
"Gen_Online": "Online",
@@ -361,7 +361,7 @@
"Gen_SelectIcon": "<i class=\"fa-solid fa-chevron-down fa-fade\"></i>",
"Gen_SelectToPreview": "Selecionar para pré-visualizar",
"Gen_Selected_Devices": "Seleciona dispostivos:",
"Gen_Sleeping": "",
"Gen_Sleeping": "A Dormir",
"Gen_Subnet": "Sub-rede",
"Gen_Switch": "Trocar",
"Gen_Upd": "Atualizado com sucesso",
@@ -375,9 +375,9 @@
"Gen_create_new_device_info": "Os dispositivos são normalmente descobertos usando <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">plugins</a>. No entanto, em certos casos, pode ser necessário adicionar dispositivos manualmente. Para explorar cenários específicos, verifique a <a target=\"_blank\" href=\"https://docs.netalertx.com/REMOTE_NETWORKS\">documentação de Redes Remotas</a>.",
"General_display_name": "Geral",
"General_icon": "<i class=\"fa fa-gears\"></i>",
"HRS_TO_KEEP_NEWDEV_description": "",
"HRS_TO_KEEP_NEWDEV_description": "Isto é uma definição de manutenção <b>ELIMINAR dispositivos</b>. Se ativado (<code>0</code> é desativado), dispositivos marcados como <b>Novo dispositivo</b> serão eliminados se o seu tempo de <b>Primeira Sessão</b> foi mais antigo que as horas especificadas nesta definição. Use esta definição se quer auto-eliminar <b>Novos dispositivos</b> após <code>X</code> horas.",
"HRS_TO_KEEP_NEWDEV_name": "Remover novos dispostivos depois",
"HRS_TO_KEEP_OFFDEV_description": "",
"HRS_TO_KEEP_OFFDEV_description": "Isto é uma definição de manutenção <b>ELIMINAR dispositivos</b>. Se ativado (<code>0</code> é desativado), dispositivos que estão <b>Offline</b> e a sua data de <b>Última conexão</b> foi mais antigo que as horas especificadas nesta definição, será eliminado. Use esta definição se quer auto-eliminar <b>Dispositivos Offline</b> após <code>X</code> horas de estarem offline.",
"HRS_TO_KEEP_OFFDEV_name": "Apagar dispositivos offline após",
"LOADED_PLUGINS_description": "Quais plugins carregar. Adicionar plugins pode deixar a aplicação lenta. Leia mais sobre quais plugins precisam ser ativados, tipos ou opções de escaneamento na <a target=\"_blank\" href=\"https://docs.netalertx.com/PLUGINS\">documentação de plugins</a>. Plugins descarregados perderão as suas configurações. Somente plugins <code>desativados</code> podem ser descarregados.",
"LOADED_PLUGINS_name": "Plugins carregados",
@@ -416,7 +416,7 @@
"Maintenance_Tool_ExportCSV": "Exportar dispostivos (csv)",
"Maintenance_Tool_ExportCSV_noti": "Exportar dispostivos (csv)",
"Maintenance_Tool_ExportCSV_noti_text": "Tem a certeza de que pretende gerar um ficheiro CSV?",
"Maintenance_Tool_ExportCSV_text": "Gere um ficheiro CSV (valor separado por vírgula) contendo a lista de dispositivos, incluindo os relacionamentos de rede entre os nós de rede e os dispositivos conectados. Também pode acionar isto a aceder esta URL <code>your_NetAlertX_url/php/server/devices.php?action=ExportCSV</code> ou ativando o plugin <a href=\"settings.php#CSVBCKP_header\">CSV Backup</a>.",
"Maintenance_Tool_ExportCSV_text": "Gere um ficheiro CSV (valor separado por vírgula) contendo a lista de dispositivos, incluindo os relacionamentos de rede entre os nós de rede e os dispositivos conectados. Também pode acionar isto ativando o plugin <a href=\"settings.php#CSVBCKP_header\">CSV Backup</a>.",
"Maintenance_Tool_ImportCSV": "Importação de dispositivos (csv)",
"Maintenance_Tool_ImportCSV_noti": "Importação de dispositivos (csv)",
"Maintenance_Tool_ImportCSV_noti_text": "Tem certeza de que deseja importar o ficheiro CSV? Isto <b>sobrescreverá</b> completamente os dispositivos na sua base de dados.",
@@ -428,10 +428,10 @@
"Maintenance_Tool_ImportPastedConfig": "Configurações Importar (colar)",
"Maintenance_Tool_ImportPastedConfig_noti_text": "Tem certeza de que deseja importar as configurações coladas? Isto irá <b>sobrescrever</b> completamente o ficheiro <code>app.conf</code>.",
"Maintenance_Tool_ImportPastedConfig_text": "Importa o ficheiro <code>app.conf</code> contendo todas as configurações da aplicação. Pode descarregar primeiro o ficheiro <code>app.conf</code> com a <b>Exportação de configurações</b>.",
"Maintenance_Tool_UnlockFields": "",
"Maintenance_Tool_UnlockFields_noti": "",
"Maintenance_Tool_UnlockFields_noti_text": "",
"Maintenance_Tool_UnlockFields_text": "",
"Maintenance_Tool_UnlockFields": "Desbloquear Campos do Dispositivo",
"Maintenance_Tool_UnlockFields_noti": "Desbloquear Campos do Dispositivo",
"Maintenance_Tool_UnlockFields_noti_text": "Tem a certeza que quer limpar todos os valores fonte (LOCKED/USER) para todos os campos de dispositivo em todos os dispositivos? Esta ação não pode ser desfeita.",
"Maintenance_Tool_UnlockFields_text": "Esta ferramenta removerá todos os valores fonte de todos os campos rastreados para todos os dispositivos, efetivamente desbloqueando todos os campos para plugins e utilizadores. Use isto com caução, uma vez que afetará todo o inventário do seu dispositivo.",
"Maintenance_Tool_arpscansw": "Alternar arp-Scan (ligado/desligado)",
"Maintenance_Tool_arpscansw_noti": "Ativar ou desativar o arp-Scan",
"Maintenance_Tool_arpscansw_noti_text": "Quando a análise é desligada, permanece desligada até ser novamente ativada.",
@@ -441,9 +441,9 @@
"Maintenance_Tool_backup_noti_text": "Tem a certeza de que pretende executar a cópia de segurança da BD? Certifique-se de que não está a ser executada nenhuma verificação.",
"Maintenance_Tool_backup_text": "Os backups da base de dados estão localizadas no diretório da base de dados como um arquivo zip, nomeado com a data de criação. Não há nenhum número máximo de backups.",
"Maintenance_Tool_check_visible": "Desmarque para esconder a coluna.",
"Maintenance_Tool_clearSourceFields_selected": "",
"Maintenance_Tool_clearSourceFields_selected_noti": "",
"Maintenance_Tool_clearSourceFields_selected_text": "",
"Maintenance_Tool_clearSourceFields_selected": "Limpar campos fonte",
"Maintenance_Tool_clearSourceFields_selected_noti": "Limpar fontes",
"Maintenance_Tool_clearSourceFields_selected_text": "Isto limpará todos os campos fonte para os dispositivos selecionados. Esta ação não pode ser desfeita.",
"Maintenance_Tool_darkmode": "Modos de alternância (escuro/claro)",
"Maintenance_Tool_darkmode_noti": "Modos de alternância",
"Maintenance_Tool_darkmode_noti_text": "Após a mudança de tema, a página tenta recarregar-se para ativar a alteração. Se necessário, a cache deve ser limpa.",
@@ -474,7 +474,7 @@
"Maintenance_Tool_del_unknowndev_noti": "Eliminar dispositivos desconhecidos",
"Maintenance_Tool_del_unknowndev_noti_text": "Tem certeza que deseja apagar todos (desconhecidos) e (nome não encontrados) dispositivos?",
"Maintenance_Tool_del_unknowndev_text": "Antes de usar esta função, faça um backup. Apagar não pode ser desfeito. Todos os dispositivos nomeados (não conhecidos) serão apagados da base de dados.",
"Maintenance_Tool_del_unlockFields_selecteddev_text": "",
"Maintenance_Tool_del_unlockFields_selecteddev_text": "Isto desbloqueará os campos LOCKED/USER para os dispositivos selecionados. Esta ação não pode ser desfeita.",
"Maintenance_Tool_displayed_columns_text": "Altere a visibilidade e a ordem das colunas na página <a href=\"devices.php\"><b> <i class=\"fa fa-portátil\"></i> Dispositivos</b></a>.",
"Maintenance_Tool_drag_me": "Arraste-me para reordenar colunas.",
"Maintenance_Tool_order_columns_text": "Maintenance_Tool_order_columns_text",
@@ -486,8 +486,8 @@
"Maintenance_Tool_restore_noti": "Restauração de DB",
"Maintenance_Tool_restore_noti_text": "Tem a certeza de que quer executar a Restauração DB? Certifique-se de que nenhuma varredura funciona atualmente.",
"Maintenance_Tool_restore_text": "O backup mais recente pode ser restaurado através do botão, mas os backups mais antigos só podem ser restaurados manualmente. Após a restauração, faça uma verificação de integridade na base de dados para segurança, caso o db estivesse atualmente em acesso de gravação quando o backup foi criado.",
"Maintenance_Tool_unlockFields_selecteddev": "",
"Maintenance_Tool_unlockFields_selecteddev_noti": "",
"Maintenance_Tool_unlockFields_selecteddev": "Desbloquear campos de dispositivo",
"Maintenance_Tool_unlockFields_selecteddev_noti": "Desbloquear campos",
"Maintenance_Tool_upgrade_database_noti": "Atualizar a base de dados",
"Maintenance_Tool_upgrade_database_noti_text": "Tem certeza de que deseja atualizar a base de dados?<br>(talvez prefira arquivá-la)",
"Maintenance_Tool_upgrade_database_text": "Este botão atualizará a base de dados para ativar o gráfico Atividade de rede nas últimas 12 horas. Faça uma cópia de segurança da sua base de dados em caso de problemas.",
@@ -563,34 +563,34 @@
"Network_ManageEdit_Name_text": "Nome sem caracteres especiais",
"Network_ManageEdit_Port": " Nova contagem de portas",
"Network_ManageEdit_Port_text": "Deixe em branco para Wi-Fi e Powerline.",
"Network_ManageEdit_Submit": "",
"Network_ManageEdit_Type": "",
"Network_ManageEdit_Type_text": "",
"Network_ManageLeaf": "",
"Network_ManageUnassign": "",
"Network_NoAssignedDevices": "",
"Network_NoDevices": "",
"Network_Node": "",
"Network_Node_Name": "",
"Network_Parent": "",
"Network_Root": "",
"Network_Root_Not_Configured": "",
"Network_Root_Unconfigurable": "",
"Network_ShowArchived": "",
"Network_ShowOffline": "",
"Network_Table_Hostname": "",
"Network_Table_IP": "",
"Network_Table_State": "",
"Network_Title": "",
"Network_UnassignedDevices": "",
"Notifications_All": "",
"Notifications_Mark_All_Read": "",
"PIALERT_WEB_PASSWORD_description": "",
"PIALERT_WEB_PASSWORD_name": "",
"PIALERT_WEB_PROTECTION_description": "",
"PIALERT_WEB_PROTECTION_name": "",
"PLUGINS_KEEP_HIST_description": "",
"PLUGINS_KEEP_HIST_name": "",
"Network_ManageEdit_Submit": "Guardar Alterações",
"Network_ManageEdit_Type": "Novo tipo de dispositivo",
"Network_ManageEdit_Type_text": "-- Selecionar tipo --",
"Network_ManageLeaf": "Gerir atribuição",
"Network_ManageUnassign": "Cancelar Atribuição",
"Network_NoAssignedDevices": "Este nó de rede não tem quaisquer dispositivos atribuídos (nós folha). Atribua um abaixo ou vá ao separador <b><i class=\"fa fa-info-circle\"> Detalhes</b> em qualquer dispositivo em <a href=\"devices.php\"><b><i class=\"fa fa-laptop\"></i> Dispositivos</b></a>, e atribua-o a um <i <b><i class=\"fa fa-server\"></i> Nó de rede (MAC)</b> e <b><i class=\"fa fa-ethernet\"></i> Porta</b> lá.",
"Network_NoDevices": "Sem dispositivos para configurar",
"Network_Node": "Nó de rede",
"Network_Node_Name": "Nome do nó",
"Network_Parent": "Dispositivo da rede parente",
"Network_Root": "Nó raiz",
"Network_Root_Not_Configured": "Selecione um tipo de dispositivo de rede, por exemplo um <b>Gateway</b>, no campo <b>Tipo</b> do <a href=\"deviceDetails.php?mac=Internet\">o dispositivo raiz da Internet</a> para começar a configurar este ecrã. <br/><br/> Mais documentação pode ser encontrada no guia <a href=\"https://docs.netalertx.com/NETWORK_TREE\" target=\"_blank\">Como configurar a sua página de Rede</a>",
"Network_Root_Unconfigurable": "Raiz não configurável",
"Network_ShowArchived": "Mostrar arquivados",
"Network_ShowOffline": "Mostrar offline",
"Network_Table_Hostname": "Nome do anfitrião",
"Network_Table_IP": "IP",
"Network_Table_State": "Estado",
"Network_Title": "Visão geral da rede",
"Network_UnassignedDevices": "Dispositivos não atribuídos",
"Notifications_All": "Todas as notificações",
"Notifications_Mark_All_Read": "Marcar todas como lidas",
"PIALERT_WEB_PASSWORD_description": "A palavra passe padrão é <code>123456</code>. Para mudar esta palavra passe execute <code>/app/back/pialert-cli</code> no contentor ou use o <a onclick=\"toggleAllSettings()\" href=\"#SETPWD_RUN\"><code>SETPWD_RUN</code> Definir plugin de palavra passe</a>.",
"PIALERT_WEB_PASSWORD_name": "Palavra passe de início de sessão",
"PIALERT_WEB_PROTECTION_description": "Quando ativo um diálogo de início de sessão é mostrado. Leia abaixo com cuidado se ficar trancado fora da sua instância.",
"PIALERT_WEB_PROTECTION_name": "Ativar início de sessão",
"PLUGINS_KEEP_HIST_description": "Quantas entradas de resultados de análise de Histórico de Plugins devem ser mantidos (por Plugin, e não específico a dispositivos).",
"PLUGINS_KEEP_HIST_name": "Histórico de Plugins",
"PRAGMA_JOURNAL_SIZE_LIMIT_description": "",
"PRAGMA_JOURNAL_SIZE_LIMIT_name": "",
"Plugins_DeleteAll": "",

211
front/plugins/fritzbox/README.md Executable file
View File

@@ -0,0 +1,211 @@
## Overview
The Fritz!Box plugin queries connected devices from a Fritz!Box router using the **TR-064** protocol (Technical Report 064), a standardized interface for managing DSL routers and home network devices. This plugin discovers all network-connected devices and reports their MAC addresses, IP addresses, hostnames, and connection types to NetAlertX.
TR-064 is a UPnP-based protocol that provides programmatic access to Fritz!Box configuration and status information. Unlike web scraping, it offers a stable, documented API that works across Fritz!Box models.
### Features
- **Device Discovery**: Automatically detects all connected devices (WiFi 2.4GHz, WiFi 5GHz, Ethernet)
- **Real-time Status**: Reports active connection status for each device
- **Guest WiFi Monitoring**: Optional synthetic Access Point device to track guest network status
- **Flexible Filtering**: Choose to report only active devices or include disconnected devices in Fritz!Box memory
- **Secure Connection**: Supports both HTTP and HTTPS with configurable SSL verification
> [!TIP]
> TR-064 is typically enabled by default on Fritz!Box routers. If you encounter connection issues, check that it hasn't been disabled in your Fritz!Box settings under **Home Network > Network > Network Settings > Allow access for applications**.
### Quick Setup Guide
To set up the plugin correctly:
1. **Enable TR-064 on Fritz!Box** (usually already enabled):
- Log in to your Fritz!Box web interface (typically `fritz.box` or `192.168.178.1`)
- Navigate to: **Home Network > Network > Network Settings**
- Ensure **"Allow access for applications"** is checked
- Note: Some models show this as **"Allow remote access"** - enable both HTTP and HTTPS
2. **Configure Plugin in NetAlertX**:
- Head to **Settings** > **Fritz!Box Plugin**
- Set the required settings (see below)
- Choose run mode: **schedule** (recommended, runs every 5 minutes)
#### Required Settings
- **Fritz!Box Host** (`FRITZBOX_HOST`): Hostname or IP address of your Fritz!Box
- Default: `fritz.box`
- Alternative: `192.168.178.1` (or your Fritz!Box's IP)
- **TR-064 Port** (`FRITZBOX_PORT`): Port for TR-064 protocol
- Default: `49443` (HTTPS). Use `49000` if HTTPS is disabled
- **Username** (`FRITZBOX_USER`): Fritz!Box username
- Can be empty for some models when accessing from local network
- For newer models, use an admin username
- **Password** (`FRITZBOX_PASS`): Fritz!Box password
- Required: Your Fritz!Box admin password
#### Optional Settings
- **Use HTTPS** (`FRITZBOX_USE_TLS`): Enable secure HTTPS connection (default: `true`)
- Recommended for security
- Requires port `49443` instead of `49000`
- **Report Guest WiFi** (`FRITZBOX_REPORT_GUEST`): Create Access Point device for guest WiFi (default: `false`)
- When enabled, adds a synthetic "Guest WiFi Network" device to your device list
- Device appears only when guest WiFi is active
- Useful for monitoring guest network status
- **Guest WiFi Service** (`FRITZBOX_GUEST_SERVICE`): Which WLANConfiguration service is the guest network (default: `3`)
- Fritz!Box typically uses `1` for 2.4GHz, `2` for 5GHz, `3` for guest WiFi
- Only relevant when **Report Guest WiFi** is enabled
- Change this if your Fritz!Box uses a non-standard configuration
- **Active Devices Only** (`FRITZBOX_ACTIVE_ONLY`): Report only connected devices (default: `true`)
- When enabled, only currently connected devices appear
- When disabled, includes all devices stored in Fritz!Box memory (even if disconnected)
### Usage
1. Head to **Settings** > **Fritz!Box** to configure the plugin
2. Set **When to run** to **schedule** (recommended) or **once** for manual testing
3. The plugin will run every 5 minutes by default (configurable via **Schedule** setting)
4. View discovered devices in the **Devices** page
5. Check logs at `/tmp/log/plugins/script.FRITZBOX.log` for troubleshooting
### Device Information Reported
The plugin reports the following information for each device:
| Field | Description | Mapped To |
|-------|-------------|-----------|
| **MAC Address** | Device hardware address (normalized format) | `devMac` |
| **IP Address** | Current IPv4 address | `devLastIP` |
| **Hostname** | Device name from Fritz!Box | `devName` |
| **Connection Status** | "Active" or "Inactive" | Plugin table only (not mapped to device fields) |
| **Interface Type** | WiFi / LAN / Guest Network | `devType` |
### Guest WiFi Feature
When **Report Guest WiFi** is enabled and guest WiFi is active on your Fritz!Box:
- A synthetic device named **"Guest WiFi Network"** appears in your device list
- Device Type: **Access Point**
- MAC Address: Locally-administered synthetic MAC derived from Fritz!Box MAC (e.g., `02:a1:b2:c3:d4:e5`)
- Status: Only appears when guest WiFi is enabled
This allows you to:
- Monitor when guest WiFi is active
- Set up notifications when guest network is enabled/disabled
- Track guest network status alongside other network devices
> [!NOTE]
> The guest WiFi device is synthetic (not a real physical device). It's created by the plugin to represent the guest network state.
### Troubleshooting
#### Connection Refused / Timeout Errors
**Symptoms**: Plugin logs show "Failed to connect to Fritz!Box" or timeout errors
**Solutions**:
1. Verify Fritz!Box is reachable:
```bash
ping fritz.box
# or
ping 192.168.178.1
```
2. Check TR-064 is enabled:
- Fritz!Box web interface > **Home Network > Network > Network Settings**
- Enable **"Allow access for applications"**
3. Verify correct port:
- HTTP: Port `49000`
- HTTPS: Port `49443`
- Match **Use HTTPS** setting with port
4. Check firewall rules (if NetAlertX runs in Docker):
- Ensure container can reach Fritz!Box network
- Use host IP instead of `fritz.box` if DNS resolution fails
#### Authentication Failed
**Symptoms**: "Authentication error" or "Invalid credentials"
**Solutions**:
1. Verify password is correct
2. Try leaving **Username** empty (some models allow this from local network)
3. Create a dedicated user in Fritz!Box:
- **System > Fritz!Box Users > Add User**
- Grant network access permissions
4. For newer Fritz!OS versions, ensure user has **"Access from home network"** permission
#### No Devices Found
**Symptoms**: Plugin runs successfully but reports 0 devices
**Solutions**:
1. Check **Active Devices Only** setting:
- If enabled, only connected devices appear
- Disable to see all devices in Fritz!Box memory
2. Verify devices are actually connected to Fritz!Box
3. Check Fritz!Box web interface > **Home Network > Mesh** to see devices
4. Increase log level to `verbose` and check `/tmp/log/plugins/script.FRITZBOX.log`
#### Guest WiFi Not Detected
**Symptoms**: Guest WiFi enabled but no Access Point device appears
**Solutions**:
1. Ensure **Report Guest WiFi** is enabled
2. Guest WiFi must be **active** (not just configured)
3. Some Fritz!Box models don't expose guest network via TR-064
4. Check plugin logs for "Guest WiFi active" message
### Limitations
- **Active-only filtering**: When `FRITZBOX_ACTIVE_ONLY` is enabled, the plugin only reports currently connected devices. Disconnected devices stored in Fritz!Box memory are ignored.
- **Guest WiFi synthetic device**: The guest WiFi Access Point is a synthetic device created by the plugin. Its MAC address is derived from the Fritz!Box MAC and doesn't represent a physical device.
- **Model differences**: Some Fritz!Box models may not expose all TR-064 services (e.g., guest WiFi detection). The plugin degrades gracefully if services are unavailable.
- **IPv6 support**: Currently reports IPv4 addresses only. IPv6 support may be added in future versions.
- **Device type detection**: Interface type (WiFi/LAN) is reported, but detailed device categorization (smartphone, laptop, etc.) is handled by NetAlertX's device type detection, not this plugin.
### Technical Details
**Protocol**: TR-064 (Technical Report 064) - UPnP-based device management protocol
**Library**: [fritzconnection](https://github.com/kbr/fritzconnection) >= 1.15.1
**Services Used**:
- `FritzHosts`: Device discovery and information
- `WLANConfiguration`: Guest WiFi status detection
- `DeviceInfo`: Fritz!Box MAC address retrieval
**Execution Schedule**: Default every 5 minutes (configurable via cron syntax)
**Timeout**: 60 seconds (configurable via `RUN_TIMEOUT`)
### Notes
- **Performance**: TR-064 queries typically complete in under 2 seconds, even with many devices
- **Security**: Passwords are stored in NetAlertX's configuration database and not logged
- **Compatibility**: Tested with Fritz!Box models running Fritz!OS 7.x and 8.x
- **Dependencies**: Requires `fritzconnection` Python library (automatically installed via requirements.txt)
### Version
- **Version**: 1.0.0
- **Author**: [@sebingel](https://github.com/sebingel)
- **Release Date**: April 2026
### Support
For issues, questions, or feature requests:
- NetAlertX GitHub: [https://github.com/netalertx/NetAlertX](https://github.com/netalertx/NetAlertX)
- Fritz!Box TR-064 Documentation: [https://avm.de/service/schnittstellen/](https://avm.de/service/schnittstellen/)

2807
front/plugins/fritzbox/config.json Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,256 @@
#!/usr/bin/env python
import os
import sys
from pytz import timezone
# Define the installation path and extend the system path for plugin imports
INSTALL_PATH = os.getenv('NETALERTX_APP', '/app')
sys.path.extend([f"{INSTALL_PATH}/front/plugins", f"{INSTALL_PATH}/server"])
from const import logPath # noqa: E402, E261 [flake8 lint suppression]
from plugin_helper import Plugin_Objects, normalize_mac # noqa: E402, E261 [flake8 lint suppression]
from logger import mylog, Logger # noqa: E402, E261 [flake8 lint suppression]
from helper import get_setting_value # noqa: E402, E261 [flake8 lint suppression]
from utils.crypto_utils import string_to_fake_mac # noqa: E402, E261 [flake8 lint suppression]
from fritzconnection import FritzConnection # noqa: E402, E261 [flake8 lint suppression]
from fritzconnection.lib.fritzhosts import FritzHosts # noqa: E402, E261 [flake8 lint suppression]
import conf # noqa: E402, E261 [flake8 lint suppression]
# Make sure the TIMEZONE for logging is correct
conf.tz = timezone(get_setting_value('TIMEZONE'))
# Make sure log level is initialized correctly
Logger(get_setting_value('LOG_LEVEL'))
pluginName = 'FRITZBOX'
INTERFACE_MAP = {
'802.11': 'WiFi',
'Ethernet': 'LAN',
}
# Define the current path and log file paths
LOG_PATH = logPath + '/plugins'
LOG_FILE = os.path.join(LOG_PATH, f'script.{pluginName}.log')
RESULT_FILE = os.path.join(LOG_PATH, f'last_result.{pluginName}.log')
# Initialize the Plugin obj output file
plugin_objects = Plugin_Objects(RESULT_FILE)
def get_fritzbox_connection(host, port, user, password, use_tls):
"""
Create FritzConnection with error handling.
Returns: FritzConnection object or None on failure
"""
try:
mylog('verbose', [f'[{pluginName}] Attempting connection to {host}:{port} (TLS: {use_tls})'])
fc = FritzConnection(
address=host,
port=port,
user=user,
password=password,
use_tls=use_tls,
timeout=10,
)
mylog('verbose', [f'[{pluginName}] Successfully connected to Fritz!Box'])
mylog('verbose', [f'[{pluginName}] Model: {fc.modelname}, Software: {fc.system_version}'])
return fc
except Exception as e:
mylog('none', [f'[{pluginName}] ⚠ ERROR: Failed to connect to Fritz!Box: {e}'])
mylog('none', [f'[{pluginName}] Check host ({host}), port ({port}), and credentials'])
mylog('none', [f'[{pluginName}] Ensure TR-064 is enabled in Fritz!Box settings'])
return None
def get_connected_devices(fc, active_only):
"""
Query all hosts from Fritz!Box via FritzHosts service.
Use get_hosts_info() for count, then get_generic_host_entry(index) for each.
Filter by NewActive status if active_only=True.
Returns: List of device dictionaries
"""
devices = []
try:
hosts = FritzHosts(fc)
host_count = hosts.host_numbers
mylog('verbose', [f'[{pluginName}] Found {host_count} total hosts in Fritz!Box'])
for index in range(host_count):
try:
host_info = hosts.get_generic_host_entry(index)
# Extract relevant fields
mac_address = host_info.get('NewMACAddress', '')
ip_address = host_info.get('NewIPAddress', '')
hostname = host_info.get('NewHostName', '')
active = host_info.get('NewActive', 0)
interface_type = host_info.get('NewInterfaceType', 'Unknown')
# Skip if active_only and device is not active
if active_only and not active:
continue
# Skip entries without MAC address
if not mac_address:
continue
# Normalize MAC address
mac_address = normalize_mac(mac_address)
# Map interface type to readable format
interface_display = interface_type
for key, value in INTERFACE_MAP.items():
if key in interface_type:
interface_display = value
break
# Build device dictionary
device = {
'mac_address': mac_address,
'ip_address': ip_address if ip_address else '',
'hostname': hostname if hostname else 'Unknown',
'active_status': 'Active' if active else 'Inactive',
'interface_type': interface_display
}
devices.append(device)
mylog('verbose', [f'[{pluginName}] Device: {mac_address} ({hostname}) - {ip_address} - {interface_display}'])
except Exception as e:
mylog('minimal', [f'[{pluginName}] Warning: Failed to get host entry {index}: {e}'])
continue
mylog('verbose', [f'[{pluginName}] Processed {len(devices)} devices'])
except Exception as e:
mylog('none', [f'[{pluginName}] ⚠ ERROR: Failed to query devices: {e}'])
return devices
def check_guest_wifi_status(fc, guest_service_num):
"""
Query a specific WLANConfiguration service for guest network status.
Returns: Dict with active status and interface info
"""
guest_info = {
'active': False,
'ssid': 'Guest WiFi',
'interface': 'Guest Network'
}
try:
service = f'WLANConfiguration{guest_service_num}'
result = fc.call_action(service, 'GetInfo')
status = result.get('NewEnable', False)
ssid = result.get('NewSSID', '')
if status:
guest_info['active'] = True
guest_info['ssid'] = ssid if ssid else 'Guest WiFi'
mylog('verbose', [f'[{pluginName}] Guest WiFi active on service {guest_service_num}: {guest_info["ssid"]}'])
else:
mylog('verbose', [f'[{pluginName}] Guest WiFi service {guest_service_num} is disabled'])
except Exception as e:
mylog('minimal', [f'[{pluginName}] Warning: Failed to query WLANConfiguration{guest_service_num}: {e}'])
return guest_info
def create_guest_wifi_device(fc, host):
"""
Create a synthetic device entry for guest WiFi.
Derives a deterministic fake MAC from the Fritz!Box hardware MAC address.
Falls back to the configured host string if the MAC cannot be retrieved.
Returns: Device dictionary
"""
try:
fritzbox_mac = fc.call_action('DeviceInfo:1', 'GetInfo').get('NewMACAddress', '')
guest_mac = string_to_fake_mac(normalize_mac(fritzbox_mac) if fritzbox_mac else host)
device = {
'mac_address': guest_mac,
'ip_address': '',
'hostname': 'Guest WiFi Network',
'active_status': 'Active',
'interface_type': 'Access Point'
}
mylog('verbose', [f'[{pluginName}] Created guest WiFi device: {guest_mac}'])
return device
except Exception as e:
mylog('minimal', [f'[{pluginName}] Warning: Failed to create guest WiFi device: {e}'])
return None
def main():
mylog('verbose', [f'[{pluginName}] In script'])
# Retrieve configuration settings
host = get_setting_value('FRITZBOX_HOST')
port = get_setting_value('FRITZBOX_PORT')
user = get_setting_value('FRITZBOX_USER')
password = get_setting_value('FRITZBOX_PASS')
use_tls = get_setting_value('FRITZBOX_USE_TLS')
report_guest = get_setting_value('FRITZBOX_REPORT_GUEST')
guest_service = get_setting_value('FRITZBOX_GUEST_SERVICE')
active_only = get_setting_value('FRITZBOX_ACTIVE_ONLY')
mylog('verbose', [f'[{pluginName}] Settings: host={host}, port={port}, use_tls={use_tls}, active_only={active_only}'])
# Create Fritz!Box connection
fc = get_fritzbox_connection(host, port, user, password, use_tls)
if not fc:
mylog('none', [f'[{pluginName}] ⚠ ERROR: Could not establish connection to Fritz!Box'])
mylog('none', [f'[{pluginName}] Plugin will return empty results'])
plugin_objects.write_result_file()
return 1
# Retrieve device data
device_data = get_connected_devices(fc, active_only)
# Check guest WiFi if enabled
if report_guest:
guest_status = check_guest_wifi_status(fc, guest_service)
if guest_status['active']:
guest_device = create_guest_wifi_device(fc, host)
if guest_device:
device_data.append(guest_device)
# Process the data into native application tables
if device_data:
for device in device_data:
plugin_objects.add_object(
primaryId=device['mac_address'],
secondaryId=device['ip_address'],
watched1=device['hostname'],
watched2=device['active_status'],
watched3=device['interface_type'],
watched4='',
extra='',
foreignKey=device['mac_address']
)
mylog('verbose', [f'[{pluginName}] Successfully processed {len(device_data)} devices'])
else:
mylog('minimal', [f'[{pluginName}] No devices found'])
# Log result
plugin_objects.write_result_file()
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -25,3 +25,4 @@ yattag
zeroconf
psutil
freebox-api
fritzconnection>=1.15.1

View File

@@ -25,3 +25,4 @@ yattag
zeroconf
psutil
freebox-api
fritzconnection>=1.15.1

View File

@@ -34,3 +34,4 @@ freebox-api
mcp
psutil
pydantic>=2.0,<3.0
fritzconnection>=1.15.1

View File

@@ -585,6 +585,11 @@ def api_device_set_alias(mac, payload=None):
device_handler = DeviceInstance()
result = device_handler.updateDeviceColumn(mac, 'devName', alias)
if not result.get("success"):
err = result.get("error") or result.get("message") or f"Failed to update alias for device {mac}"
return jsonify({"success": False, "error": err})
return jsonify(result)

View File

@@ -33,9 +33,19 @@ COLUMN_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_]+$")
# Security whitelists & Literals for documentation
ALLOWED_DEVICE_COLUMNS = Literal[
# Main Info
"devName", "devOwner", "devType", "devVendor",
"devGroup", "devLocation", "devComments", "devFavorite",
"devParentMAC", "devCanSleep"
"devGroup", "devLocation", "devComments", "devIcon",
# Alerts & Behavior
"devFavorite", "devAlertEvents", "devAlertDown",
"devCanSleep", "devSkipRepeated", "devReqNicsOnline", "devForceStatus",
# Network topology
"devParentMAC", "devParentPort", "devParentRelType",
"devSSID", "devSite", "devVlan",
# Display / Status
"devStaticIP", "devIsNew", "devIsArchived",
# Custom properties
"devCustomProps",
]
ALLOWED_NMAP_MODES = Literal[
@@ -407,7 +417,7 @@ class UpdateDeviceColumnRequest(BaseModel):
class LockDeviceFieldRequest(BaseModel):
"""Request to lock/unlock a device field."""
fieldName: str = Field(..., description="Field name to lock/unlock (e.g., devName, devVendor). Required.")
lock: bool = Field(True, description="True to lock the field, False to unlock")
lock: bool = Field(False, description="True to lock the field, False (default) to unlock")
class UnlockDeviceFieldsRequest(BaseModel):
@@ -420,7 +430,7 @@ class UnlockDeviceFieldsRequest(BaseModel):
None,
description="List of field names to unlock. If omitted, all tracked fields will be unlocked"
)
clear_all: bool = Field(
clearAll: bool = Field(
False,
description="True to clear all sources, False to clear only LOCKED/USER"
)

View File

@@ -0,0 +1,423 @@
"""
Tests for Fritz!Box plugin (fritzbox.py).
fritzbox.py is imported directly. Its module-level side effects
(get_setting_value, Logger, Plugin_Objects) are patched out before the
first import so no live config reads, log files, or result files are
created during tests.
"""
import hashlib
import sys
import os
from unittest.mock import patch, MagicMock
import pytest
# ---------------------------------------------------------------------------
# Path setup
# ---------------------------------------------------------------------------
_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
_SERVER = os.path.join(_ROOT, "server")
_PLUGIN_DIR = os.path.join(_ROOT, "front", "plugins", "fritzbox")
for _p in [_ROOT, _SERVER, _PLUGIN_DIR]:
if _p not in sys.path:
sys.path.insert(0, _p)
# ---------------------------------------------------------------------------
# Import fritzbox with module-level side effects patched
# ---------------------------------------------------------------------------
# fritzbox.py calls get_setting_value(), Logger(), and Plugin_Objects() at
# module level. Patching these before the first import prevents live config
# reads, log-file creation, and result-file creation during tests.
with patch("helper.get_setting_value", return_value="UTC"), \
patch("logger.Logger"), \
patch("plugin_helper.Plugin_Objects"):
import fritzbox # noqa: E402
from plugin_helper import normalize_mac # noqa: E402
# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------
def _make_host_entry(mac="AA:BB:CC:DD:EE:FF", ip="192.168.1.10",
hostname="testdevice", active=1, interface="Ethernet"):
return {
"NewMACAddress": mac,
"NewIPAddress": ip,
"NewHostName": hostname,
"NewActive": active,
"NewInterfaceType": interface,
}
@pytest.fixture
def mock_fritz_hosts():
"""
Patches fritzconnection.lib.fritzhosts in sys.modules so that
fritzbox.get_connected_devices() uses a controllable FritzHosts mock.
Yields the FritzHosts *instance* (what FritzHosts(fc) returns).
"""
hosts_instance = MagicMock()
fritz_hosts_module = MagicMock()
fritz_hosts_module.FritzHosts = MagicMock(return_value=hosts_instance)
with patch.dict("sys.modules", {
"fritzconnection": MagicMock(),
"fritzconnection.lib": MagicMock(),
"fritzconnection.lib.fritzhosts": fritz_hosts_module,
}):
yield hosts_instance
# ===========================================================================
# get_connected_devices
# ===========================================================================
class TestGetConnectedDevices:
def test_returns_active_device(self, mock_fritz_hosts):
mock_fritz_hosts.host_numbers = 1
mock_fritz_hosts.get_generic_host_entry.return_value = _make_host_entry(active=1)
devices = fritzbox.get_connected_devices(MagicMock(), active_only=True)
assert len(devices) == 1
assert devices[0]["active_status"] == "Active"
def test_active_only_filters_inactive_device(self, mock_fritz_hosts):
mock_fritz_hosts.host_numbers = 2
mock_fritz_hosts.get_generic_host_entry.side_effect = [
_make_host_entry(mac="AA:BB:CC:DD:EE:01", active=1),
_make_host_entry(mac="AA:BB:CC:DD:EE:02", active=0),
]
devices = fritzbox.get_connected_devices(MagicMock(), active_only=True)
assert len(devices) == 1
assert devices[0]["mac_address"] == "aa:bb:cc:dd:ee:01"
def test_active_only_false_includes_inactive_device(self, mock_fritz_hosts):
mock_fritz_hosts.host_numbers = 2
mock_fritz_hosts.get_generic_host_entry.side_effect = [
_make_host_entry(mac="AA:BB:CC:DD:EE:01", active=1),
_make_host_entry(mac="AA:BB:CC:DD:EE:02", active=0),
]
devices = fritzbox.get_connected_devices(MagicMock(), active_only=False)
assert len(devices) == 2
assert devices[1]["active_status"] == "Inactive"
def test_device_without_mac_is_skipped(self, mock_fritz_hosts):
mock_fritz_hosts.host_numbers = 2
mock_fritz_hosts.get_generic_host_entry.side_effect = [
_make_host_entry(mac=""),
_make_host_entry(mac="AA:BB:CC:DD:EE:01"),
]
devices = fritzbox.get_connected_devices(MagicMock(), active_only=False)
assert len(devices) == 1
assert devices[0]["mac_address"] == "aa:bb:cc:dd:ee:01"
def test_ethernet_interface_maps_to_lan(self, mock_fritz_hosts):
mock_fritz_hosts.host_numbers = 1
mock_fritz_hosts.get_generic_host_entry.return_value = _make_host_entry(interface="Ethernet")
devices = fritzbox.get_connected_devices(MagicMock(), active_only=False)
assert devices[0]["interface_type"] == "LAN"
def test_wifi_interface_maps_to_wifi(self, mock_fritz_hosts):
mock_fritz_hosts.host_numbers = 1
mock_fritz_hosts.get_generic_host_entry.return_value = _make_host_entry(interface="802.11")
devices = fritzbox.get_connected_devices(MagicMock(), active_only=False)
assert devices[0]["interface_type"] == "WiFi"
def test_unknown_interface_is_preserved(self, mock_fritz_hosts):
mock_fritz_hosts.host_numbers = 1
mock_fritz_hosts.get_generic_host_entry.return_value = _make_host_entry(interface="SomeOtherType")
devices = fritzbox.get_connected_devices(MagicMock(), active_only=False)
assert devices[0]["interface_type"] == "SomeOtherType"
def test_mac_address_is_normalized_to_lowercase(self, mock_fritz_hosts):
mock_fritz_hosts.host_numbers = 1
mock_fritz_hosts.get_generic_host_entry.return_value = _make_host_entry(mac="AA:BB:CC:DD:EE:FF")
devices = fritzbox.get_connected_devices(MagicMock(), active_only=False)
assert devices[0]["mac_address"] == "aa:bb:cc:dd:ee:ff"
def test_missing_hostname_defaults_to_unknown(self, mock_fritz_hosts):
mock_fritz_hosts.host_numbers = 1
mock_fritz_hosts.get_generic_host_entry.return_value = _make_host_entry(hostname="")
devices = fritzbox.get_connected_devices(MagicMock(), active_only=False)
assert devices[0]["hostname"] == "Unknown"
def test_failed_host_entry_does_not_abort_remaining(self, mock_fritz_hosts):
mock_fritz_hosts.host_numbers = 3
mock_fritz_hosts.get_generic_host_entry.side_effect = [
_make_host_entry(mac="AA:BB:CC:DD:EE:01"),
Exception("TR-064 timeout"),
_make_host_entry(mac="AA:BB:CC:DD:EE:03"),
]
devices = fritzbox.get_connected_devices(MagicMock(), active_only=False)
assert len(devices) == 2
def test_empty_host_list_returns_empty(self, mock_fritz_hosts):
mock_fritz_hosts.host_numbers = 0
devices = fritzbox.get_connected_devices(MagicMock(), active_only=False)
assert devices == []
# ===========================================================================
# check_guest_wifi_status
# ===========================================================================
class TestCheckGuestWifiStatus:
def test_disabled_service_returns_inactive(self):
fc = MagicMock()
fc.call_action.return_value = {"NewEnable": False, "NewSSID": ""}
result = fritzbox.check_guest_wifi_status(fc, guest_service_num=3)
assert result["active"] is False
def test_enabled_service_returns_active(self):
fc = MagicMock()
fc.call_action.return_value = {"NewEnable": True, "NewSSID": "MyGuestWiFi"}
result = fritzbox.check_guest_wifi_status(fc, guest_service_num=3)
assert result["active"] is True
assert result["ssid"] == "MyGuestWiFi"
def test_queries_correct_service_number(self):
fc = MagicMock()
fc.call_action.return_value = {"NewEnable": True, "NewSSID": "Guest"}
fritzbox.check_guest_wifi_status(fc, guest_service_num=2)
fc.call_action.assert_called_once_with("WLANConfiguration2", "GetInfo")
def test_service_exception_returns_inactive(self):
fc = MagicMock()
fc.call_action.side_effect = Exception("Service unavailable")
result = fritzbox.check_guest_wifi_status(fc, guest_service_num=3)
assert result["active"] is False
def test_empty_ssid_uses_default_label(self):
fc = MagicMock()
fc.call_action.return_value = {"NewEnable": True, "NewSSID": ""}
result = fritzbox.check_guest_wifi_status(fc, guest_service_num=3)
assert result["active"] is True
assert result["ssid"] == "Guest WiFi"
def test_service1_can_be_guest(self):
fc = MagicMock()
fc.call_action.return_value = {"NewEnable": True, "NewSSID": "Gast"}
result = fritzbox.check_guest_wifi_status(fc, guest_service_num=1)
assert result["active"] is True
fc.call_action.assert_called_once_with("WLANConfiguration1", "GetInfo")
# ===========================================================================
# create_guest_wifi_device
# ===========================================================================
class TestCreateGuestWifiDevice:
def _fc_with_mac(self, mac):
fc = MagicMock()
fc.call_action.return_value = {"NewMACAddress": mac}
return fc
def test_returns_device_dict(self):
device = fritzbox.create_guest_wifi_device(self._fc_with_mac("AA:BB:CC:DD:EE:FF"))
assert device is not None
assert "mac_address" in device
assert device["hostname"] == "Guest WiFi Network"
assert device["active_status"] == "Active"
assert device["interface_type"] == "Access Point"
assert device["ip_address"] == ""
def test_guest_mac_has_locally_administered_bit(self):
"""First byte must be 0x02 — locally-administered, unicast."""
device = fritzbox.create_guest_wifi_device(self._fc_with_mac("AA:BB:CC:DD:EE:FF"))
first_byte = int(device["mac_address"].split(":")[0], 16)
assert first_byte == 0x02
def test_guest_mac_format_is_valid(self):
"""MAC must be 6 colon-separated lowercase hex pairs."""
device = fritzbox.create_guest_wifi_device(self._fc_with_mac("AA:BB:CC:DD:EE:FF"))
parts = device["mac_address"].split(":")
assert len(parts) == 6
for part in parts:
assert len(part) == 2
int(part, 16) # raises ValueError if not valid hex
def test_guest_mac_is_deterministic(self):
"""Same Fritz!Box MAC must always produce the same guest MAC."""
fc = self._fc_with_mac("AA:BB:CC:DD:EE:FF")
mac1 = fritzbox.create_guest_wifi_device(fc)["mac_address"]
mac2 = fritzbox.create_guest_wifi_device(fc)["mac_address"]
assert mac1 == mac2
def test_different_fritzbox_macs_produce_different_guest_macs(self):
mac_a = fritzbox.create_guest_wifi_device(self._fc_with_mac("AA:BB:CC:DD:EE:01"))["mac_address"]
mac_b = fritzbox.create_guest_wifi_device(self._fc_with_mac("AA:BB:CC:DD:EE:02"))["mac_address"]
assert mac_a != mac_b
def test_no_fritzbox_mac_uses_fallback(self):
"""When DeviceInfo returns no MAC, fall back to 02:00:00:00:00:01."""
fc = MagicMock()
fc.call_action.return_value = {"NewMACAddress": ""}
device = fritzbox.create_guest_wifi_device(fc)
assert device["mac_address"] == "02:00:00:00:00:01"
def test_device_info_exception_returns_none(self):
"""If DeviceInfo call raises, create_guest_wifi_device must return None."""
fc = MagicMock()
fc.call_action.side_effect = Exception("Connection refused")
device = fritzbox.create_guest_wifi_device(fc)
assert device is None
def test_known_mac_produces_known_guest_mac(self):
"""
Regression anchor: for a fixed Fritz!Box MAC, the expected guest MAC
is precomputed here independently. If the hashing logic in
fritzbox.py changes, this test fails immediately.
"""
fritzbox_mac = "aa:bb:cc:dd:ee:ff" # normalize_mac output of "AA:BB:CC:DD:EE:FF"
digest = hashlib.md5(f"GUEST:{fritzbox_mac}".encode()).digest()
expected = "02:" + ":".join(f"{b:02x}" for b in digest[:5])
device = fritzbox.create_guest_wifi_device(self._fc_with_mac("AA:BB:CC:DD:EE:FF"))
assert device["mac_address"] == expected
# ===========================================================================
# get_fritzbox_connection
# ===========================================================================
class TestGetFritzboxConnection:
def test_successful_connection(self):
fc_instance = MagicMock()
fc_instance.modelname = "FRITZ!Box 7590"
fc_instance.system_version = "7.57"
fc_class = MagicMock(return_value=fc_instance)
fc_module = MagicMock()
fc_module.FritzConnection = fc_class
with patch.dict("sys.modules", {"fritzconnection": fc_module}):
result = fritzbox.get_fritzbox_connection("fritz.box", 49443, "admin", "pass", True)
assert result is fc_instance
fc_class.assert_called_once_with(
address="fritz.box", port=49443, user="admin", password="pass", use_tls=True, timeout=10,
)
def test_import_error_returns_none(self):
with patch.dict("sys.modules", {"fritzconnection": None}):
result = fritzbox.get_fritzbox_connection("fritz.box", 49443, "admin", "pass", True)
assert result is None
def test_connection_exception_returns_none(self):
fc_module = MagicMock()
fc_module.FritzConnection.side_effect = Exception("Connection refused")
with patch.dict("sys.modules", {"fritzconnection": fc_module}):
result = fritzbox.get_fritzbox_connection("fritz.box", 49443, "admin", "pass", True)
assert result is None
# ===========================================================================
# main
# ===========================================================================
class TestMain:
_SETTINGS = {
"FRITZBOX_HOST": "fritz.box",
"FRITZBOX_PORT": 49443,
"FRITZBOX_USER": "admin",
"FRITZBOX_PASS": "secret",
"FRITZBOX_USE_TLS": True,
"FRITZBOX_REPORT_GUEST": False,
"FRITZBOX_GUEST_SERVICE": 3,
"FRITZBOX_ACTIVE_ONLY": True,
}
def _patch_settings(self):
return patch.object(
fritzbox, "get_setting_value",
side_effect=lambda key: self._SETTINGS[key],
)
def test_connection_failure_returns_1(self):
mock_po = MagicMock()
with self._patch_settings(), \
patch.object(fritzbox, "get_fritzbox_connection", return_value=None), \
patch.object(fritzbox, "plugin_objects", mock_po):
result = fritzbox.main()
assert result == 1
mock_po.write_result_file.assert_called_once()
mock_po.add_object.assert_not_called()
def test_scan_processes_devices(self):
devices = [
{"mac_address": "aa:bb:cc:dd:ee:01", "ip_address": "192.168.1.10",
"hostname": "device1", "active_status": "Active", "interface_type": "LAN"},
{"mac_address": "aa:bb:cc:dd:ee:02", "ip_address": "192.168.1.11",
"hostname": "device2", "active_status": "Active", "interface_type": "WiFi"},
]
mock_po = MagicMock()
with self._patch_settings(), \
patch.object(fritzbox, "get_fritzbox_connection", return_value=MagicMock()), \
patch.object(fritzbox, "get_connected_devices", return_value=devices), \
patch.object(fritzbox, "plugin_objects", mock_po):
result = fritzbox.main()
assert result == 0
assert mock_po.add_object.call_count == 2
mock_po.write_result_file.assert_called_once()
def test_guest_wifi_device_appended_when_active(self):
devices = [
{"mac_address": "aa:bb:cc:dd:ee:01", "ip_address": "192.168.1.10",
"hostname": "device1", "active_status": "Active", "interface_type": "LAN"},
]
guest_device = {
"mac_address": "02:a1:b2:c3:d4:e5", "ip_address": "",
"hostname": "Guest WiFi Network", "active_status": "Active",
"interface_type": "Access Point",
}
settings = {**self._SETTINGS, "FRITZBOX_REPORT_GUEST": True}
mock_po = MagicMock()
with patch.object(fritzbox, "get_setting_value", side_effect=lambda k: settings[k]), \
patch.object(fritzbox, "get_fritzbox_connection", return_value=MagicMock()), \
patch.object(fritzbox, "get_connected_devices", return_value=devices), \
patch.object(fritzbox, "check_guest_wifi_status", return_value={"active": True, "ssid": "Guest"}), \
patch.object(fritzbox, "create_guest_wifi_device", return_value=guest_device), \
patch.object(fritzbox, "plugin_objects", mock_po):
result = fritzbox.main()
assert result == 0
assert mock_po.add_object.call_count == 2 # 1 device + 1 guest
# Verify the guest device was passed correctly
guest_call = mock_po.add_object.call_args_list[1]
assert guest_call.kwargs["primaryId"] == "02:a1:b2:c3:d4:e5"
assert guest_call.kwargs["watched3"] == "Access Point"
def test_guest_wifi_not_appended_when_inactive(self):
devices = [
{"mac_address": "aa:bb:cc:dd:ee:01", "ip_address": "192.168.1.10",
"hostname": "device1", "active_status": "Active", "interface_type": "LAN"},
]
settings = {**self._SETTINGS, "FRITZBOX_REPORT_GUEST": True}
mock_po = MagicMock()
with patch.object(fritzbox, "get_setting_value", side_effect=lambda k: settings[k]), \
patch.object(fritzbox, "get_fritzbox_connection", return_value=MagicMock()), \
patch.object(fritzbox, "get_connected_devices", return_value=devices), \
patch.object(fritzbox, "check_guest_wifi_status", return_value={"active": False, "ssid": ""}), \
patch.object(fritzbox, "plugin_objects", mock_po):
result = fritzbox.main()
assert result == 0
assert mock_po.add_object.call_count == 1 # only the real device