mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-07 18:51:35 -07:00
Merge branch 'main' of github.com:netalertx/NetAlertX
Some checks are pending
Some checks are pending
This commit is contained in:
@@ -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 | | |
|
||||
|
||||
@@ -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",
|
||||
@@ -805,4 +805,4 @@
|
||||
"settings_system_label": "Sistema",
|
||||
"settings_update_item_warning": "Actualitza el valor sota. Sigues curós de seguir el format anterior. <b>No hi ha validació.</b>",
|
||||
"test_event_tooltip": "Deseu els canvis primer abans de comprovar la configuració."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "",
|
||||
@@ -805,4 +805,4 @@
|
||||
"settings_system_label": "",
|
||||
"settings_update_item_warning": "",
|
||||
"test_event_tooltip": "Guarde as alterações antes de testar as definições."
|
||||
}
|
||||
}
|
||||
|
||||
211
front/plugins/fritzbox/README.md
Executable file
211
front/plugins/fritzbox/README.md
Executable 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
2807
front/plugins/fritzbox/config.json
Executable file
File diff suppressed because it is too large
Load Diff
256
front/plugins/fritzbox/fritzbox.py
Executable file
256
front/plugins/fritzbox/fritzbox.py
Executable 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())
|
||||
@@ -25,3 +25,4 @@ yattag
|
||||
zeroconf
|
||||
psutil
|
||||
freebox-api
|
||||
fritzconnection>=1.15.1
|
||||
|
||||
@@ -25,3 +25,4 @@ yattag
|
||||
zeroconf
|
||||
psutil
|
||||
freebox-api
|
||||
fritzconnection>=1.15.1
|
||||
|
||||
@@ -34,3 +34,4 @@ freebox-api
|
||||
mcp
|
||||
psutil
|
||||
pydantic>=2.0,<3.0
|
||||
fritzconnection>=1.15.1
|
||||
|
||||
423
test/plugins/test_fritzbox.py
Normal file
423
test/plugins/test_fritzbox.py
Normal 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
|
||||
Reference in New Issue
Block a user