fml unifi

This commit is contained in:
Benoît Vézina 2025-03-11 16:19:49 -04:00
parent 5bfcaa37c2
commit b2183d8601
57 changed files with 5422 additions and 630 deletions

158
unifi_integration/README.md Normal file
View file

@ -0,0 +1,158 @@
# UniFi Integration for Odoo
Module d'intégration entre Odoo et les appareils UniFi Network (UDM Pro, UCG Max).
## API Integration Notes
### Authentication
The UniFi API requires specific endpoints and headers for authentication. Note that there are critical differences between UniFi controllers and the UDM Pro/UCG Max API:
```python
# Python Example
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
headers = {
"Accept": "application/json",
"Content-Type": "application/json"
}
data = {
'username': 'your_username',
'password': 'your_password',
'rememberMe': True
}
session = requests.Session()
response = session.post(
'https://udmp:443/api/auth/login',
headers=headers,
json=data,
verify=False
)
```
### API Endpoints
All API endpoints (except authentication) must be prefixed with `/proxy/network`. Common endpoints include:
#### System and Status
- System Health: `/api/system/health`
- System Info: `/proxy/network/api/s/{site}/stat/sysinfo`
- Dashboard Health: `/proxy/network/api/s/{site}/stat/health`
#### Network Configuration
- Networks: `/proxy/network/api/s/{site}/rest/networkconf`
- Firewall Rules: `/proxy/network/api/s/{site}/rest/firewallrule`
- Port Forwards: `/proxy/network/api/s/{site}/stat/portforward`
- Routes: `/proxy/network/api/s/{site}/rest/routing`
#### Devices and Clients
- All Devices: `/proxy/network/api/s/{site}/stat/device`
- Basic Device Info: `/proxy/network/api/s/{site}/stat/device-basic`
- Active Clients: `/proxy/network/api/s/{site}/stat/sta`
- All Clients: `/proxy/network/api/s/{site}/stat/user`
### Headers Required
All API requests must include these headers:
```python
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "Odoo UniFi Integration"
}
# Add CSRF token for authenticated requests
if csrf_token:
headers["X-CSRF-Token"] = csrf_token
```
### SSL Verification
The UDM Pro uses self-signed certificates. SSL verification must be disabled:
```python
# Disable SSL verification warnings
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
# Skip certificate validation in requests
verify=False
```
## Module Features
### Network Configuration
- Import and manage network settings
- Configure port forwarding rules
- Manage firewall rules and groups
- Handle routing configuration
### Device Management
- Monitor device status and health
- Track active and configured clients
- View detailed device statistics
### System Monitoring
- Dashboard with health metrics
- System performance statistics
- Network usage and throughput
### Multi-Site Support
- Manage multiple UniFi sites
- Site-specific configurations
- Independent metrics per site
## Installation
1. Install the module in your Odoo instance
2. Configure your UDM Pro credentials in the UniFi Integration settings
3. Import your network configuration
## Configuration
### Site Setup
1. Go to UniFi Integration > Sites
2. Create a new site with:
- Name: Your site name
- Site ID: Usually "default" unless configured otherwise
- Description: Optional site description
- Physical Address: Optional location information
### UDM Pro Configuration
1. Add your UDM Pro configuration:
- Host: IP address or hostname
- Port: Usually 443 (HTTPS)
- Username: Your UDM Pro username
- Password: Your UDM Pro password
- MFA Token: If two-factor authentication is enabled
### API Access
1. Create a dedicated API user in UDM Pro
2. Assign minimum required permissions
3. Use secure network access (VPN or internal network)
## Security Considerations
### Authentication
- Use dedicated API credentials
- Enable two-factor authentication when possible
- Regularly rotate passwords
### Network Security
- Restrict API access to specific networks
- Use VPN for remote access
- Configure appropriate firewall rules
### Monitoring
- Monitor API access logs
- Track authentication failures
- Review system alerts and notifications
### Data Protection
- Store credentials securely
- Encrypt sensitive configuration data
- Regular security audits

View file

@ -2,3 +2,4 @@
from . import models
from . import controllers
from . import wizards

View file

@ -27,8 +27,12 @@ This module allows you to:
'views/udm_user_views.xml',
'views/udm_settings_views.xml',
'views/udm_firewall_views.xml',
'views/udm_network_config_views.xml',
'views/udm_menu_views.xml',
'views/templates.xml',
'views/udm_pro_docs_templates.xml',
'wizards/udm_site_import_wizard_views.xml',
'wizards/views/udm_mfa_wizard_views.xml',
],
'demo': [],
'installable': True,
@ -40,7 +44,7 @@ This module allows you to:
},
'assets': {
'web.assets_backend': [
'udm_pro_docs/static/src/css/udm_pro.css',
'unifi_integration/static/src/css/udm_pro.css',
],
},
}

View file

@ -1,41 +1,40 @@
# -*- coding: utf-8 -*-
# pylint: disable=import-error
from odoo import http, _ # IDE peut signaler une erreur, mais fonctionne dans l'environnement Odoo
from odoo.http import request # IDE peut signaler une erreur, mais fonctionne dans l'environnement Odoo
from odoo import http, _ # IDE may report an error, but works in Odoo environment
from odoo.http import request # IDE may report an error, but works in Odoo environment
# pylint: enable=import-error
import logging
import json # Nécessaire pour parser les réponses API et utilisé dans les méthodes de traitement
import json # Required to parse API responses and used in processing methods
import requests
from requests.exceptions import RequestException, ConnectionError
import urllib3
from urllib3.exceptions import InsecureRequestWarning
from datetime import datetime
# Supprimer les avertissements pour les connexions non sécurisées
# Remove warnings for insecure connections
try:
# Pour les versions plus récentes de requests
# For newer versions of requests
import urllib3
urllib3.disable_warnings(category=InsecureRequestWarning)
except ImportError:
# Pour les versions plus anciennes de requests
# For older versions of requests
try:
# Désactiver l'avertissement de sécurité de pylint pour cette ligne spécifique
# pylint: disable=no-member
requests.packages.urllib3.disable_warnings()
# pylint: enable=no-member
# Disable pylint security warning for this specific line
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
except AttributeError:
# Si ni l'un ni l'autre ne fonctionne, nous ignorons silencieusement
# If neither works, we silently ignore
pass
_logger = logging.getLogger(__name__)
class UdmProController(http.Controller):
"""Contrôleur pour les fonctionnalités liées à UDM Pro dans Odoo"""
"""Controller for UDM Pro related functionalities in Odoo"""
@http.route('/udm_pro/advanced_options', type='http', auth='user', website=True)
def advanced_options_form(self):
"""Affiche le formulaire des options avancées pour l'API UDM Pro"""
return request.render('udm_pro_docs.advanced_options_form', {
"""Display the advanced options form for the UDM Pro API"""
return request.render('unifi_integration.advanced_options_form', {
'default_site': 'default',
'fixed_only': True,
'lowercase_hostnames': True,
@ -43,37 +42,40 @@ class UdmProController(http.Controller):
@http.route('/udm_pro/restart_device', type='http', auth='user', website=True)
def restart_device_form(self):
"""Affiche le formulaire pour redémarrer un appareil UDM Pro"""
if not request.env.user.has_group('udm_pro_docs.group_udm_pro_manager'):
return request.render('udm_pro_docs.access_denied', {
"""Display the form to restart a UDM Pro device"""
if not request.env.user.has_group('unifi_integration.group_udm_pro_manager'):
return request.render('unifi_integration.access_denied', {
'error_message': _("You don't have permission to restart devices.")
})
# Récupérer les configurations UDM Pro enregistrées pour le formulaire
# Get saved UDM Pro configurations for the form
configs = request.env['udm.configuration'].sudo().search([])
return request.render('udm_pro_docs.restart_device_form', {
return request.render('unifi_integration.restart_device_form', {
'configs': configs
})
@http.route('/udm_pro/restart_device', type='http', auth='user', website=True, methods=['POST'])
def restart_device(self, **post):
"""Traite la demande de redémarrage d'un appareil UDM Pro"""
if not request.env.user.has_group('udm_pro_docs.group_udm_pro_manager'):
return request.render('udm_pro_docs.access_denied', {
"""Process the request to restart a UDM Pro device"""
if not request.env.user.has_group('unifi_integration.group_udm_pro_manager'):
return request.render('unifi_integration.access_denied', {
'error_message': _("You don't have permission to restart devices.")
})
config_id = int(post.get('config_id'))
config_id_str = post.get('config_id')
if not config_id_str:
raise ValueError(_('Configuration ID is required'))
config_id = int(config_id_str)
mac_address = post.get('mac_address')
if not mac_address:
return request.render('udm_pro_docs.restart_device_form', {
return request.render('unifi_integration.restart_device_form', {
'error_message': _("Please provide the MAC address of the device to restart."),
'configs': request.env['udm.configuration'].sudo().search([])
})
try:
# Récupérer la configuration
# Get the configuration
config = request.env['udm.configuration'].sudo().browse(config_id)
if not config.exists():
raise ValueError(_("Configuration not found"))
@ -86,44 +88,44 @@ class UdmProController(http.Controller):
port=config.port or 443
)
# Authentifier le client
# Authenticate the client
if not client.login():
return request.render('udm_pro_docs.restart_device_form', {
return request.render('unifi_integration.restart_device_form', {
'error_message': _("Authentication failed. Please check the configuration credentials."),
'configs': request.env['udm.configuration'].sudo().search([])
})
# Redémarrer l'appareil
# Restart the device
success = client.restart_device(mac_address)
if not success:
return request.render('udm_pro_docs.restart_device_form', {
return request.render('unifi_integration.restart_device_form', {
'error_message': _("Failed to restart the device. Please check logs for details."),
'configs': request.env['udm.configuration'].sudo().search([])
})
return request.render('udm_pro_docs.restart_success', {
return request.render('unifi_integration.restart_success', {
'mac_address': mac_address
})
except (ConnectionError, RequestException) as e:
_logger.error("Error during UDM Pro device restart: %s", str(e))
return request.render('udm_pro_docs.restart_device_form', {
return request.render('unifi_integration.restart_device_form', {
'error_message': _("Error connecting to UDM Pro: %s") % str(e),
'configs': request.env['udm.configuration'].sudo().search([])
})
except Exception as e: # pylint: disable=broad-except
_logger.exception("Unexpected error during UDM Pro device restart")
return request.render('udm_pro_docs.restart_device_form', {
return request.render('unifi_integration.restart_device_form', {
'error_message': _("An unexpected error occurred: %s") % str(e),
'configs': request.env['udm.configuration'].sudo().search([])
})
@http.route('/udm_pro/generate_hosts', type='http', auth='user', website=True)
def generate_hosts_form(self):
"""Affiche le formulaire pour générer un fichier hosts depuis UDM Pro"""
# Récupérer les configurations UDM Pro enregistrées pour le formulaire
"""Display the form to generate a hosts file from UDM Pro"""
# Get saved UDM Pro configurations for the form
configs = request.env['udm.configuration'].sudo().search([])
return request.render('udm_pro_docs.generate_hosts_form', {
return request.render('unifi_integration.generate_hosts_form', {
'configs': configs,
'fixed_only': True,
'lowercase_hostnames': True
@ -131,18 +133,21 @@ class UdmProController(http.Controller):
@http.route('/udm_pro/generate_hosts', type='http', auth='user', website=True, methods=['POST'])
def generate_hosts(self, **post):
"""Génère un fichier hosts à partir des clients réseau UDM Pro"""
config_id = int(post.get('config_id'))
"""Generate a hosts file from UDM Pro network clients"""
config_id_str = post.get('config_id')
if not config_id_str:
raise ValueError(_('Configuration ID is required'))
config_id = int(config_id_str)
fixed_only = post.get('fixed_only') == 'on'
lowercase_hostnames = post.get('lowercase_hostnames') == 'on'
try:
# Récupérer la configuration
# Get the configuration
config = request.env['udm.configuration'].sudo().browse(config_id)
if not config.exists():
raise ValueError(_("Configuration not found"))
# Initialiser le client UDM Pro avec les options avancées
# Initialize the UDM Pro client with advanced options
client = UdmProClient(
host=config.host,
username=config.username,
@ -152,27 +157,34 @@ class UdmProController(http.Controller):
lowercase_hostnames=lowercase_hostnames
)
# Authentifier le client
# Authenticate the client
if not client.login():
return request.render('udm_pro_docs.generate_hosts_form', {
return request.render('unifi_integration.generate_hosts_form', {
'error_message': _("Authentication failed. Please check the configuration credentials."),
'configs': request.env['udm.configuration'].sudo().search([]),
'fixed_only': fixed_only,
'lowercase_hostnames': lowercase_hostnames
})
# Générer le fichier hosts
# Generate the hosts file
hosts_content = client.generate_hosts_file()
# Retourner le contenu sous forme de fichier à télécharger
response = request.make_response(hosts_content)
response.headers['Content-Type'] = 'text/plain'
response.headers['Content-Disposition'] = 'attachment; filename=udm_hosts.txt'
return response
# Return the content as a downloadable file
# Create and configure the HTTP response
try:
response = request.make_response(hosts_content)
if not response:
raise ValueError(_('Failed to create response'))
response.headers['Content-Type'] = 'text/plain'
response.headers['Content-Disposition'] = 'attachment; filename=udm_hosts.txt'
return response
except (AttributeError, TypeError) as e:
_logger.error('Error creating response: %s', str(e))
raise ValueError(_('Failed to create response'))
except (ConnectionError, RequestException) as e:
_logger.error("Error during UDM Pro hosts file generation: %s", str(e))
return request.render('udm_pro_docs.generate_hosts_form', {
return request.render('unifi_integration.generate_hosts_form', {
'error_message': _("Error connecting to UDM Pro: %s") % str(e),
'configs': request.env['udm.configuration'].sudo().search([]),
'fixed_only': fixed_only,
@ -180,7 +192,7 @@ class UdmProController(http.Controller):
})
except Exception as e: # pylint: disable=broad-except
_logger.exception("Unexpected error during UDM Pro hosts file generation")
return request.render('udm_pro_docs.generate_hosts_form', {
return request.render('unifi_integration.generate_hosts_form', {
'error_message': _("An unexpected error occurred: %s") % str(e),
'configs': request.env['udm.configuration'].sudo().search([]),
'fixed_only': fixed_only,
@ -189,10 +201,10 @@ class UdmProController(http.Controller):
@http.route('/udm_pro/network_clients', type='http', auth='user', website=True)
def network_clients_form(self):
"""Affiche le formulaire pour consulter les clients réseau UDM Pro"""
# Récupérer les configurations UDM Pro enregistrées pour le formulaire
"""Display the form to view UDM Pro network clients"""
# Get saved UDM Pro configurations for the form
configs = request.env['udm.configuration'].sudo().search([])
return request.render('udm_pro_docs.network_clients_form', {
return request.render('unifi_integration.network_clients_form', {
'configs': configs,
'fixed_only': False,
'lowercase_hostnames': True
@ -200,18 +212,21 @@ class UdmProController(http.Controller):
@http.route('/udm_pro/network_clients', type='http', auth='user', website=True, methods=['POST'])
def get_network_clients(self, **post):
"""Récupère et affiche la liste des clients réseau UDM Pro"""
config_id = int(post.get('config_id'))
"""Get and display the list of UDM Pro network clients"""
config_id_str = post.get('config_id')
if not config_id_str:
raise ValueError(_('Configuration ID is required'))
config_id = int(config_id_str)
fixed_only = post.get('fixed_only') == 'on'
lowercase_hostnames = post.get('lowercase_hostnames') == 'on'
try:
# Récupérer la configuration
# Get the configuration
config = request.env['udm.configuration'].sudo().browse(config_id)
if not config.exists():
raise ValueError(_("Configuration not found"))
# Initialiser le client UDM Pro avec les options avancées
# Initialize the UDM Pro client with advanced options
client = UdmProClient(
host=config.host,
username=config.username,
@ -221,26 +236,26 @@ class UdmProController(http.Controller):
lowercase_hostnames=lowercase_hostnames
)
# Authentifier le client
# Authenticate the client
if not client.login():
return request.render('udm_pro_docs.network_clients_form', {
return request.render('unifi_integration.network_clients_form', {
'error_message': _("Authentication failed. Please check the configuration credentials."),
'configs': request.env['udm.configuration'].sudo().search([]),
'fixed_only': fixed_only,
'lowercase_hostnames': lowercase_hostnames
})
# Récupérer les clients réseau
# Get network clients
network_clients = client.get_network_clients()
return request.render('udm_pro_docs.network_clients_result', {
return request.render('unifi_integration.network_clients_result', {
'clients': network_clients,
'config': config
})
except (ConnectionError, RequestException) as e:
_logger.error("Error retrieving UDM Pro network clients: %s", str(e))
return request.render('udm_pro_docs.network_clients_form', {
return request.render('unifi_integration.network_clients_form', {
'error_message': _("Error connecting to UDM Pro: %s") % str(e),
'configs': request.env['udm.configuration'].sudo().search([]),
'fixed_only': fixed_only,
@ -248,7 +263,7 @@ class UdmProController(http.Controller):
})
except ValueError as e:
_logger.error("Value error retrieving UDM Pro network clients: %s", str(e))
return request.render('udm_pro_docs.network_clients_form', {
return request.render('unifi_integration.network_clients_form', {
'error_message': _("Configuration error: %s") % str(e),
'configs': request.env['udm.configuration'].sudo().search([]),
'fixed_only': fixed_only,
@ -256,16 +271,16 @@ class UdmProController(http.Controller):
})
except (AttributeError, KeyError) as e:
_logger.error("Data format error retrieving UDM Pro network clients: %s", str(e))
return request.render('udm_pro_docs.network_clients_form', {
return request.render('unifi_integration.network_clients_form', {
'error_message': _("Data error: %s") % str(e),
'configs': request.env['udm.configuration'].sudo().search([]),
'fixed_only': fixed_only,
'lowercase_hostnames': lowercase_hostnames
})
except Exception as e: # pylint: disable=broad-except
# Conserver cette exception générique comme dernier recours, avec un avertissement explicite pour pylint
# Keep this generic exception as a last resort, with an explicit warning for pylint
_logger.exception("Unexpected error retrieving UDM Pro network clients")
return request.render('udm_pro_docs.network_clients_form', {
return request.render('unifi_integration.network_clients_form', {
'error_message': _("An unexpected error occurred: %s") % str(e),
'configs': request.env['udm.configuration'].sudo().search([]),
'fixed_only': fixed_only,
@ -274,24 +289,25 @@ class UdmProController(http.Controller):
@http.route('/udm_pro/import_config', type='http', auth='user', website=True)
def import_config_form(self):
"""Affiche le formulaire d'importation de configuration UDM Pro"""
return request.render('udm_pro_docs.import_config_form', {})
"""Display the UDM Pro configuration import form"""
return request.render('unifi_integration.import_config_form', {})
@http.route('/udm_pro/import_config', type='http', auth='user', website=True, methods=['POST'])
def import_config(self, **post):
"""Importe une configuration UDM Pro depuis l'appareil"""
if not request.env.user.has_group('udm_pro_docs.group_udm_pro_manager'):
return request.render('udm_pro_docs.access_denied', {
"""Import UDM Pro configuration from the device"""
if not request.env.user.has_group('unifi_integration.group_udm_pro_manager'):
return request.render('unifi_integration.access_denied', {
'error_message': _("You don't have permission to import configurations.")
})
host = post.get('host')
username = post.get('username')
password = post.get('password')
port = int(post.get('port') or 443)
port_str = post.get('port')
port = int(port_str) if port_str else 443
if not all([host, username, password]):
return request.render('udm_pro_docs.import_config_form', {
return request.render('unifi_integration.import_config_form', {
'error_message': _("Please provide all required fields."),
'host': host,
'username': username,
@ -299,10 +315,10 @@ class UdmProController(http.Controller):
})
try:
# Utiliser le client API pour récupérer la configuration
# Use the API client to get the configuration
client = UdmProClient(host, username, password, port)
if not client.login():
return request.render('udm_pro_docs.import_config_form', {
return request.render('unifi_integration.import_config_form', {
'error_message': _("Authentication failed. Please check your credentials."),
'host': host,
'username': username,
@ -311,14 +327,14 @@ class UdmProController(http.Controller):
config_data = client.get_full_configuration()
# Importer la configuration dans Odoo
# Import the configuration into Odoo
config_id = request.env['udm.configuration'].sudo().import_configuration(config_data)
return request.redirect('/web#id=%s&model=udm.configuration&view_type=form' % config_id)
except (ConnectionError, RequestException) as e:
_logger.error("Error during UDM Pro configuration import: %s", str(e))
return request.render('udm_pro_docs.import_config_form', {
return request.render('unifi_integration.import_config_form', {
'error_message': _("Error connecting to UDM Pro: %s") % str(e),
'host': host,
'username': username,
@ -326,7 +342,7 @@ class UdmProController(http.Controller):
})
except (ValueError, TypeError, AttributeError) as e:
_logger.error("Data processing error during UDM Pro configuration import: %s", str(e))
return request.render('udm_pro_docs.import_config_form', {
return request.render('unifi_integration.import_config_form', {
'error_message': _("Error processing data: %s") % str(e),
'host': host,
'username': username,
@ -334,7 +350,7 @@ class UdmProController(http.Controller):
})
except Exception as e: # pylint: disable=broad-except
_logger.exception("Unexpected error during UDM Pro configuration import")
return request.render('udm_pro_docs.import_config_form', {
return request.render('unifi_integration.import_config_form', {
'error_message': _("An unexpected error occurred. Please check server logs."),
'host': host,
'username': username,
@ -343,9 +359,9 @@ class UdmProController(http.Controller):
class UdmProClient:
"""Client pour interagir avec l'API UDM Pro."""
"""Client to interact with the UDM Pro API."""
# Points d'accès de l'API
# API endpoints
API_LOGIN_ENDPOINT = '/api/auth/login'
API_SYSTEM_INFO_ENDPOINT = '/api/system'
API_NETWORK_ENDPOINT = '/api/networks'
@ -354,25 +370,25 @@ class UdmProClient:
API_SETTINGS_ENDPOINT = '/api/settings'
API_FIREWALL_ENDPOINT = '/api/firewall'
# Nouveaux endpoints inspirés du client Go
# New endpoints inspired by the Go client
API_ACTIVE_CLIENTS_ENDPOINT = '/proxy/network/api/s/{site}/stat/sta'
API_CONFIGURED_CLIENTS_ENDPOINT = '/proxy/network/api/s/{site}/list/user'
API_DEVICE_RESTART_ENDPOINT = '/proxy/network/api/s/{site}/cmd/devmgr'
def __init__(self, host, username, password, port=443, verify_ssl=False, site='default', fixed_only=True, lowercase_hostnames=True, debug=False):
"""
Initialise le client API UDM Pro.
Initialize the UDM Pro API client.
Args:
host (str): Adresse IP ou nom d'hôte de l'UDM Pro
username (str): Nom d'utilisateur pour l'API
password (str): Mot de passe pour l'API
port (int): Port pour la connexion (par défaut 443)
verify_ssl (bool): Vérifier le certificat SSL (par défaut False)
site (str): Identifiant du site pour l'API UniFi (par défaut 'default')
fixed_only (bool): Ne considérer que les clients avec adresse IP fixe (par défaut True)
lowercase_hostnames (bool): Convertir les noms d'hôtes en minuscules (par défaut True)
debug (bool): Activer le mode débogage pour les requêtes HTTP (par défaut False)
host (str): IP address or hostname of the UDM Pro
username (str): Username for the API
password (str): Password for the API
port (int): Port for connection (default 443)
verify_ssl (bool): Verify SSL certificate (default False)
site (str): Site identifier for UniFi API (default 'default')
fixed_only (bool): Only consider clients with fixed IP address (default True)
lowercase_hostnames (bool): Convert hostnames to lowercase (default True)
debug (bool): Enable debug mode for HTTP requests (default False)
"""
self.host = host
self.username = username
@ -390,16 +406,16 @@ class UdmProClient:
self.session.verify = verify_ssl
def _get_auth_headers(self):
"""Retourne les en-têtes d'authentification."""
"""Return authentication headers."""
if not self.token:
raise ValueError("Non authentifié. Appelez login() d'abord.")
raise ValueError("Not authenticated. Call login() first.")
headers = {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
# Ajouter le token CSRF si disponible
# Add CSRF token if available
if self.csrf_token:
headers["X-Csrf-Token"] = self.csrf_token
@ -407,10 +423,10 @@ class UdmProClient:
def login(self):
"""
Authentifie le client auprès de l'API UDM Pro.
Authenticates the client with the UDM Pro API.
Returns:
bool: True si l'authentification a réussi, False sinon
bool: True if authentication succeeded, False otherwise
"""
try:
login_url = f"{self.base_url}{self.API_LOGIN_ENDPOINT}"
@ -419,139 +435,145 @@ class UdmProClient:
"password": self.password
}
_logger.debug("Tentative de connexion à %s", login_url)
_logger.debug("Attempting to connect to %s", login_url)
response = self.session.post(login_url, json=payload)
response.raise_for_status()
# Capture du token CSRF s'il existe
# Capture CSRF token if it exists
csrf_token = response.headers.get('X-Csrf-Token')
if csrf_token:
self.csrf_token = csrf_token
_logger.debug("Token CSRF capturé: %s", csrf_token)
_logger.debug("CSRF token captured: %s", csrf_token)
data = response.json()
if not data.get('token'):
_logger.error("Aucun token n'a été retourné par l'API")
_logger.error("No token was returned by the API")
return False
self.token = data['token']
_logger.debug("Authentification réussie")
_logger.debug("Authentication successful")
return True
except RequestException as e:
_logger.error("Erreur d'authentification: %s", str(e))
_logger.error("Authentication error: %s", str(e))
return False
def _make_api_request(self, method, endpoint, params=None, data=None, retry=True):
"""
Effectue une requête API.
Makes an API request.
Args:
method (str): Méthode HTTP (GET, POST, etc.)
endpoint (str): Point d'accès API
params (dict): Paramètres de requête
data (dict): Données à envoyer dans le corps de la requête
retry (bool): Réessayer en cas d'erreur d'authentification
method (str): HTTP method (GET, POST, etc.)
endpoint (str): API endpoint
params (dict): Request parameters
data (dict): Data to send in request body
retry (bool): Retry on authentication error
Returns:
dict: Réponse JSON de l'API
dict: JSON response from API
"""
if not self.token and retry:
_logger.debug("Non authentifié, tentative d'authentification")
_logger.debug("Not authenticated, attempting authentication")
if not self.login():
raise ConnectionError("Impossible de s'authentifier à l'API UDM Pro")
raise ConnectionError("Unable to authenticate with UDM Pro API")
url = f"{self.base_url}{endpoint}"
try:
_logger.debug("Requête %s vers %s", method, url)
_logger.debug("Request %s to %s", method, url)
headers = self._get_auth_headers()
response = None
response = self.session.request(
method=method,
url=url,
headers=headers,
params=params,
json=data
)
# Capture du token CSRF s'il existe dans la réponse
csrf_token = response.headers.get('X-Csrf-Token')
if csrf_token and csrf_token != self.csrf_token:
self.csrf_token = csrf_token
_logger.debug("Token CSRF mis à jour: %s", csrf_token)
response.raise_for_status()
return response.json()
except RequestException as e:
if response.status_code == 401 or response.status_code == 403:
if retry:
_logger.debug("Token expiré, nouvelle tentative d'authentification")
self.token = None
return self._make_api_request(method, endpoint, params, data, retry=False)
_logger.error("Erreur API (%s %s): %s", method, url, str(e))
try:
response = self.session.request(
method=method,
url=url,
headers=headers,
params=params,
json=data
)
# Capture CSRF token if it exists in the response
csrf_token = response.headers.get('X-Csrf-Token')
if csrf_token and csrf_token != self.csrf_token:
self.csrf_token = csrf_token
_logger.debug("CSRF token updated: %s", csrf_token)
response.raise_for_status()
return response.json()
except RequestException as request_error:
if response and (response.status_code == 401 or response.status_code == 403):
if retry:
_logger.debug("Token expired, attempting re-authentication")
self.token = None
return self._make_api_request(method, endpoint, params, data, retry=False)
_logger.error("API error (%s %s): %s", method, url, str(request_error))
raise
except Exception as e:
_logger.error("Unexpected error in API request: %s", str(e))
raise
def get_system_info(self):
"""
Récupère les informations système de l'UDM Pro.
Gets system information from UDM Pro.
Returns:
dict: Informations système
dict: System information
"""
return self._make_api_request('GET', self.API_SYSTEM_INFO_ENDPOINT)
def get_networks(self):
"""
Récupère la configuration des réseaux.
Gets network configuration.
Returns:
dict: Configuration des réseaux
dict: Network configuration
"""
return self._make_api_request('GET', self.API_NETWORK_ENDPOINT)
def get_devices(self):
"""
Récupère la liste des périphériques.
Gets the list of devices.
Returns:
dict: Liste des périphériques
dict: List of devices
"""
return self._make_api_request('GET', self.API_DEVICES_ENDPOINT)
def get_users(self):
"""
Récupère la liste des utilisateurs.
Gets the list of users.
Returns:
dict: Liste des utilisateurs
dict: List of users
"""
return self._make_api_request('GET', self.API_USERS_ENDPOINT)
def get_settings(self):
"""
Récupère les paramètres généraux.
Gets general settings.
Returns:
dict: Paramètres généraux
dict: General settings
"""
return self._make_api_request('GET', self.API_SETTINGS_ENDPOINT)
def get_firewall_rules(self):
"""
Récupère les règles de pare-feu.
Gets firewall rules.
Returns:
dict: Règles de pare-feu
dict: Firewall rules
"""
return self._make_api_request('GET', self.API_FIREWALL_ENDPOINT)
def get_active_clients(self):
"""
Récupère la liste des clients actuellement connectés.
Gets the list of currently connected clients.
Returns:
list: Liste des clients actifs
list: List of active clients
"""
endpoint = self.API_ACTIVE_CLIENTS_ENDPOINT.format(site=self.site)
response = self._make_api_request('GET', endpoint)
@ -563,10 +585,10 @@ class UdmProClient:
def get_configured_clients(self):
"""
Récupère la liste des clients configurés statiquement.
Gets the list of statically configured clients.
Returns:
list: Liste des clients configurés
list: List of configured clients
"""
endpoint = self.API_CONFIGURED_CLIENTS_ENDPOINT.format(site=self.site)
response = self._make_api_request('GET', endpoint)
@ -578,14 +600,14 @@ class UdmProClient:
def restart_device(self, mac_address):
"""
Redémarre un appareil géré par l'UDM Pro (ex: point d'accès WiFi).
Nécessite des permissions de niveau 'admin du site'.
Restarts a device managed by UDM Pro (e.g. WiFi access point).
Requires 'site admin' level permissions.
Args:
mac_address (str): Adresse MAC de l'appareil à redémarrer
mac_address (str): MAC address of device to restart
Returns:
bool: True si l'opération a réussi, False sinon
bool: True if operation succeeded, False otherwise
"""
endpoint = self.API_DEVICE_RESTART_ENDPOINT.format(site=self.site)
payload = {
@ -600,28 +622,28 @@ class UdmProClient:
return True
return False
except Exception as e: # pylint: disable=broad-except
_logger.error("Erreur lors du redémarrage de l'appareil %s: %s", mac_address, str(e))
_logger.error("Error while restarting device %s: %s", mac_address, str(e))
return False
def get_network_clients(self):
"""
Récupère tous les clients réseau (actifs et/ou configurés selon les paramètres).
Gets all network clients (active and/or configured according to parameters).
Returns:
list: Liste des clients réseau
list: List of network clients
"""
clients = []
# Toujours inclure les clients configurés statiquement
# Always include statically configured clients
configured_clients = self.get_configured_clients()
clients.extend(configured_clients)
# Inclure les clients actifs si fixed_only est False
# Include active clients if fixed_only is False
if not self.fixed_only:
active_clients = self.get_active_clients()
clients.extend(active_clients)
# Traitement des noms d'hôtes si nécessaire
# Process hostnames if needed
if self.lowercase_hostnames:
for client in clients:
if client.get('hostname'):
@ -633,10 +655,10 @@ class UdmProClient:
def generate_hosts_file(self):
"""
Génère un fichier hosts à partir des clients réseau.
Generates a hosts file from network clients.
Returns:
str: Contenu du fichier hosts
str: Content of hosts file
"""
clients = self.get_network_clients()
hosts_content = "# UDM Pro Generated Hosts File\n"
@ -654,17 +676,17 @@ class UdmProClient:
def get_full_configuration(self):
"""
Récupère la configuration complète de l'UDM Pro.
Gets complete UDM Pro configuration.
Returns:
dict: Configuration complète de l'UDM Pro
dict: Complete UDM Pro configuration
"""
# S'authentifier d'abord
# Authenticate first
if not self.token and not self.login():
raise ConnectionError("Échec de l'authentification pour la récupération de la configuration complète")
raise ConnectionError("Authentication failed while retrieving complete configuration")
try:
# Récupérer chaque partie de la configuration
# Get each part of the configuration
system_info = self.get_system_info()
networks = self.get_networks()
devices = self.get_devices()
@ -672,10 +694,10 @@ class UdmProClient:
settings = self.get_settings()
firewall = self.get_firewall_rules()
# Récupérer également les clients réseau (nouvelle fonctionnalité)
# Also get network clients (new feature)
network_clients = self.get_network_clients()
# Combiner toutes les parties dans un seul dictionnaire
# Combine all parts into a single dictionary
return {
'system_info': system_info,
'networks': networks,
@ -687,5 +709,5 @@ class UdmProClient:
}
except RequestException as e:
_logger.error("Erreur lors de la récupération de la configuration complète: %s", str(e))
_logger.error("Error while retrieving complete configuration: %s", str(e))
raise

View file

@ -0,0 +1,406 @@
Documentation of API endpoints on the UniFi controller software. This is a reverse engineering project that is based on browser captures, jar dumps, and reviewing other software that has been written to work with the controller. It's received minimal testing.
There are two main types of calls. One would be the REST-like which provide get/post/put/delete where post is to the base and put/delete are often tied to the _id of the object that you are working with. The second major type of web interface provided is an agent/call based system where you pass a command to an agent to perform an action. Both use application/json formatting for all data transfer. When updating a specific object you must use PUT or else a new object will be created.
NOTE: All calls are relative to the base controller URL
Calls return both a web status as well as JSON formatted output. 200 codes indicate a successful call and other indicate errors. I am using the placeholder `{site}` for the site name which for many installations will be `default`.
# Login required
{ "data" : [ ] , "meta" : { "msg" : "api.err.LoginRequired" , "rc" : "error"}}
# Call was a success but returned no values
{ "data" : [ ] , "meta" : { "rc" : "ok"}}
# NOTE: If meta contains a count it's because the data values have been truncated
'meta': {'count': 4818, 'rc': 'ok'} # from the api/s/{site}/stat/event endpoint
UDM Pro and UCG Max API
NOTE: There are two critical differences between Unifi controllers and the API of the UDM Pro and UCG Max:
The login endpoint is /api/auth/login
All API endpoints need to be prefixed with /proxy/network (e.g. https://192.168.0.1/proxy/network/api/s/default/self)
Examples
curl
# authenticate and save the cookie contents in local file cookie.txt with switch '-c'
curl -k -X POST --data '{"username": "usr", "password": "$pw"}' --header 'Content-Type: application/json' -c cookie.txt https://udmp:443/api/auth/login
# responds with json data
# pass the local file cookie.txt with switch '-b'
curl -k -X GET -b cookie.txt https://udmp/proxy/network/api/s/default/self
# responds with proper json
python
Controller Endpoints
These are REST calls that can be made without a site context. I do not believe any updates ( PUT ) can be called on these endpoints.
Path Method Notes
status GET Returns some very basic server information - This appears to be the only endpoint that can be reached without an authentication
{ "data" : [ ] , "meta" : { "rc" : "ok" , "server_version" : "5.7.23" , "up" : true , "uuid" : "0e727580-ffff-ffff-ffff-403dcd5a7bd4"}}
api/login POST requires dict of username, password, and optionally remember=true for long-running sessions. Returns 200 for success and a cookie that is your session. NOTE: On UDM Pros this is api/auth/login.
api/logout POST destroys the sever side session id which will make future attempts with that cookie fail
api/self GET Logged in user NOTE: On UDM Pros this is api/users/self.
api/self/sites GET Get basic information for all sites on this controller
api/stat/sites GET Same as above with an additional information on health and new alerts for each site
api/stat/admin GET List administrators and permissions for all sites
api/system/poweroff POST Turns off the UDM NOTE: X-CSRF-Token header required (from e.g. the login response) + Super Admin access rights
api/system/reboot POST Reboot the UDM NOTE: X-CSRF-Token header required (from e.g. the login response) + Super Admin access rights
Site Endpoints
All commands are presumed to be prefixed with api/s/{site}
Path Method Notes
stat/health GET Health status of the site
self GET Logged in user
stat/ccode GET List of country codes
stat/current-channel GET List of all RF channels based on the site country code
stat/sysinfo GET Some high-level information about the controller
stat/event GET List site events by most recent first, 3000 result limit
rest/event GET List site events by oldest, no limit? (api.err.NotFound per controller version 7.1.66)
stat/alarm GET List alarms by most recent, 3000 result limit?
rest/alarm GET List alarms by oldest, no limit? (api.err.NotFound per controller version 7.1.66)
stat/sta GET List of all _active_ clients on the site
rest/user GET/POST/PUT List of all configured/known clients on the site
stat/device-basic GET List of site devices with only 'adopted', 'disabled', 'mac', 'state', 'type' keys, useful for filtering on type
stat/device GET/POST Detailed list of all devices on site. (Controller only) Can be filtered by POSTing {"macs": ["mac1", ... ]}
stat/device/{mac} GET (UDM only) Detailed list of device filtered by mac address
rest/device/{_id} PUT Updates to devices get PUT here, why?
rest/setting GET/PUT Detailed site settings, updating requires adding key and _id to path for PUT ../setting/{key}/{_id}
stat/routing GET All active routes on the device
rest/routing GET/PUT User defined routes (HTTP response 500 per controller version 7.1.66)
rest/firewallrule GET/PUT User defined firewall rules. This does not show auto-generated rules
rest/firewallgroup GET/PUT User defined firewall groups.
rest/wlanconf GET/PUT List WLANs, edit current WLANs and create new WLANs
rest/wlanconf/{_id} GET Get details of WLAN designated by '_id'
rest/wlanconf/{_id} PUT Update configuration of current WLAN designated by '_id'
rest/tag GET/PUT? Tagged macs (api.err.Invalid per controller version 7.1.66)
stat/rogueap GET/POST Neighboring APs optional json post 'within' = seen in the last x hours
stat/sitedpi GET/POST DPI stats requires type="by_app" or "by_cat"
stat/stadpi GET/POST DPI stats requires type="by_app" or "by_cat" optionally filtered macs=[…, ]
stat/dynamicdns GET DynamicDNS information and status like current ip, last changed, and status
rest/dynamicdns GET/PUT DynamicDNS configuration
rest/portconf GET Switch port profiles
stat/spectrumscan GET Get RF scan results, can be for a specific mac by appending to endpoint
rest/radiusprofile GET/POST/PUT Radius profiles
rest/account GET/POST/PUT Radius accounts
rest/portforward GET List all port forwards configured on the site
stat/report/{interval}.{type} POST Intervals are '5minutes', 'hourly', and 'daily'. Report types are 'site', 'user', and 'ap'. Must specify attributes to be returned 'bytes', 'wan-tx_bytes', 'wan-rx_bytes', 'wlan_bytes', 'num_sta', 'lan-num_sta', 'wlan-num_sta', 'time', 'rx_bytes', 'tx_bytes'. Can be filtered with 'macs': […]
stat/authorization POST JSON as "{"start": "START TIMESTAMP", "end": "END TIMESTAMP"}" and you will get the code that have been used between the Timestams NOTE: X-CSRF-Token header required (from e.g. the login response) for UDM
stat/sdn GET Return values as if the site is connected with the Unifi Cloud or the SSO
Callable commands
Posting to the endpoint api/s/{site}/cmd/<manager> with the json {"cmd": "command"} you can invoke commands on the controller.
Manager Call Notes
evtmgt archive-all-alarms
sitemgr add-site desc = Descriptive name ( required ), name = shortname ( in the URL )
sitemgr delete-site name = short name ( required )
sitemgr update-site desc = Descriptive name ( required )
sitemgr get-admins List all administrators and permission for this site
sitemgr move-device mac = device mac ( required ), site_id = 24 digit id ( required )
sitemgr delete-device mac = device mac ( required )
stamgr block-sta mac = client mac ( required )
stamgr unblock-sta mac = client mac ( required )
stamgr kick-sta Disconnect: mac = client mac (required )
stamgr forget-sta Forget a client ( controller 5.9.x only )
stamgr unauthorize-guest Unauthorize a client device, mac = client mac (required)
devmgr adopt mac = device mac ( required )
devmgr restart mac = device mac ( required )
devmgr force-provision mac = device mac ( required )
devmgr power-cycle mac = switch mac ( required ), port_idx = PoE port to cycle ( required )
devmgr speedtest Start a speed test
devmgr speedtest-status get the current state of the speed test
devmgr set-locate mac = device mac ( required ) blink unit to locate
devmgr unset-locate mac = device mac ( required ) led to normal state
devmgr upgrade mac = device mac ( required ) upgrade firmware
devmgr upgrade-external mac = device mac ( required ), url = firmware URL ( required )
devmgr migrate mac = device mac ( required ), inform_url = New Inform URL to push to device (required)
devmgr cancel-migrate mac = device mac ( required )
devmgr spectrum-scan mac = device mac ( ap only, required ) trigger RF scan
backup list-backups list of autobackup files
backup delete-backup filename ( required )
system backup create a backup. This appears to backup to a fixed location in the filesystem
stat clear-dpi resets the site wide DPI counters
Data Tables
This data was extracted from the javascript of the site.
Model Type SKU Name
BZ2 uap UAP Access Point
BZ2LR uap UAP-LR Access Point Long-Range
S216150 usw US-16-150W Switch 16 PoE (150 W)
S224250 usw US-24-250W Switch 24 PoE (250 W)
S224500 usw US-24-500W Switch 24 PoE (500 W)
S248500 usw US-48-500W Switch 48 PoE (500 W)
S248750 usw US-48-750W Switch 48 PoE (750 W)
S28150 usw US-8-150W Switch 8 PoE (150 W)
U2HSR uap UAP-Outdoor+ Access Point Outdoor+
U2IW uap UAP-IW Access Point In-Wall
U2L48 uap UAP-LR Access Point Long-Range
U2Lv2 uap UAP-LRv2 Access Point Long-Range
U2M uap UAP-Mini Access Point Mini
U2O uap UAP-Outdoor Access Point Outdoor
U2S48 uap UAP Access Point
U2Sv2 uap UAPv2 Access Point
U5O uap UAP-Outdoor5 Access Point Outdoor 5
U6ENT uap U6-Enterprise Access Point WiFi 6 Enterprise
U6EXT uap U6-Extender Access Point WiFi 6 Extender
U6IW uap U6-IW Access Point WiFi 6 In-Wall
U6M uap U6-Mesh Access Point WiFi 6 Mesh
U7E uap UAP-AC Access Point AC
U7EDU uap UAP-AC-EDU Access Point AC EDU
U7Ev2 uap UAP-AC Access Point AC
U7HD uap UAP-AC-HD Access Point AC HD
U7IW uap UAP-AC-IW Access Point AC In-Wall
U7IWP uap UAP-AC-IW-Pro Access Point AC In-Wall Pro
U7LR uap UAP-AC-LR Access Point AC Long-Range
U7LT uap UAP-AC-Lite Access Point AC Lite
U7MP uap UAP-AC-M-Pro Access Point AC Mesh Pro
U7MSH uap UAP-AC-M Access Point AC Mesh
U7NHD uap UAP-nanoHD Access Point nanoHD
U7O uap UAP-AC-Outdoor Access Point AC Outdoor
U7P uap UAP-AC-Pro Access Point AC Pro
U7PG2 uap UAP-AC-Pro Access Point AC Pro
U7SHD uap UAP-AC-SHD Access Point AC SHD
UAE6 uap U6-Extender-EA Access Point WiFi 6 Extender
UAIW6 uap U6-IW-EA Access Point WiFi 6 In-Wall
UAL6 uap U6-Lite Access Point WiFi 6 Lite
UALR6 uap U6-LR-EA Access Point WiFi 6 Long-Range
UALR6v2 uap U6-LR Access Point WiFi 6 Long-Range
UALR6v3 uap U6-LR Access Point WiFi 6 Long-Range
UAM6 uap U6-Mesh-EA Access Point WiFi 6 Mesh
UAP6 uap U6-LR Access Point WiFi 6 Long-Range
UAP6MP uap U6-Pro Access Point WiFi 6 Pro
UASXG uas UAS-XG Application Server XG
UBB ubb UBB Building-to-Building Bridge
UBBXG ubb UBB-XG Building-to-Building Bridge XG
UCGMAX ucg UCG-MAX Cloud Gateway Max
UCK uck UCK Cloud Key
UCK-v2 uck UCK Cloud Key
UCK-v3 uck UCK Cloud Key
UCKG2 uck UCK-G2 Cloud Key Gen2
UCKP uck UCK-G2-Plus Cloud Key Gen2 Plus
UCMSH uap UAP-XG-Mesh Access Point Mesh XG
UCXG uap UAP-XG Access Point XG
UDC48X6 usw USW-Leaf Switch Leaf
UDM udm UDM Dream Machine
UDMB uap UAP-BeaconHD Access Point BeaconHD
UDMPRO udm UDM-Pro Dream Machine Pro
UDMPROSE udm UDM-SE Dream Machine Special Edition
UDR udm UDR Dream Router
UDW udm UDW Dream Wall
UDWPRO udm UDWPRO Dream Wall Pro
UFLHD uap UAP-FlexHD Access Point FlexHD
UGW3 ugw USG-3P Security Gateway
UGW4 ugw USG-Pro-4 Security Gateway Pro
UGWHD4 ugw USG Security Gateway
UGWXG ugw USG-XG-8 Security Gateway XG
UHDIW uap UAP-IW-HD Access Point In-Wall HD
ULTE uap U-LTE UniFi LTE
ULTEPEU uap U-LTE-Pro UniFi LTE Pro
ULTEPUS uap U-LTE-Pro UniFi LTE Pro
UP1 uap USP-Plug SmartPower Plug
UP4 uph UVP-X Phone
UP5 uph UVP Phone
UP5c uph UVP Phone
UP5t uph UVP-Pro Phone Professional
UP5tc uph UVP-Pro Phone Professional
UP6 uap USP-Strip SmartPower Strip (6 ports)
UP7 uph UVP-Executive Phone Executive
UP7c uph UVP-Executive Phone Executive
US16P150 usw US-16-150W Switch 16 PoE (150 W)
US24 usw USW-24-G1 Switch 24
US24P250 usw US-24-250W Switch 24 PoE (250 W)
US24P500 usw US-24-500W Switch 24 PoE (500 W)
US24PL2 usw US-L2-24-PoE Switch 24 PoE
US24PRO usw USW-Pro-24-PoE Switch Pro 24 PoE
US24PRO2 usw USW-Pro-24 Switch Pro 24
US48 usw US-48-G1 Switch 48
US48P500 usw US-48-500W Switch 48 PoE (500 W)
US48P750 usw US-48-750W Switch 48 PoE (750 W)
US48PL2 usw US-L2-48-PoE Switch 48 PoE
US48PRO usw USW-Pro-48-PoE Switch Pro 48 PoE
US48PRO2 usw USW-Pro-48 Switch Pro 48
US624P usw USW-Enterprise-24-PoE Switch Enterprise 24 PoE
US648P usw USW-Enterprise-48-PoE Switch Enterprise 48 PoE
US68P usw USW-Enterprise-8-PoE Switch Enterprise 8 PoE
US6XG150 usw US-XG-6PoE Switch 6 XG PoE
US8 usw US-8 Switch 8
US8P150 usw US-8-150W Switch 8 PoE (150 W)
US8P60 usw US-8-60W Switch 8 (60 W)
USAGGPRO usw USW-Pro-Aggregation Switch Aggregation Pro
USC8 usw US-8 Switch 8
USC8P150 usw US-8-150W Switch 8 PoE (150 W)
USC8P450 usw USW-Industrial Switch Industrial
USC8P60 usw US-8-60W Switch 8 (60 W)
USF5P usw USW-Flex Switch Flex
USFXG usw USW-Flex-XG Switch Flex XG
USL16LP usw USW-Lite-16-PoE Switch Lite 16 PoE
USL16P usw USW-16-PoE Switch 16 PoE
USL24 usw USW-24-G2 Switch 24
USL24P usw USW-24-PoE Switch 24 PoE
USL48 usw USW-48-G2 Switch 48
USL48P usw USW-48-PoE Switch 48 PoE
USL8A usw USW-Aggregation Switch Aggregation
USL8LP usw USW-Lite-8-PoE Switch Lite 8 PoE
USL8MP usw USW-Mission-Critical Switch Mission Critical
USMINI usw USW-Flex-Mini Switch Flex Mini
USPPDUP usw USP-PDU-Pro SmartPower PDU Pro
USPRPS usw USP-RPS SmartPower Redundant Power System
USXG usw US-16-XG Switch XG 16
USXG24 usw USW-EnterpriseXG-24 Switch Enterprise XG 24
UXBSDM uap UWB-XG-BK WiFi BaseStation XG
UXGPRO uxg UXG-Pro Next-Generation Gateway Pro
UXSDM uap UWB-XG WiFi BaseStation XG
p2N uap PICOM2HP PicoStation M2 HP
DPI Category Code Name
0 Instant messaging
1 P2P
3 File Transfer
4 Streaming Media
5 Mail and Collaboration
6 Voice over IP
7 Database
8 Games
9 Network Management
10 Remote Access Terminals
11 Bypass Proxies and Tunnels
12 Stock Market
13 Web
14 Security Update
15 Web IM
17 Business
18 Network Protocols
19 Network Protocols
20 Network Protocols
23 Private Protocol
24 Social Network
255 Unknown
There are 2,213 named applications in the javascript dynamic.dpi.js. See extracted cat_app.json to include for lookups and example usage.
The application id is a compound id using bitwise shift left on the category id + application id sent from the api using list_dpi_stats_filtered
function compoundId($cat, $app){
return (intval($cat) << 16) + intval($app);
}
Uncategorized
Dump of found endpoints waiting for documentation
# logged in user
api/s/{site}/self
# Country codes
api/s/{site}/stat/ccode
# Availible WiFi channels
api/s/{site}/stat/current-channel
# Dashboard health
api/s/{site}/stat/health
# Active client devices
api/s/{site}/stat/sta
# Configured clients
api/s/{site}/stat/user
# Devices
api/s/{site}/stat/device-basic - mac, type
api/s/{site}/stat/device - can be filtered with macs: [ ..., ... ]
# Detailed site settings
api/s/{site}/stat/sysinfo
# /rest/ endpoints also have a /cnt/ which returns the count for the data portion
# can be used for any but seems targeted towards alarms
# Site settings
api/s/{site}/rest/setting - this is a big one with a weird mechanism for updating
# Firewall rules
api/s/{site}/rest/firewallrule - only lists user-defined rules
# Firewall groups
api/s/{site}/rest/firewallgroup
# routes
api/s/{site}/rest/routing
# Alarms
# List of alarms
api/s/{site}/rest/alarm
# list of unarchived alarms
api/s/{site}/rest/alarm?archived=false
# User groups - bandwith settings
api/s/{site}/rest/usergroup
# ?
api/s/{site}/rest/wlangroup
# Wireless networks
api/s/{site}/rest/wlanconf
# ?
api/s/{site}/rest/tag
# Site networks
api/s/{site}/rest/networkconf
# example backup path
dl/autobackup/autobackup_5.7.23_20180513_0000_1526169600008.unf
# Insights - sessions
api/s/{site}/stat/session?type=all&start=1526515200&end=1526688000
# Insights - EDU streams
api/s/{site}/stat/stream
# Switch port conf?
api/s/{site}/rest/portconf
# Configured port forwards and uPNP - transfer bytes is listed but doesn't appear populated
api/s/{site}/stat/portforward
# Update User (User are the clients)
api/s/{site}/upd/user/{UserId}
you can get the users and the userid from "/api/s/{SiteId}/stat/alluser" (All Clients) or "/api/s/{SiteId}/stat/sta" (Active Clients) which contains the client id (_id).
example: change name of user with clientid 5aca464bb79fc60200460394 to 'test-raw':
${curl_cmd} --data "json={'name':'test-raw'}" $baseurl/api/s/$site/upd/user/5aca464bb79fc60200460394
# Get Hotspot Configuration
guest/s/{site}/hotspotconfig
You will get in "auth" the value "none" if it is not activated, if it is activated you will get for exemple "hotspot" and many other values on the design of the page.
# Get Hotspot Packages
guest/s/{site}/hotspotpackages
??
# Get trafficrules
v2/api/site/{site}/trafficrules
also possible to add new rule with a POST request.
# Edit trafficrules
v2/api/site/{site}/trafficrules/{id}/
PUT or DELETE request to update or delete traffic rule
GET is not allowed on specific trafficrules.
With PUT the result code is 201 and not 200 for successful change.
# Possible list of all callable managers
system
devmgr
stamgr
evtmgr
cfgmgr
hotspot
sitemgr
streammgr
backup
throughput
stat
firmware
firewall
elite
Update of Port Forward Rules
This may apply to other configurations, but initial testing shows that port forward rules can be enabled/disabled using PUT against the endpoint /api/s/{site}/rest/portforward/{rule-id} with a body such as:
{
"enabled": true
}
The rule ID can be retrieved using the above described port forwarding GET request and is found in the "_id" key.
New rules can be created using POST, but be aware that there seems to be very little validation (it's possible to create entries with no information other than the fact that they're enabled, for example).

View file

@ -0,0 +1,5 @@
UDMP_HOST=https://10.10.1.10
UDMP_USERNAME=api
UDMP_PASSWORD='o=7Jd&WDZ.T,u6t?'
UDMP_PORT=443
VERIFY_SSL=false

View file

@ -0,0 +1,164 @@
# Documentation des Modèles d'Intégration UniFi
Ce document décrit les modèles de données utilisés dans le module d'intégration UniFi pour Odoo 17.0.
## Modèles Principaux
### Site UniFi (`udm.site`)
Représente un site UniFi géré par un UDM/UDR ou un contrôleur logiciel.
**Champs:**
- `name` (Char): Nom du site
- `site_id` (Char): Identifiant du site dans UniFi (toujours 'default')
- `description` (Text): Description du site
- `address` (Text): Adresse physique
- `active` (Boolean): Statut actif
- `controller_type` (Selection): Type de contrôleur (UDM/UDR, Software)
- `host` (Char): Adresse IP ou nom d'hôte du contrôleur
- `port` (Integer): Numéro de port (par défaut: 443)
- `username` (Char): Nom d'utilisateur pour l'authentification
- `password` (Char): Mot de passe pour l'authentification
- `mfa_token` (Char): Code d'authentification à deux facteurs
- `timestamp` (Datetime): Horodatage de la dernière mise à jour
- `raw_data` (Text): Données de configuration brutes au format JSON
**Relations:**
- `system_info_id` (Many2one): Informations système
- `network_ids` (One2many): Configurations réseau
- `vlan_ids` (One2many): Configurations VLAN
- `device_ids` (One2many): Appareils connectés
- `user_ids` (One2many): Comptes utilisateur
- `settings_id` (Many2one): Paramètres généraux
- `firewall_rule_ids` (One2many): Règles de pare-feu
- `port_forward_ids` (One2many): Règles de redirection de port
- `dns_config_id` (Many2one): Configuration DNS
- `routing_config_id` (Many2one): Configuration de routage
- `dashboard_ids` (One2many): Métriques du tableau de bord
- `statistic_ids` (One2many): Statistiques
## Modèles de Réseau
### Network (`udm.network`)
Représente une configuration réseau.
**Champs:**
- `name` (Char): Nom du réseau
- `purpose` (Selection): Objectif du réseau (entreprise, invité, etc.)
- `subnet` (Char): Sous-réseau
- `vlan_enabled` (Boolean): Statut VLAN
- `vlan_id` (Integer): Identifiant VLAN
- `dhcp_enabled` (Boolean): Statut DHCP
- `dhcp_start` (Char): Adresse de début DHCP
- `dhcp_stop` (Char): Adresse de fin DHCP
- `domain_name` (Char): Nom de domaine du réseau
- `raw_data` (Text): Données réseau brutes
### VLAN (`udm.vlan`)
Stocke la configuration VLAN.
**Champs:**
- `vlan_id` (Integer): Identifiant VLAN
- `name` (Char): Nom du VLAN
- `raw_data` (Text): Données VLAN brutes
## Modèles de Sécurité
### Firewall Rule (`udm.firewall.rule`)
Définit les règles du pare-feu.
**Champs:**
- `name` (Char): Nom de la règle
- `description` (Text): Description de la règle
- `action` (Selection): Action (accepter, rejeter, bloquer)
- `protocol` (Char): Protocole réseau
- `source` (Char): Adresse/réseau source
- `destination` (Char): Adresse/réseau de destination
- `enabled` (Boolean): Statut de la règle
- `raw_data` (Text): Données de règle brutes
### Port Forward (`udm.port.forward`)
Définit les règles de redirection de port.
**Champs:**
- `name` (Char): Nom de la règle
- `enabled` (Boolean): Statut de la règle
- `src_port` (Char): Port source
- `dst_port` (Char): Port de destination
- `protocol` (Selection): Protocole (TCP, UDP, les deux)
- `dst_address` (Char): Adresse de destination
- `raw_data` (Text): Données de règle brutes
## Modèles Système
### System Info (`udm.system.info`)
Stocke les informations système de l'UDM Pro.
**Champs:**
- `hostname` (Char): Nom d'hôte du système
- `version` (Char): Version du firmware
- `model` (Char): Modèle de l'appareil
- `uptime` (Integer): Temps de fonctionnement
- `serial` (Char): Numéro de série
- `mac_address` (Char): Adresse MAC
- `raw_data` (Text): Données système brutes
### Settings (`udm.settings`)
Stocke les paramètres généraux de l'UDM Pro.
**Champs:**
- `timezone` (Char): Fuseau horaire du système
- `ntp_servers` (Char): Liste des serveurs NTP
- `dns_servers` (Char): Liste des serveurs DNS
- `raw_data` (Text): Données de paramètres brutes
### DNS Config (`udm.dns.config`)
Stocke la configuration DNS.
**Champs:**
- `enabled` (Boolean): Statut du service DNS
- `filters_enabled` (Boolean): Statut du filtrage de contenu
- `custom_dns` (Char): Serveurs DNS personnalisés
- `raw_data` (Text): Configuration DNS brute
### Routing Config (`udm.routing.config`)
Stocke la configuration de routage.
**Champs:**
- `ospf_enabled` (Boolean): Statut OSPF
- `static_routes` (Text): Liste des routes statiques
- `raw_data` (Text): Données de routage brutes
## Modèles Utilisateur
### User (`udm.user`)
Représente un compte utilisateur UniFi.
**Champs:**
- `name` (Char): Nom d'utilisateur
- `email` (Char): Adresse email
- `role` (Char): Rôle de l'utilisateur
- `enabled` (Boolean): Statut du compte
- `raw_data` (Text): Données utilisateur brutes
### Device (`udm.device`)
Représente un appareil réseau.
**Champs:**
- `name` (Char): Nom de l'appareil
- `mac` (Char): Adresse MAC
- `ip` (Char): Adresse IP
- `device_type` (Char): Type d'appareil
- `model` (Char): Modèle de l'appareil
- `last_seen` (Datetime): Dernier horodatage de connexion
- `raw_data` (Text): Données de l'appareil brutes

View file

@ -0,0 +1,2 @@
requests>=2.31.0
python-dotenv>=1.0.0

View file

@ -0,0 +1,98 @@
"""
Script to display UniFi UDM Pro clients in a clear format
"""
from datetime import datetime
from unifi_client import UnifiClient
def format_uptime(uptime_seconds):
"""Format uptime in a readable format"""
if not uptime_seconds:
return "N/A"
days = uptime_seconds // (24 * 3600)
hours = (uptime_seconds % (24 * 3600)) // 3600
minutes = (uptime_seconds % 3600) // 60
if days > 0:
return f"{days}j {hours}h {minutes}m"
elif hours > 0:
return f"{hours}h {minutes}m"
else:
return f"{minutes}m"
def format_bytes(bytes_value):
"""Format bytes in human readable format"""
if not bytes_value:
return "0 B"
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_value < 1024:
return f"{bytes_value:.1f} {unit}"
bytes_value /= 1024
return f"{bytes_value:.1f} PB"
def get_client_type(client):
"""Determine client type based on various fields"""
if client.get('is_wired', False):
return 'Filaire'
elif client.get('is_wireless', False):
return 'WiFi'
return 'Inconnu'
def main():
"""Main function to display clients"""
with UnifiClient() as client:
print("Récupération des clients connectés...")
# Get clients
clients = client.get_clients()
if not clients or 'data' not in clients:
print("Aucun client trouvé ou erreur de connexion")
return
# Get networks for VLAN mapping
networks = client.get_networks()
vlan_names = {
str(net.get('vlan', '')): net.get('name', 'N/A')
for net in networks.get('data', [])
if net.get('vlan') is not None
}
# Group clients by connection type
wired_clients = []
wireless_clients = []
for c in clients['data']:
if c.get('is_wired', False):
wired_clients.append(c)
elif c.get('is_wireless', False):
wireless_clients.append(c)
# Display clients
def print_client_list(client_list, title):
if not client_list:
return
print(f"\n=== Clients {title} ===")
print(f"{'Nom':<25} {'IP':<15} {'MAC':<18} {'VLAN':<15} {'Uptime':<15} {'Trafic TX/RX'}")
print("-" * 100)
for c in sorted(client_list, key=lambda x: x.get('name', '')):
name = c.get('name', c.get('hostname', 'N/A'))
ip = c.get('ip', 'N/A')
mac = c.get('mac', 'N/A')
vlan_id = str(c.get('vlan', ''))
vlan = f"{vlan_id} ({vlan_names.get(vlan_id, 'N/A')})"
uptime = format_uptime(c.get('uptime', 0))
tx = format_bytes(c.get('tx_bytes', 0))
rx = format_bytes(c.get('rx_bytes', 0))
print(f"{name[:25]:<25} {ip:<15} {mac:<18} {vlan[:15]:<15} {uptime:<15} {tx}/{rx}")
print_client_list(wired_clients, "Filaires")
print_client_list(wireless_clients, "WiFi")
print(f"\nTotal: {len(wired_clients)} clients filaires, {len(wireless_clients)} clients WiFi")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,163 @@
"""
Script to display detailed information about a UniFi device
"""
from datetime import datetime
from unifi_client import UnifiClient
def format_bytes(bytes_value):
"""Format bytes in human readable format"""
if not bytes_value:
return "0 B"
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_value < 1024:
return f"{bytes_value:.1f} {unit}"
bytes_value /= 1024
return f"{bytes_value:.1f} PB"
def format_temperature(temp_celsius):
"""Format temperature in Celsius"""
if not temp_celsius:
return "N/A"
return f"{temp_celsius}°C"
def format_radio_stats(radio):
"""Format radio statistics"""
channel = radio.get('channel', 'N/A')
width = radio.get('channel_width', 'N/A')
tx_power = radio.get('tx_power', 'N/A')
tx_power_str = f"{tx_power}dBm" if tx_power != 'N/A' else 'N/A'
utilization = radio.get('utilization', 'N/A')
util_str = f"{utilization}%" if utilization != 'N/A' else 'N/A'
return f"Canal {channel} ({width}MHz), Puissance: {tx_power_str}, Utilisation: {util_str}"
def format_client_info(client):
"""Format client information"""
name = client.get('name', client.get('hostname', 'Inconnu'))
ip = client.get('ip', 'N/A')
signal = client.get('signal', 0)
uptime = client.get('uptime', 0)
uptime_str = f"{uptime//3600}h {(uptime%3600)//60}m" if uptime else 'N/A'
tx_rate = client.get('tx_rate', 0)
rx_rate = client.get('rx_rate', 0)
return f"{name:<20} IP: {ip:<15} Signal: {signal}dBm, Up: {uptime_str}, Vitesse: ↓{rx_rate}Mbps ↑{tx_rate}Mbps"
def format_port_status(port):
"""Format port status information"""
if not port:
return "N/A"
speed = port.get('speed', 0)
if speed == 0:
return "Déconnecté"
return f"{speed}Mbps {'(PoE)' if port.get('poe_enable', False) else ''}"
def show_device_details(device):
"""Display detailed information about a device"""
print(f"\n=== Détails de l'appareil : {device.get('name', 'N/A')} ===")
# Informations de base
print("\nInformations de base:")
print(f" Modèle : {device.get('model', 'N/A')}")
print(f" Version : {device.get('version', 'N/A')}")
print(f" IP : {device.get('ip', 'N/A')}")
print(f" MAC : {device.get('mac', 'N/A')}")
print(f" État : {'En ligne' if device.get('state', 0) == 1 else 'Hors ligne'}")
# Statistiques système
print("\nStatistiques système:")
print(f" CPU : {device.get('system_stats', {}).get('cpu', 'N/A')}%")
print(f" Mémoire : {device.get('system_stats', {}).get('mem', 'N/A')}%")
print(f" Température : {format_temperature(device.get('general_temperature', 0))}")
# Statistiques réseau
stats = device.get('stat', {})
print("\nStatistiques réseau:")
print(f" Bytes reçus : {format_bytes(stats.get('rx_bytes', 0))}")
print(f" Bytes envoyés : {format_bytes(stats.get('tx_bytes', 0))}")
print(f" Paquets reçus : {stats.get('rx_packets', 0)}")
print(f" Paquets envoyés: {stats.get('tx_packets', 0)}")
# Information sur les ports (pour les switches)
if 'port_table' in device:
print("\nPorts:")
for port in device['port_table']:
port_name = port.get('name', f"Port {port.get('port_idx', 'N/A')}")
status = format_port_status(port)
print(f" {port_name:<12} : {status}")
# Information WiFi (pour les points d'accès)
if device.get('radio_table'):
print("\nRadios WiFi:")
for radio in device['radio_table']:
band = "5 GHz" if radio.get('radio', '') == 'na' else "2.4 GHz"
channel = radio.get('channel', 'N/A')
tx_power = radio.get('tx_power', 'N/A')
print(f" {band:<12} : Canal {channel}, Puissance {tx_power}dBm")
def format_poe_status(port):
"""Format PoE status information"""
if not port or not port.get('poe_enable'):
return ''
try:
power = float(port.get('poe_power', 0))
voltage = float(port.get('poe_voltage', 0))
current = float(port.get('poe_current', 0))
if power > 0:
return f"PoE: {power:.1f}W ({voltage:.1f}V, {current:.0f}mA)"
except (ValueError, TypeError):
pass
return 'PoE activé'
def main():
"""Main function to display device details"""
with UnifiClient() as client:
print("Récupération des appareils UniFi...")
# Get devices
devices = client.get_devices()
if not devices or 'data' not in devices:
print("Aucun appareil trouvé ou erreur de connexion")
return
# Find all APs
aps = [device for device in devices['data'] if device.get('model') in ['U7PG2', 'U7LR', 'U7IW']]
if aps:
for i, ap in enumerate(aps, 1):
if i > 1:
print("\n" + "=" * 80 + "\n")
# Show basic details
show_device_details(ap)
# Show WiFi details
if 'radio_table' in ap:
print("\nRadios WiFi:")
for radio in ap['radio_table']:
band = "5 GHz" if radio.get('radio') == 'na' else "2.4 GHz"
print(f" {band}: {format_radio_stats(radio)}")
# Show connected clients
if 'vap_table' in ap:
print("\nClients connectés:")
for vap in ap['vap_table']:
if vap.get('sta_count', 0) > 0:
ssid = vap.get('essid', 'N/A')
print(f"\nSSID: {ssid}")
print(" Nom IP Signal Temps Vitesse")
print(" " + "-" * 75)
for client in vap.get('sta_table', []):
print(f" {format_client_info(client)}")
else:
print("Aucun point d'accès trouvé")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,86 @@
"""
Script to display UniFi UDM Pro devices in a clear format
"""
from datetime import datetime
from unifi_client import UnifiClient
def format_uptime(uptime_seconds):
"""Format uptime in a readable format"""
if not uptime_seconds:
return "N/A"
days = uptime_seconds // (24 * 3600)
hours = (uptime_seconds % (24 * 3600)) // 3600
minutes = (uptime_seconds % 3600) // 60
if days > 0:
return f"{days}j {hours}h {minutes}m"
elif hours > 0:
return f"{hours}h {minutes}m"
else:
return f"{minutes}m"
def get_device_type(device):
"""Get device type in French"""
model = device.get('model', '').lower()
if 'udm' in model:
return 'Routeur'
elif 'usw' in model:
return 'Switch'
elif 'uap' in model:
return 'Point d\'accès'
return 'Autre'
def get_device_status(device):
"""Get device status in French"""
if device.get('state', 0) == 1:
return '✓ En ligne'
return '✗ Hors ligne'
def format_device(device):
"""Format device information"""
return {
'name': device.get('name', 'N/A'),
'model': device.get('model', 'N/A'),
'type': get_device_type(device),
'ip': device.get('ip', 'N/A'),
'mac': device.get('mac', 'N/A'),
'version': device.get('version', 'N/A'),
'status': get_device_status(device),
'uptime': format_uptime(device.get('uptime', 0))
}
def main():
"""Main function to display devices"""
with UnifiClient() as client:
print("Récupération des appareils UniFi...")
# Get devices
devices = client.get_devices()
if not devices or 'data' not in devices:
print("Aucun appareil trouvé ou erreur de connexion")
return
# Group devices by type
devices_by_type = {}
for device in devices['data']:
device_type = get_device_type(device)
if device_type not in devices_by_type:
devices_by_type[device_type] = []
devices_by_type[device_type].append(device)
# Display devices by type
for device_type, device_list in devices_by_type.items():
print(f"\n=== {device_type}s ===")
print(f"{'Nom':<20} {'Modèle':<15} {'IP':<15} {'Version':<10} {'État':<15} {'Uptime'}")
print("-" * 90)
for device in sorted(device_list, key=lambda x: x.get('name', '')):
d = format_device(device)
print(f"{d['name'][:20]:<20} {d['model'][:15]:<15} {d['ip']:<15} "
f"{d['version'][:10]:<10} {d['status']:<15} {d['uptime']}")
print(f"\nTotal: {len(devices['data'])} appareils UniFi")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,68 @@
"""
Script to display UniFi UDM Pro firewall rules in a clear format
"""
import json
from unifi_client import UnifiClient
def format_rule(rule):
"""Format a firewall rule for better readability"""
direction = "IN" if rule.get('ruleset', '').endswith('_IN') else "OUT"
src = rule.get('src_address', 'any')
dst = rule.get('dst_address', 'any')
if direction == "IN":
from_to = f"FROM {src} TO {dst if dst != '' else 'any'}"
else:
from_to = f"FROM {src if src != '' else 'any'} TO {dst}"
return {
'name': rule.get('name', 'N/A'),
'action': rule.get('action', 'N/A').upper(),
'direction': direction,
'protocol': rule.get('protocol', 'all').upper(),
'ports': f"{rule.get('src_port', 'any')} -> {rule.get('dst_port', 'any')}",
'addresses': from_to,
'enabled': '' if rule.get('enabled', True) else ''
}
def print_rules_by_type(rules, ruleset_type):
"""Print rules filtered by ruleset type"""
filtered_rules = [r for r in rules if ruleset_type in r.get('ruleset', '')]
if filtered_rules:
print(f"\n=== Règles {ruleset_type} ===")
for rule in filtered_rules:
r = format_rule(rule)
print(f"\n{r['name']}")
print(f" État : {r['enabled']}")
print(f" Action : {r['action']}")
print(f" Protocol : {r['protocol']}")
print(f" Ports : {r['ports']}")
print(f" Adresses : {r['addresses']}")
def main():
"""Main function to display firewall rules"""
with UnifiClient() as client:
print("Récupération des règles de pare-feu...")
# Get firewall rules
rules = client.get_firewall_rules()
if not rules or 'data' not in rules:
print("Aucune règle trouvée ou erreur de connexion")
return
# Display rules by type
print_rules_by_type(rules['data'], 'WAN_IN')
print_rules_by_type(rules['data'], 'WAN_OUT')
print_rules_by_type(rules['data'], 'LAN_IN')
print_rules_by_type(rules['data'], 'LAN_OUT')
# Get and display firewall groups
groups = client.get_firewall_groups()
if groups and 'data' in groups:
print("\n=== Groupes de Pare-feu ===")
for group in groups['data']:
print(f"\n{group.get('name', 'N/A')} ({group.get('group_type', 'N/A')})")
print(f" Membres : {', '.join(group.get('group_members', []))}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,58 @@
"""
Script to display UniFi UDM Pro networks in a clear format
"""
import json
from unifi_client import UnifiClient
def format_network(network):
"""Format network information for better readability"""
purpose = network.get('purpose', 'corporate')
if purpose == 'corporate':
purpose = 'Entreprise'
elif purpose == 'guest':
purpose = 'Invité'
elif purpose == 'vlan-only':
purpose = 'VLAN uniquement'
dhcp_enabled = network.get('dhcpd_enabled', False)
dhcp_info = ""
if dhcp_enabled:
start = network.get('dhcpd_start', 'N/A')
stop = network.get('dhcpd_stop', 'N/A')
dhcp_info = f"({start} - {stop})"
return {
'nom': network.get('name', 'N/A'),
'vlan': network.get('vlan', 'Non'),
'purpose': purpose,
'subnet': network.get('subnet', 'N/A'),
'dhcp': 'Activé ' + dhcp_info if dhcp_enabled else 'Désactivé',
'dns': network.get('dns_nameservers', ['N/A']),
'enabled': network.get('enabled', True)
}
def main():
"""Main function to display networks"""
with UnifiClient() as client:
print("Récupération des réseaux configurés...")
# Get networks
networks = client.get_networks()
if not networks or 'data' not in networks:
print("Aucun réseau trouvé ou erreur de connexion")
return
# Display networks
print("\n=== Réseaux Configurés ===")
for network in networks['data']:
net = format_network(network)
print(f"\n{net['nom']}")
print(f" État : {'Actif' if net['enabled'] else 'Inactif'}")
print(f" Type : {net['purpose']}")
print(f" VLAN : {net['vlan']}")
print(f" Subnet : {net['subnet']}")
print(f" DHCP : {net['dhcp']}")
print(f" DNS : {', '.join(net['dns'])}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,54 @@
"""
Script to display UniFi UDM Pro VLANs in a clear format
"""
from unifi_client import UnifiClient
def get_vlan_purpose(network):
"""Get the purpose/usage of a VLAN"""
purpose = network.get('purpose', 'corporate')
if purpose == 'corporate':
return 'Entreprise'
elif purpose == 'guest':
return 'Invité'
elif purpose == 'vlan-only':
return 'VLAN uniquement'
return purpose
def main():
"""Main function to display VLANs"""
with UnifiClient() as client:
print("Récupération des VLANs configurés...")
# Get networks
networks = client.get_networks()
if not networks or 'data' not in networks:
print("Aucun VLAN trouvé ou erreur de connexion")
return
# Filter and sort networks with VLANs
vlan_networks = [n for n in networks['data'] if n.get('vlan', None) is not None]
vlan_networks.sort(key=lambda x: x.get('vlan', 0))
# Display VLANs
print("\n=== VLANs Configurés ===")
print(f"{'VLAN ID':<8} {'Nom':<20} {'Type':<15} {'Plage DHCP':<35} {'État'}")
print("-" * 85)
for network in vlan_networks:
vlan_id = network.get('vlan', 'N/A')
name = network.get('name', 'N/A')
purpose = get_vlan_purpose(network)
# Format DHCP range
dhcp_range = 'DHCP désactivé'
if network.get('dhcpd_enabled', False):
start = network.get('dhcpd_start', '')
stop = network.get('dhcpd_stop', '')
dhcp_range = f"{start} - {stop}"
state = 'Actif' if network.get('enabled', True) else 'Inactif'
print(f"{vlan_id:<8} {name[:20]:<20} {purpose[:15]:<15} {dhcp_range[:35]:<35} {state}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,74 @@
"""
Test script for UniFi UDM Pro Firewall Management
This script demonstrates the firewall management capabilities of the UnifiClient class.
"""
import json
from unifi_client import UnifiClient
def format_firewall_rule(rule):
"""Format a firewall rule for display
Args:
rule (dict): Firewall rule data
Returns:
dict: Formatted rule data
"""
return {
'name': rule.get('name', 'N/A'),
'ruleset': rule.get('ruleset', 'N/A'),
'action': rule.get('action', 'N/A'),
'protocol': rule.get('protocol', 'all'),
'src_address': rule.get('src_address', 'any'),
'dst_address': rule.get('dst_address', 'any'),
'enabled': rule.get('enabled', True)
}
def main():
"""
Main test function to demonstrate firewall management
"""
with UnifiClient() as client:
print("Connexion à l'UDM Pro...")
# Get existing firewall rules
print("\nRègles de pare-feu existantes:")
rules = client.get_firewall_rules()
if rules and rules.get('data'):
for rule in rules['data']:
formatted_rule = format_firewall_rule(rule)
print(json.dumps(formatted_rule, indent=2))
# Get firewall groups
print("\nGroupes de pare-feu:")
groups = client.get_firewall_groups()
if groups and groups.get('data'):
for group in groups['data']:
print(json.dumps({
'name': group.get('name', 'N/A'),
'type': group.get('group_type', 'N/A'),
'members': group.get('group_members', [])
}, indent=2))
# Example: Create a new firewall rule
# Note: Commented out to prevent accidental rule creation
"""
new_rule = {
'name': 'Test SSH Access',
'enabled': True,
'action': 'accept',
'ruleset': 'WAN_IN',
'protocol': 'tcp',
'dst_port': '22',
'description': 'Allow SSH access from WAN'
}
print("\nCréation d'une nouvelle règle:")
result = client.create_firewall_rule(new_rule)
if result:
print("Règle créée avec succès:")
print(json.dumps(format_firewall_rule(result['data'][0]), indent=2))
"""
if __name__ == "__main__":
main()

View file

@ -0,0 +1,91 @@
"""
Test script for UniFi UDM Pro API Client
This script demonstrates the basic usage of the UnifiClient class.
"""
import json
from unifi_client import UnifiClient
def format_response(data, max_items=5):
"""Format API response data to show only essential information
Args:
data (dict): API response data
max_items (int): Maximum number of items to show
Returns:
dict: Formatted data
"""
if not data or 'data' not in data:
return data
result = {'meta': data.get('meta', {}), 'data': []}
# Take only the first max_items
items = data['data'][:max_items]
for item in items:
# For devices, show only essential info
if 'model' in item:
result['data'].append({
'name': item.get('name', 'N/A'),
'model': item.get('model', 'N/A'),
'ip': item.get('ip', 'N/A'),
'status': item.get('state', 'N/A')
})
# For clients, show only essential info
elif 'hostname' in item:
result['data'].append({
'name': item.get('hostname', 'N/A'),
'ip': item.get('ip', 'N/A'),
'mac': item.get('mac', 'N/A'),
'network': item.get('network', 'N/A')
})
# For other types, keep all fields
else:
result['data'].append(item)
if len(data['data']) > max_items:
print(f"\nNote: Showing {max_items} of {len(data['data'])} items")
return result
def main():
"""
Main test function to demonstrate UnifiClient usage
"""
# Using context manager for automatic login/logout
with UnifiClient() as client:
# Get system information
system_info = client.get_system_info()
if system_info:
print("Successfully connected to UDM Pro!")
print("\nSystem Information:")
system_info = format_response(system_info)
print(json.dumps(system_info, indent=2))
# Get network health status
health = client.get_health()
if health:
print("\nNetwork Health:")
health = format_response(health)
print(json.dumps(health, indent=2))
# Get connected devices
devices = client.get_devices()
if devices:
print("\nConnected Devices:")
devices = format_response(devices)
print(json.dumps(devices, indent=2))
# Get active clients
clients = client.get_clients()
if clients:
print("\nActive Clients:")
clients = format_response(clients)
print(json.dumps(clients, indent=2))
else:
print("Failed to retrieve system information")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,306 @@
"""
UniFi UDM Pro API Client
This module provides a client to interact with the UniFi Dream Machine Pro API.
"""
import os
import json
import requests
from urllib3.exceptions import InsecureRequestWarning
import dotenv
# Suppress only the single warning from urllib3 needed.
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
class UnifiClient:
"""
A client for interacting with the UniFi Dream Machine Pro API.
Handles authentication and basic API operations.
"""
def __init__(self, config_file=None):
"""
Initialize the UniFi client with configuration from environment file.
Args:
config_file (str): Path to the configuration file (default: Doc/credential.txt)
"""
if config_file is None:
config_file = os.path.join(os.path.dirname(__file__), 'Doc/credential.txt')
# Load configuration from file
dotenv.load_dotenv(config_file)
self.host = os.getenv('UDMP_HOST').rstrip('/')
self.username = os.getenv('UDMP_USERNAME')
self.password = os.getenv('UDMP_PASSWORD')
self.verify_ssl = os.getenv('VERIFY_SSL', 'false').lower() == 'true'
self.session = requests.Session()
self.session.verify = self.verify_ssl
self.csrf_token = None
def login(self):
"""
Authenticate with the UDM Pro.
Returns:
bool: True if login successful, False otherwise
"""
login_url = f"{self.host}/api/auth/login"
headers = {'Content-Type': 'application/json'}
data = {
'username': self.username,
'password': self.password
}
try:
response = self.session.post(
login_url,
headers=headers,
json=data,
timeout=10 # 10 seconds timeout
)
response.raise_for_status()
# Store CSRF token if present
if 'X-CSRF-Token' in response.headers:
self.csrf_token = response.headers['X-CSRF-Token']
return True
except requests.exceptions.RequestException as e:
print(f"Login failed: {str(e)}")
return False
def get_system_info(self):
"""
Get basic system information.
Returns:
dict: System information or None if request fails
"""
url = f"{self.host}/proxy/network/api/s/default/self"
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Failed to get system info: {str(e)}")
return None
def logout(self):
"""
Logout from the UDM Pro.
Returns:
bool: True if logout successful, False otherwise
"""
logout_url = f"{self.host}/api/auth/logout"
# Add required headers for logout
headers = {
'Content-Type': 'application/json',
}
if self.csrf_token:
headers['X-CSRF-Token'] = self.csrf_token
try:
# Send empty JSON body as required by the API
response = self.session.post(logout_url, headers=headers, json={}, timeout=10)
# Note: UDM Pro returns 200 even if session is already expired
if response.status_code == 200:
return True
response.raise_for_status()
return True
except requests.exceptions.RequestException as e:
# If we get a 403, it might mean we're already logged out
if isinstance(e, requests.exceptions.HTTPError) and e.response.status_code == 403:
return True
print(f"Logout failed: {str(e)}")
return False
def __enter__(self):
"""Context manager entry point"""
self.login()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit point"""
self.logout()
def get_devices(self):
"""Get all devices connected to the UDM Pro.
Returns:
dict: List of devices or None if request fails
"""
url = f"{self.host}/proxy/network/api/s/default/stat/device"
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Failed to get devices: {str(e)}")
return None
def get_clients(self):
"""Get all clients (users) connected to the network.
Returns:
dict: List of clients or None if request fails
"""
url = f"{self.host}/proxy/network/api/s/default/stat/sta"
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Failed to get clients: {str(e)}")
return None
def get_health(self):
"""Get the health status of the UDM Pro and network.
Returns:
dict: Health information or None if request fails
"""
url = f"{self.host}/proxy/network/api/s/default/stat/health"
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Failed to get health status: {str(e)}")
return None
def get_firewall_rules(self):
"""Get all firewall rules.
Returns:
dict: List of firewall rules or None if request fails
"""
url = f"{self.host}/proxy/network/api/s/default/rest/firewallrule"
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Failed to get firewall rules: {str(e)}")
return None
def get_firewall_groups(self):
"""Get all firewall groups (address groups, port groups).
Returns:
dict: List of firewall groups or None if request fails
"""
url = f"{self.host}/proxy/network/api/s/default/rest/firewallgroup"
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Failed to get firewall groups: {str(e)}")
return None
def create_firewall_rule(self, rule_data):
"""Create a new firewall rule.
Args:
rule_data (dict): Rule configuration with the following fields:
- name: Rule name
- action: 'accept' or 'drop' or 'reject'
- ruleset: 'WAN_IN', 'WAN_OUT', 'WAN_LOCAL', 'LAN_IN', etc.
- protocol: 'all', 'tcp', 'udp', etc.
- src_address: Source address/network (optional)
- dst_address: Destination address/network (optional)
- src_port: Source port (optional)
- dst_port: Destination port (optional)
- enabled: True/False
Returns:
dict: Created rule data or None if request fails
"""
url = f"{self.host}/proxy/network/api/s/default/rest/firewallrule"
try:
response = self.session.post(url, json=rule_data, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Failed to create firewall rule: {str(e)}")
return None
def update_firewall_rule(self, rule_id, rule_data):
"""Update an existing firewall rule.
Args:
rule_id (str): ID of the rule to update
rule_data (dict): Updated rule configuration
Returns:
dict: Updated rule data or None if request fails
"""
url = f"{self.host}/proxy/network/api/s/default/rest/firewallrule/{rule_id}"
try:
response = self.session.put(url, json=rule_data, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Failed to update firewall rule: {str(e)}")
return None
def delete_firewall_rule(self, rule_id):
"""Delete a firewall rule.
Args:
rule_id (str): ID of the rule to delete
Returns:
bool: True if successful, False otherwise
"""
url = f"{self.host}/proxy/network/api/s/default/rest/firewallrule/{rule_id}"
try:
response = self.session.delete(url, timeout=10)
response.raise_for_status()
return True
except requests.exceptions.RequestException as e:
print(f"Failed to delete firewall rule: {str(e)}")
return False
def get_networks(self):
"""Get all networks configuration.
Returns:
dict: List of networks or None if request fails
"""
url = f"{self.host}/proxy/network/api/s/default/rest/networkconf"
try:
response = self.session.get(url, timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Failed to get networks: {str(e)}")
return None

View file

@ -0,0 +1,157 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * unifi_integration
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-11 12:34+0000\n"
"PO-Revision-Date: 2025-03-11 12:34+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: unifi_integration
#: model:ir.model.fields,field_description:unifi_integration.field_udm_port_forward__name
msgid "Name"
msgstr "Nom"
#. module: unifi_integration
#: model:ir.model.fields,field_description:unifi_integration.field_udm_port_forward__enabled
msgid "Enabled"
msgstr "Activé"
#. module: unifi_integration
#: model:ir.model.fields,field_description:unifi_integration.field_udm_port_forward__src_port
msgid "Source Port"
msgstr "Port source"
#. module: unifi_integration
#: model:ir.model.fields,field_description:unifi_integration.field_udm_port_forward__dst_port
msgid "Destination Port"
msgstr "Port de destination"
#. module: unifi_integration
#: model:ir.model.fields,field_description:unifi_integration.field_udm_port_forward__dst_address
msgid "Destination Address"
msgstr "Adresse de destination"
#. module: unifi_integration
#: model:ir.model.fields,field_description:unifi_integration.field_udm_dns_config__enabled
msgid "Enabled"
msgstr "Activé"
#. module: unifi_integration
#: model:ir.model.fields,field_description:unifi_integration.field_udm_dns_config__filters_enabled
msgid "Content Filtering Enabled"
msgstr "Filtrage de contenu activé"
#. module: unifi_integration
#: model:ir.model.fields,field_description:unifi_integration.field_udm_dns_config__custom_dns
msgid "Custom DNS Servers"
msgstr "Serveurs DNS personnalisés"
#. module: unifi_integration
#: model:ir.model.fields,field_description:unifi_integration.field_udm_routing_config__ospf_enabled
msgid "OSPF Enabled"
msgstr "OSPF activé"
#. module: unifi_integration
#: model:ir.model.fields,field_description:unifi_integration.field_udm_routing_config__static_routes
msgid "Static Routes"
msgstr "Routes statiques"
#. module: unifi_integration
#: model:ir.model.fields,field_description:unifi_integration.field_udm_port_forward__protocol
msgid "Protocol"
msgstr "Protocole"
#. module: unifi_integration
#: model:ir.model.fields.selection,name:unifi_integration.selection__udm_port_forward__protocol__tcp
msgid "TCP"
msgstr "TCP"
#. module: unifi_integration
#: model:ir.model.fields.selection,name:unifi_integration.selection__udm_port_forward__protocol__udp
msgid "UDP"
msgstr "UDP"
#. module: unifi_integration
#: model:ir.model.fields.selection,name:unifi_integration.selection__udm_port_forward__protocol__both
msgid "TCP & UDP"
msgstr "TCP et UDP"
#. module: unifi_integration
#: model:ir.actions.act_window,name:unifi_integration.action_udm_port_forward
#: model:ir.ui.menu,name:unifi_integration.menu_udm_port_forward
msgid "Port Forwards"
msgstr "Redirections de ports"
#. module: unifi_integration
#: model:ir.actions.act_window,name:unifi_integration.action_udm_dns_config
#: model:ir.ui.menu,name:unifi_integration.menu_udm_dns_config
msgid "DNS Configuration"
msgstr "Configuration DNS"
#. module: unifi_integration
#: model:ir.actions.act_window,name:unifi_integration.action_udm_routing_config
#: model:ir.ui.menu,name:unifi_integration.menu_udm_routing_config
msgid "Routing Configuration"
msgstr "Configuration du routage"
#. module: unifi_integration
#: model:ir.model,name:unifi_integration.model_udm_port_forward
msgid "UDM Pro Port Forward Rule"
msgstr "Règle de redirection de port UDM Pro"
#. module: unifi_integration
#: model:ir.model,name:unifi_integration.model_udm_dns_config
msgid "UDM Pro DNS Configuration"
msgstr "Configuration DNS UDM Pro"
#. module: unifi_integration
#: model:ir.model,name:unifi_integration.model_udm_routing_config
msgid "UDM Pro Routing Configuration"
msgstr "Configuration du routage UDM Pro"
#. module: unifi_integration
#: model_terms:ir.ui.view,arch_db:unifi_integration.view_udm_port_forward_form
msgid "Identification"
msgstr "Identification"
#. module: unifi_integration
#: model_terms:ir.ui.view,arch_db:unifi_integration.view_udm_port_forward_form
msgid "Redirection"
msgstr "Redirection"
#. module: unifi_integration
#: model_terms:ir.ui.view,arch_db:unifi_integration.view_udm_dns_config_form
msgid "État"
msgstr "État"
#. module: unifi_integration
#: model_terms:ir.ui.view,arch_db:unifi_integration.view_udm_dns_config_form
#: model_terms:ir.ui.view,arch_db:unifi_integration.view_udm_routing_config_form
msgid "Configuration"
msgstr "Configuration"
#. module: unifi_integration
#: model_terms:ir.ui.view,arch_db:unifi_integration.view_udm_routing_config_form
msgid "Protocoles de routage"
msgstr "Protocoles de routage"
#. module: unifi_integration
#: model_terms:ir.ui.view,arch_db:unifi_integration.view_udm_routing_config_form
msgid "Routes"
msgstr "Routes"
#. module: unifi_integration
#: model_terms:ir.ui.view,arch_db:unifi_integration.view_udm_port_forward_form
#: model_terms:ir.ui.view,arch_db:unifi_integration.view_udm_dns_config_form
#: model_terms:ir.ui.view,arch_db:unifi_integration.view_udm_routing_config_form
msgid "Technical"
msgstr "Technique"

View file

@ -1,9 +1,25 @@
# -*- coding: utf-8 -*-
# Import module files
from . import udm_site
from . import udm_config
from . import udm_system_info
from . import udm_network
from . import udm_device
from . import udm_user
from . import udm_settings
from . import udm_firewall
from . import udm_port_forward
from . import udm_dns
from . import udm_routing
# Import all models to ensure they are registered with Odoo
from .udm_site import UdmSite
from .udm_config import UdmConfiguration
from .udm_network import UdmNetwork, UdmVlan
from .udm_device import UdmDevice
from .udm_user import UdmUser
from .udm_settings import UdmSettings
from .udm_firewall import UdmFirewallRule
from .udm_port_forward import UdmPortForward
from .udm_dns import UdmDnsConfig
from .udm_routing import UdmRoutingConfig

View file

@ -0,0 +1,316 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError
import json
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
class UdmDashboardMetric(models.Model):
"""Model for storing and managing UDM Pro dashboard metrics.
This model stores real-time and near-real-time metrics from UDM Pro devices,
such as bandwidth usage, CPU/memory utilization, connected clients count,
and security threat counts. Each metric is associated with a specific site
and includes both raw and formatted values for display.
The model supports:
- Multiple metric types with appropriate formatting
- Status computation based on thresholds
- Historical data storage for trending
- Automatic value formatting based on metric type
Metrics are ordered by last update time to show most recent data first.
"""
_name = 'udm.dashboard.metric'
_description = 'UDM Dashboard Metric'
_order = 'last_update desc' # Most recent metrics first
# Site this metric belongs to, cascade deletion if site is deleted
site_id = fields.Many2one('udm.site', string='Site', required=True, ondelete='cascade')
# Type of metric being tracked
metric_type = fields.Selection([
('bandwidth_usage', 'Bandwidth Usage'), # Network bandwidth utilization
('cpu_usage', 'CPU Usage'), # CPU utilization percentage
('memory_usage', 'Memory Usage'), # Memory utilization percentage
('clients_count', 'Connected Clients'), # Number of connected network clients
('wan_status', 'WAN Status'), # WAN connection status (up/down)
('threat_count', 'Security Threats'), # Number of security threats detected
('device_status', 'Device Status'), # Status of network devices (online/offline)
], string='Metric Type', required=True)
# Raw metric values
current_value = fields.Char(string='Current Value', required=True,
help="Current raw value of the metric")
max_value = fields.Char(string='Maximum Value',
help="Maximum allowed value for this metric, used for threshold calculations")
history_data = fields.Text(string='Historical Data',
help="JSON data containing historical values for graphing")
last_update = fields.Datetime(string='Last Update', default=fields.Datetime.now,
help="Timestamp of the last metric update")
# Computed fields for display
formatted_value = fields.Char(compute='_compute_formatted_value', string='Formatted Value',
help="Human-readable formatted value with appropriate units")
status = fields.Selection([
('normal', 'Normal'), # Operating within normal parameters
('warning', 'Warning'), # Approaching critical thresholds
('critical', 'Critical'), # Exceeded critical thresholds
], compute='_compute_status', string='Status',
help="Status indicator based on metric thresholds")
@api.depends('current_value', 'metric_type')
def _compute_formatted_value(self):
"""Compute a human-readable formatted value based on the metric type.
This method formats the raw value into a user-friendly string with appropriate units:
- For bandwidth: Converts to Mbps or Gbps with 1 decimal place
- For CPU/Memory: Adds percentage sign with 1 decimal place
- For other metrics: Uses the raw value as is
The formatted value is used in the UI to display metrics in a consistent
and readable format.
"""
for record in self:
if not record.current_value:
record.formatted_value = ''
continue
if record.metric_type == 'bandwidth_usage':
try:
value = float(record.current_value)
if value >= 1000:
record.formatted_value = f"{value/1000:.1f} Gbps"
else:
record.formatted_value = f"{value:.1f} Mbps"
except (ValueError, TypeError):
record.formatted_value = record.current_value
elif record.metric_type in ['cpu_usage', 'memory_usage']:
try:
value = float(record.current_value)
record.formatted_value = f"{value:.1f}%"
except (ValueError, TypeError):
record.formatted_value = record.current_value
else:
record.formatted_value = record.current_value
@api.depends('current_value', 'metric_type', 'max_value')
def _compute_status(self):
"""Compute the status of the metric based on predefined thresholds.
This method evaluates the current value against thresholds to determine
if the metric is in a normal, warning, or critical state. The thresholds
vary by metric type:
CPU/Memory Usage:
- Critical: >= 90%
- Warning: >= 75%
Bandwidth Usage:
- Critical: >= 90% of max
- Warning: >= 75% of max
Security Threats:
- Critical: >= 10 threats
- Warning: >= 5 threats
WAN Status:
- Critical: not 'up'
Device Status (format: 'online/total'):
- Critical: > 2 devices offline
- Warning: > 0 devices offline
The status is used in the UI to highlight metrics that need attention,
using color-coded badges (green/yellow/red).
"""
for record in self:
status = 'normal'
try:
if record.metric_type in ['cpu_usage', 'memory_usage']:
value = float(record.current_value)
if value >= 90:
status = 'critical'
elif value >= 75:
status = 'warning'
elif record.metric_type == 'bandwidth_usage':
if record.max_value:
value = float(record.current_value)
max_val = float(record.max_value)
usage_pct = (value / max_val) * 100
if usage_pct >= 90:
status = 'critical'
elif usage_pct >= 75:
status = 'warning'
elif record.metric_type == 'threat_count':
value = int(record.current_value)
if value >= 10:
status = 'critical'
elif value >= 5:
status = 'warning'
elif record.metric_type == 'wan_status':
if record.current_value != 'up':
status = 'critical'
elif record.metric_type == 'device_status':
if '/' in record.current_value:
online, total = map(int, record.current_value.split('/'))
offline = total - online
if offline > 2:
status = 'critical'
elif offline > 0:
status = 'warning'
except (ValueError, TypeError):
# If we can't parse the value, assume normal status
status = 'normal'
record.status = status
class UdmDashboardStat(models.Model):
"""Model for storing and analyzing historical UDM Pro statistics.
This model stores historical statistical data from UDM Pro devices for
long-term trend analysis and reporting. It supports both raw data points
and aggregated statistics (e.g., daily averages, totals).
Key features:
- Multiple statistic types (bandwidth, clients, threats, uptime)
- Support for different units of measurement
- Time-based statistics with start/end times
- Aggregation capabilities (sum, average, min, max)
- Built-in graphing support
Statistics are ordered by date (descending) to show most recent data first.
Raw and aggregated statistics are stored separately to maintain data
integrity while allowing flexible reporting.
"""
_name = 'udm.dashboard.stat'
_description = 'UDM Dashboard Statistic'
_order = 'date desc' # Most recent statistics first
# Site this statistic belongs to, cascade deletion if site is deleted
site_id = fields.Many2one('udm.site', string='Site', required=True, ondelete='cascade')
# Date of the statistic record
date = fields.Date(string='Date', required=True, default=fields.Date.today,
help="Date this statistic was recorded")
# Type of statistic being tracked
stat_type = fields.Selection([
('bandwidth_usage', 'Bandwidth Usage'), # Total bandwidth used
('client_count', 'Client Count'), # Number of clients over time
('threat_blocked', 'Threats Blocked'), # Number of security threats blocked
('device_uptime', 'Device Uptime'), # Device uptime duration
], string='Statistic Type', required=True,
help="Type of statistical data being recorded")
# Numerical value and its unit
value = fields.Float(string='Value', required=True,
help="Numerical value of the statistic")
unit = fields.Selection([
('bytes', 'Bytes'), # For bandwidth measurements
('count', 'Count'), # For counting items (clients, threats)
('percentage', 'Percentage'), # For utilization metrics
('hours', 'Hours'), # For time-based metrics
], string='Unit', required=True,
help="Unit of measurement for the value")
# Time range for detailed statistics
time_start = fields.Datetime(string='Start Time',
help="Start time for time-based statistics (e.g. hourly bandwidth usage)")
time_end = fields.Datetime(string='End Time',
help="End time for time-based statistics")
# Aggregation fields
is_aggregate = fields.Boolean(string='Is Aggregate', default=False,
help="Indicates if this record represents aggregated data")
aggregate_type = fields.Selection([
('sum', 'Sum'), # Total over the period
('avg', 'Average'), # Average over the period
('min', 'Minimum'), # Minimum value in the period
('max', 'Maximum'), # Maximum value in the period
], string='Aggregate Type',
help="Type of aggregation used for this record")
@api.model
def aggregate_stats(self, site_id, stat_type, start_date, end_date, aggregate_type='avg'):
"""Aggregate statistics for a specific site and type over a date range.
This method calculates aggregate values (sum, average, min, max) for
non-aggregated statistics within the specified date range.
Args:
site_id (int): ID of the site to aggregate stats for
stat_type (str): Type of statistic to aggregate
start_date (date): Start date of the range (inclusive)
end_date (date): End date of the range (inclusive)
aggregate_type (str): Type of aggregation to perform
'sum': Total of all values
'avg': Average of all values
'min': Minimum value
'max': Maximum value
Returns:
float: The aggregated value, or 0.0 if no stats found
"""
domain = [
('site_id', '=', site_id),
('stat_type', '=', stat_type),
('date', '>=', start_date),
('date', '<=', end_date),
('is_aggregate', '=', False) # Only aggregate raw statistics
]
stats = self.search(domain)
if not stats:
return 0.0
values = stats.mapped('value')
if aggregate_type == 'sum':
return sum(values)
elif aggregate_type == 'avg':
return sum(values) / len(values)
elif aggregate_type == 'min':
return min(values)
elif aggregate_type == 'max':
return max(values)
else:
return 0.0
def action_view_graph(self):
"""Open a graph view showing statistics over time.
This method creates an action to display a line graph of the statistic
values over time. The graph shows raw (non-aggregated) values grouped
by day.
The graph view is configured to:
- Show values on the y-axis
- Use a line graph for trend visualization
- Group data points by day on the x-axis
- Filter for the same site and statistic type
- Exclude aggregated records
Returns:
dict: An action dictionary that Odoo uses to open the graph view
"""
self.ensure_one()
action = {
'name': _('Statistics Graph'),
'view_mode': 'graph',
'res_model': 'udm.dashboard.stat',
'type': 'ir.actions.act_window',
'domain': [
('site_id', '=', self.site_id.id),
('stat_type', '=', self.stat_type),
('is_aggregate', '=', False) # Show only raw values
],
'context': {
'graph_measure': 'value', # Y-axis measurement
'graph_mode': 'line', # Line graph for trends
'graph_groupbys': ['date:day'] # Group by day on X-axis
}
}
return action

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
class UdmDashboardDataPoint(models.Model):
"""Data point for dashboard statistics"""
_name = 'udm.dashboard.data.point'
_description = 'Dashboard Data Point'
_order = 'sequence'
# Reference to the statistic this data point belongs to
stat_id = fields.Many2one('udm.dashboard.stat', string='Statistic',
required=True, ondelete='cascade')
# Data point attributes
sequence = fields.Integer(string='Sequence', default=10,
help='Order of this data point in the series')
value = fields.Float(string='Value',
help='Numeric value of this data point')
timestamp = fields.Datetime(string='Timestamp',
help='When this data point was recorded')

View file

@ -3,20 +3,44 @@
from odoo import models, fields, api, _
class UdmDevice(models.Model):
"""Représentation d'un périphérique dans l'UDM Pro"""
"""Represents a network device in the UniFi system
This model stores information about devices connected to the network,
including both UniFi devices (APs, switches) and client devices.
Devices are linked to a specific site and are automatically deleted when
the site is deleted (cascade).
"""
_name = 'udm.device'
_description = 'UDM Pro Device'
_description = 'UniFi Device'
_order = 'last_seen desc' # Most recently seen devices first
config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade')
name = fields.Char(string='Name')
mac = fields.Char(string='MAC Address')
ip = fields.Char(string='IP Address')
device_type = fields.Char(string='Device Type')
model = fields.Char(string='Model')
last_seen = fields.Datetime(string='Last Seen')
raw_data = fields.Text(string='Raw Data')
site_id = fields.Many2one('udm.site', string='Site', required=True,
ondelete='cascade',
help='Site this device belongs to')
name = fields.Char(string='Name',
help='Device name or hostname')
mac_address = fields.Char(string='MAC Address', required=True,
help='Device MAC address')
ip_address = fields.Char(string='IP Address',
help='Current IP address')
device_type = fields.Selection([
('uap', 'Access Point'),
('usw', 'Switch'),
('ugw', 'Gateway'),
('udm', 'Dream Machine'),
('client', 'Client Device'),
('other', 'Other')
], string='Device Type', required=True, default='client',
help='Type of device')
model = fields.Char(string='Model',
help='Device model name')
last_seen = fields.Datetime(string='Last Seen',
help='Last time the device was seen online')
raw_data = fields.Text(string='Raw Data',
help='Raw device data in JSON format')
# Champs calculés
# Computed fields
status = fields.Selection([
('online', 'Online'),
('offline', 'Offline'),
@ -25,8 +49,13 @@ class UdmDevice(models.Model):
@api.depends('last_seen')
def _compute_status(self):
# Calcul du statut basé sur la dernière fois où le périphérique a été vu
# Ceci est un exemple simplifié
"""Compute device online status based on last seen timestamp
Status is determined by comparing the last_seen timestamp with current time:
- Online: seen within last 10 minutes
- Offline: not seen for more than 10 minutes
- Unknown: never seen (no last_seen timestamp)
"""
for record in self:
if not record.last_seen:
record.status = 'unknown'

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
class UdmDnsConfig(models.Model):
"""Configuration DNS pour le système UniFi
Ce modèle stocke la configuration DNS pour les contrôleurs UDM/UDR.
Il gère les paramètres DNS comme les serveurs personnalisés et le filtrage de contenu.
La configuration DNS est liée à un site spécifique et est automatiquement supprimée
lorsque le site est supprimé (cascade).
"""
_name = 'udm.dns.config'
_description = 'UniFi DNS Configuration'
site_id = fields.Many2one('udm.site', string='Site', required=True,
ondelete='cascade',
help='Site this DNS configuration belongs to')
enabled = fields.Boolean(string='Enabled', default=True,
help='Enable DNS server')
filters_enabled = fields.Boolean(string='Content Filtering Enabled', default=False,
help='Enable DNS content filtering')
custom_dns = fields.Char(string='Custom DNS Servers',
help='Comma-separated list of custom DNS servers')
raw_data = fields.Text(string='Raw Data',
help='Raw DNS configuration data in JSON format')

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
class UdmDnsConfig(models.Model):
"""DNS configuration for the UDM Pro"""
_name = 'udm.dns.config'
_description = 'UDM Pro DNS Configuration'
# Site this DNS configuration belongs to
site_id = fields.Many2one('udm.site', string='Site', required=True, ondelete='cascade')
# DNS settings
enabled = fields.Boolean(string='Enabled', default=True,
help='Whether DNS service is enabled')
filters_enabled = fields.Boolean(string='Content Filtering Enabled', default=False,
help='Enable content filtering on DNS queries')
custom_dns = fields.Char(string='Custom DNS Servers',
help='Comma-separated list of custom DNS servers')
raw_data = fields.Text(string='Raw Data',
help='Raw configuration data in JSON format')

View file

@ -3,35 +3,75 @@
from odoo import models, fields, api, _
class UdmFirewallRule(models.Model):
"""Représentation d'une règle de pare-feu dans l'UDM Pro"""
_name = 'udm.firewall.rule'
_description = 'UDM Pro Firewall Rule'
"""Represents a firewall rule in the UniFi system
config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade')
name = fields.Char(string='Name', required=True)
enabled = fields.Boolean(string='Enabled', default=True)
This model stores firewall rules that control network traffic. Each rule
specifies what traffic is allowed or blocked based on various criteria such
as source/destination addresses, ports, and protocols.
Rules are linked to a specific site and are automatically deleted when
the site is deleted (cascade).
"""
_name = 'udm.firewall.rule'
_description = 'UniFi Firewall Rule'
_order = 'sequence, id'
site_id = fields.Many2one('udm.site', string='Site', required=True,
ondelete='cascade',
help='Site this firewall rule belongs to')
name = fields.Char(string='Name', required=True,
help='Rule name')
description = fields.Text(string='Description',
help='Detailed description of the rule purpose')
enabled = fields.Boolean(string='Enabled', default=True,
help='Whether this rule is active')
sequence = fields.Integer(string='Sequence', default=10,
help='Order in which rules are evaluated')
action = fields.Selection([
('accept', 'Accept'),
('drop', 'Drop'),
('reject', 'Reject')
], string='Action')
], string='Action', required=True, default='drop',
help='Action to take when rule matches')
protocol = fields.Selection([
('tcp', 'TCP'),
('udp', 'UDP'),
('icmp', 'ICMP'),
('all', 'All')
], string='Protocol')
src_address = fields.Char(string='Source Address')
dst_address = fields.Char(string='Destination Address')
src_port = fields.Char(string='Source Port')
dst_port = fields.Char(string='Destination Port')
raw_data = fields.Text(string='Raw Data')
], string='Protocol', required=True, default='all',
help='Network protocol this rule applies to')
source = fields.Char(string='Source',
help='Source network or IP address in CIDR format')
destination = fields.Char(string='Destination',
help='Destination network or IP address in CIDR format')
src_address = fields.Char(string='Source Address',
help='Legacy field, use source instead')
dst_address = fields.Char(string='Destination Address',
help='Legacy field, use destination instead')
src_port = fields.Char(string='Source Port',
help='Source port number or range (e.g. 80 or 1024-2048)')
dst_port = fields.Char(string='Destination Port',
help='Destination port number or range (e.g. 80 or 1024-2048)')
raw_data = fields.Text(string='Raw Data',
help='Raw firewall rule data in JSON format')
# Champs calculés
# Computed fields
rule_summary = fields.Char(string='Rule Summary', compute='_compute_rule_summary')
@api.depends('action', 'protocol', 'src_address', 'dst_address', 'src_port', 'dst_port')
def _compute_rule_summary(self):
"""Compute a human-readable summary of the firewall rule
This method generates a concise description of the rule's action,
protocol, and source/destination addresses and ports. The summary
is used in list views and reports to quickly understand what the
rule does without viewing all details.
Example summaries:
- ACCEPT TCP from 192.168.1.0/24:80 to 10.0.0.0/8:443
- DROP ICMP from 172.16.0.0/16 to any
- REJECT UDP from any to 192.168.2.10:53
"""
for record in self:
parts = []
@ -41,16 +81,18 @@ class UdmFirewallRule(models.Model):
if record.protocol:
parts.append(record.protocol.upper())
if record.src_address:
src = f"from {record.src_address}"
if record.src_port:
src += f":{record.src_port}"
parts.append(src)
# Use new source/destination fields if available, fall back to legacy fields
src_addr = record.source or record.src_address or 'any'
dst_addr = record.destination or record.dst_address or 'any'
if record.dst_address:
dst = f"to {record.dst_address}"
if record.dst_port:
dst += f":{record.dst_port}"
parts.append(dst)
src = f"from {src_addr}"
if record.src_port:
src += f":{record.src_port}"
parts.append(src)
record.rule_summary = ' '.join(parts) if parts else 'No details'
dst = f"to {dst_addr}"
if record.dst_port:
dst += f":{record.dst_port}"
parts.append(dst)
record.rule_summary = ' '.join(parts)

View file

@ -3,34 +3,64 @@
from odoo import models, fields, api, _
class UdmNetwork(models.Model):
"""Représentation d'un réseau dans l'UDM Pro"""
"""Represents a network in the UniFi system
This model stores network configuration for both UDM/UDR and software controllers.
Each network can be associated with a VLAN and contains DHCP configuration.
Networks are linked to a specific site and are automatically deleted when
the site is deleted (cascade).
"""
_name = 'udm.network'
_description = 'UDM Pro Network'
_description = 'UniFi Network'
config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade')
name = fields.Char(string='Name', required=True)
purpose = fields.Char(string='Purpose')
subnet = fields.Char(string='Subnet')
vlan_id_number = fields.Integer(string='VLAN ID')
vlan_id = fields.Many2one('udm.vlan', string='VLAN', compute='_compute_vlan_id', store=True)
dhcp_enabled = fields.Boolean(string='DHCP Enabled', default=False)
dhcp_start = fields.Char(string='DHCP Start')
dhcp_stop = fields.Char(string='DHCP Stop')
domain_name = fields.Char(string='Domain Name')
raw_data = fields.Text(string='Raw Data')
site_id = fields.Many2one('udm.site', string='Site', required=True, ondelete='cascade',
help='Site this network belongs to')
name = fields.Char(string='Name', required=True,
help='Network name')
purpose = fields.Selection([
('corporate', 'Corporate'),
('guest', 'Guest'),
('iot', 'IoT'),
('other', 'Other')
], string='Purpose', required=True, default='corporate',
help='Network purpose/type')
subnet = fields.Char(string='Subnet', required=True,
help='Network subnet in CIDR format (e.g. 192.168.1.0/24)')
vlan_id_number = fields.Integer(string='VLAN ID',
help='VLAN ID number (1-4094)')
vlan_id = fields.Many2one('udm.vlan', string='VLAN',
compute='_compute_vlan_id', store=True,
help='Associated VLAN configuration')
dhcp_enabled = fields.Boolean(string='DHCP Enabled', default=True,
help='Enable DHCP server for this network')
dhcp_start = fields.Char(string='DHCP Start',
help='Start of DHCP range')
dhcp_stop = fields.Char(string='DHCP Stop',
help='End of DHCP range')
domain_name = fields.Char(string='Domain Name',
help='Network domain name')
raw_data = fields.Text(string='Raw Data',
help='Raw network configuration data in JSON format')
# Statistiques
# Statistics
device_count = fields.Integer(string='Device Count', compute='_compute_device_count')
@api.depends('vlan_id_number', 'config_id')
@api.depends('vlan_id_number', 'site_id')
def _compute_vlan_id(self):
"""Compute the associated VLAN based on the VLAN ID number
This method searches for a VLAN with matching ID in the same site.
The VLAN ID is stored and updated automatically when either the
VLAN ID number or site changes.
"""
for record in self:
if not record.vlan_id_number or not record.config_id:
if not record.vlan_id_number or not record.site_id:
record.vlan_id = False
continue
vlan = self.env['udm.vlan'].search([
('config_id', '=', record.config_id.id),
('site_id', '=', record.site_id.id),
('vlan_id', '=', record.vlan_id_number)
], limit=1)
@ -38,23 +68,36 @@ class UdmNetwork(models.Model):
def _compute_device_count(self):
for record in self:
# Cette fonction serait plus précise si les périphériques étaient liés aux réseaux
# Pour l'instant, c'est juste un exemple
# This function would be more accurate if devices were linked to networks
# For now, this is just an example
record.device_count = 0
class UdmVLAN(models.Model):
"""Représentation d'un VLAN dans l'UDM Pro"""
class UdmVlan(models.Model):
"""Represents a VLAN in the UniFi system
This model stores VLAN configurations for both UDM/UDR and software controllers.
VLANs are used to segment network traffic and can be associated with multiple
networks.
VLANs are linked to a specific site and are automatically deleted when
the site is deleted (cascade).
"""
_name = 'udm.vlan'
_description = 'UDM Pro VLAN'
_description = 'UniFi VLAN'
config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade')
vlan_id = fields.Integer(string='VLAN ID', required=True)
name = fields.Char(string='Name', required=True)
enabled = fields.Boolean(string='Enabled', default=True)
raw_data = fields.Text(string='Raw Data')
site_id = fields.Many2one('udm.site', string='Site', required=True, ondelete='cascade',
help='Site this VLAN belongs to')
vlan_id = fields.Integer(string='VLAN ID', required=True,
help='VLAN ID number (1-4094)')
name = fields.Char(string='Name', required=True,
help='VLAN name')
enabled = fields.Boolean(string='Enabled', default=True,
help='VLAN status')
raw_data = fields.Text(string='Raw Data',
help='Raw VLAN configuration data in JSON format')
# Relations inverses
# Reverse relations
network_ids = fields.One2many('udm.network', 'vlan_id', string='Networks')
network_count = fields.Integer(compute='_compute_network_count')

View file

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
class UdmPortForward(models.Model):
"""Represents a port forwarding rule in the UniFi system
This model stores port forwarding rules that allow external access to
internal services. Each rule maps an external port to an internal
IP address and port.
Port forwards are linked to a specific site and are automatically deleted
when the site is deleted (cascade).
"""
_name = 'udm.port.forward'
_description = 'UniFi Port Forward'
_order = 'sequence, id'
site_id = fields.Many2one('udm.site', string='Site', required=True,
ondelete='cascade',
help='Site this port forward belongs to')
name = fields.Char(string='Name', required=True,
help='Rule name')
description = fields.Text(string='Description',
help='Detailed description of the port forward purpose')
enabled = fields.Boolean(string='Enabled', default=True,
help='Whether this rule is active')
sequence = fields.Integer(string='Sequence', default=10,
help='Order in which rules are evaluated')
protocol = fields.Selection([
('tcp', 'TCP'),
('udp', 'UDP'),
('tcp_udp', 'TCP & UDP')
], string='Protocol', required=True, default='tcp',
help='Network protocol to forward')
source = fields.Char(string='Source',
help='Source network or IP address in CIDR format')
dst_port = fields.Char(string='Destination Port', required=True,
help='External port number or range (e.g. 80 or 1024-2048)')
fwd_ip = fields.Char(string='Forward IP', required=True,
help='Internal IP address to forward to')
fwd_port = fields.Char(string='Forward Port', required=True,
help='Internal port number or range')
raw_data = fields.Text(string='Raw Data',
help='Raw port forward data in JSON format')
# Computed fields
rule_summary = fields.Char(string='Rule Summary', compute='_compute_rule_summary')
@api.depends('protocol', 'source', 'dst_port', 'fwd_ip', 'fwd_port')
def _compute_rule_summary(self):
"""Compute a human-readable summary of the port forward rule
This method generates a concise description of the rule's protocol,
source, and port mappings. The summary is used in list views and
reports to quickly understand what the rule does without viewing
all details.
Example summaries:
- TCP from any:80 to 192.168.1.100:8080
- UDP from 203.0.113.0/24:53 to 192.168.2.10:53
- TCP & UDP from any:25565 to 192.168.3.50:25565
"""
for record in self:
parts = []
if record.protocol:
parts.append(record.protocol.upper())
src = f"from {record.source or 'any'}"
if record.dst_port:
src += f":{record.dst_port}"
parts.append(src)
dst = f"to {record.fwd_ip}"
if record.fwd_port:
dst += f":{record.fwd_port}"
parts.append(dst)
record.rule_summary = ' '.join(parts)

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
class UdmRoutingConfig(models.Model):
"""Routing configuration for the UniFi system
This model stores routing configuration for both UDM/UDR controllers.
It manages routing settings such as OSPF and static routes.
Routing configuration is linked to a specific site and is automatically deleted
when the site is deleted (cascade).
"""
_name = 'udm.routing.config'
_description = 'UniFi Routing Configuration'
site_id = fields.Many2one('udm.site', string='Site', required=True,
ondelete='cascade',
help='Site this routing configuration belongs to')
ospf_enabled = fields.Boolean(string='OSPF Enabled', default=False,
help='Enable OSPF routing')
static_routes = fields.Text(string='Static Routes',
help='Comma-separated list of static routes in format: network/prefix via nexthop')
raw_data = fields.Text(string='Raw Data',
help='Raw routing configuration data in JSON format')

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
class UdmRoutingConfig(models.Model):
"""Routing configuration for the UDM Pro"""
_name = 'udm.routing.config'
_description = 'UDM Pro Routing Configuration'
# Site this routing configuration belongs to
site_id = fields.Many2one('udm.site', string='Site', required=True, ondelete='cascade')
# Routing settings
ospf_enabled = fields.Boolean(string='OSPF Enabled', default=False,
help='Enable OSPF routing protocol')
static_routes = fields.Text(string='Static Routes',
help='List of static routes in format: network/prefix via nexthop')
raw_data = fields.Text(string='Raw Data',
help='Raw configuration data in JSON format')

View file

@ -3,25 +3,90 @@
from odoo import models, fields, api, _
class UdmSettings(models.Model):
"""Configuration générale de l'UDM Pro"""
"""General configuration of the UDM Pro"""
_name = 'udm.settings'
_description = 'UDM Pro Settings'
config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade')
timezone = fields.Char(string='Timezone')
ntp_servers = fields.Char(string='NTP Servers')
dns_servers = fields.Char(string='DNS Servers')
raw_data = fields.Text(string='Raw Data')
# Basic fields
site_id = fields.Many2one('udm.site', string='Site', required=True, ondelete='cascade',
help='Site these settings belong to')
name = fields.Char(string='Name', compute='_compute_name', store=True,
help='Settings name for display')
# Champs calculés
ntp_server_list = fields.Many2many('ir.model.data', string='NTP Server List',
compute='_compute_server_lists')
# Time settings
timezone = fields.Selection([
('UTC', 'UTC'),
('America/Montreal', 'America/Montreal'),
('America/New_York', 'America/New_York'),
('America/Toronto', 'America/Toronto'),
('Europe/Paris', 'Europe/Paris')
], string='Timezone', default='America/Montreal',
help='Timezone for this site')
ntp_enabled = fields.Boolean(string='NTP Enabled', default=True,
help='Enable NTP synchronization')
ntp_servers = fields.Char(string='NTP Servers',
help='Comma-separated list of NTP servers')
# DNS settings
dns_enabled = fields.Boolean(string='DNS Enabled', default=True,
help='Enable DNS service')
dns_servers = fields.Char(string='DNS Servers',
help='Comma-separated list of DNS servers')
dns_forwarding = fields.Boolean(string='DNS Forwarding', default=True,
help='Enable DNS forwarding')
# Advanced settings
upnp_enabled = fields.Boolean(string='UPnP Enabled', default=False,
help='Enable Universal Plug and Play')
mdns_enabled = fields.Boolean(string='mDNS Enabled', default=True,
help='Enable multicast DNS')
igmp_proxy = fields.Boolean(string='IGMP Proxy', default=False,
help='Enable IGMP proxy')
# Raw data
raw_data = fields.Text(string='Raw Data',
help='Raw settings data in JSON format')
# Computed fields
ntp_server_list = fields.Many2many('ir.model.data', string='NTP Server List',
compute='_compute_server_lists',
help='List of NTP servers for display')
dns_server_list = fields.Many2many('ir.model.data', string='DNS Server List',
compute='_compute_server_lists')
compute='_compute_server_lists',
help='List of DNS servers for display')
@api.depends('site_id')
def _compute_name(self):
"""Compute a display name for the settings
The name is based on the site name and includes a timestamp to
differentiate between multiple settings records for the same site.
"""
for record in self:
if record.site_id:
record.name = f'{record.site_id.name} Settings'
else:
record.name = 'New Settings'
@api.depends('ntp_servers', 'dns_servers')
def _compute_server_lists(self):
"""Convert comma-separated server lists to Many2many fields
This method splits the NTP and DNS server strings into lists
for display in the user interface. The lists are stored in
technical fields that are not persisted to the database.
"""
for record in self:
# Conversion des chaînes de caractères en listes pour l'affichage dans l'interface
record.ntp_server_list = False # Ceci est un champ technique pour l'UI
record.dns_server_list = False # Ceci est un champ technique pour l'UI
# Convert NTP servers string to list
if record.ntp_servers:
ntp_servers = [s.strip() for s in record.ntp_servers.split(',')]
record.ntp_server_list = [(6, 0, ntp_servers)]
else:
record.ntp_server_list = False
# Convert DNS servers string to list
if record.dns_servers:
dns_servers = [s.strip() for s in record.dns_servers.split(',')]
record.dns_server_list = [(6, 0, dns_servers)]
else:
record.dns_server_list = False

View file

@ -0,0 +1,231 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError
import requests
import json
import logging
import random
from datetime import timedelta
_logger = logging.getLogger(__name__)
class UdmSite(models.Model):
"""Represents a UniFi site managed by one or more UDM Pro
This model is the central entity that groups all UniFi configurations and devices.
Each site can have multiple configurations, devices, networks, and users.
"""
_name = 'udm.site'
_description = 'UniFi Site'
_order = 'name'
_inherit = ['mail.thread']
# Basic site information
name = fields.Char(string='Name', required=True, tracking=True,
help='Site name')
site_id = fields.Char(string='Site ID',
help="Site identifier in UniFi (usually 'default')",
default='default',
readonly=True,
required=True)
description = fields.Text(string='Description', tracking=True,
help='Site description')
address = fields.Text(string='Physical Address', tracking=True,
help='Physical location of this site')
active = fields.Boolean(string='Active', default=True, tracking=True,
help='Indicates if this site is currently active')
# Controller configuration
controller_type = fields.Selection([
('udm', 'UDM/UDR'),
('software', 'Software Controller')
], string='Controller Type', required=True, default='udm', tracking=True,
help='Type of UniFi controller managing this site')
host = fields.Char(string='Host', required=True, tracking=True,
help='IP address or hostname of the controller')
port = fields.Integer(string='Port', default=443, required=True,
help='Port number (default: 443)')
username = fields.Char(string='Username', required=True, tracking=True)
password = fields.Char(string='Password', required=True)
mfa_token = fields.Char(string='MFA Token',
help='Two-factor authentication token if enabled')
# Configuration data
timestamp = fields.Datetime(string='Last Update', tracking=True)
raw_data = fields.Text(string='Raw Data',
help='Raw configuration data in JSON format')
# Related Records
configuration_ids = fields.One2many('udm.configuration', 'site_id',
string='Configurations',
help='Configurations for this site')
network_ids = fields.One2many('udm.network', 'site_id',
string='Networks',
help='Networks in this site')
vlan_ids = fields.One2many('udm.vlan', 'site_id',
string='VLANs',
help='VLANs in this site')
device_ids = fields.One2many('udm.device', 'site_id',
string='Devices',
help='Devices in this site')
user_ids = fields.One2many('udm.user', 'site_id',
string='Users',
help='Users in this site')
firewall_rule_ids = fields.One2many('udm.firewall.rule', 'site_id',
string='Firewall Rules',
help='Firewall rules for this site')
port_forward_ids = fields.One2many('udm.port.forward', 'site_id',
string='Port Forwards',
help='Port forwarding rules for this site')
dns_config_ids = fields.One2many('udm.dns.config', 'site_id',
string='DNS Configurations',
help='DNS configurations for this site')
routing_config_ids = fields.One2many('udm.routing.config', 'site_id',
string='Routing Configurations',
help='Routing configurations for this site')
# Dashboard Metrics
dashboard_metric_ids = fields.One2many('udm.dashboard.metric', 'site_id',
string='Dashboard Metrics',
help='Real-time metrics for this site')
dashboard_stat_ids = fields.One2many('udm.dashboard.stat', 'site_id',
string='Statistics',
help='Historical statistics for this site')
# Computed Fields
config_count = fields.Integer(compute='_compute_counts',
string='Configuration Count',
help='Number of configurations in this site')
device_count = fields.Integer(compute='_compute_device_count',
string='Total Devices',
help='Total number of devices in this site')
client_count = fields.Integer(compute='_compute_client_count',
string='Connected Clients',
help='Number of currently connected clients')
@api.depends('configuration_ids')
def _compute_counts(self):
"""Compute the number of configurations in this site"""
for site in self:
site.config_count = len(site.configuration_ids)
@api.depends('device_ids')
def _compute_device_count(self):
"""Compute the total number of devices in this site"""
for site in self:
site.device_count = len(site.device_ids)
@api.depends('user_ids')
def _compute_client_count(self):
"""Compute the number of connected clients"""
for site in self:
site.client_count = len(site.user_ids.filtered(lambda u: u.is_connected))
def action_view_configurations(self):
"""Open the configurations view filtered for this site"""
self.ensure_one()
return {
'name': _('Configurations'),
'type': 'ir.actions.act_window',
'res_model': 'udm.configuration',
'view_mode': 'tree,form',
'domain': [('site_id', '=', self.id)],
'context': {'default_site_id': self.id},
}
def action_view_dashboard(self):
"""Display the dashboard for this site"""
self.ensure_one()
return {
'name': _('Site Dashboard'),
'type': 'ir.actions.act_window',
'res_model': 'udm.dashboard',
'view_mode': 'kanban,form',
'domain': [('site_id', '=', self.id)],
'context': {'default_site_id': self.id},
}
def action_refresh_metrics(self):
"""Refresh the dashboard metrics and statistics for this site"""
self.ensure_one()
for config in self.configuration_ids:
config.action_update_dashboard_metrics()
_sql_constraints = [
('name_uniq', 'unique(name)',
'Site name must be unique!'),
]
@api.model_create_multi
def create(self, vals_list):
"""Override create to verify connection before saving"""
for vals in vals_list:
if not self._verify_connection(vals):
raise UserError(_('Could not connect to the UniFi controller. '
'Please verify your credentials and connection details.'))
return super(UdmSite, self).create(vals_list)
def write(self, vals):
"""Override write to verify connection if connection details change"""
if any(field in vals for field in ['host', 'port', 'username',
'password', 'mfa_token']):
for record in self:
test_vals = {**record.copy_data()[0], **vals}
if not self._verify_connection(test_vals):
raise UserError(_('Could not connect to the UniFi controller. '
'Please verify your credentials and connection details.'))
return super(UdmSite, self).write(vals)
def _verify_connection(self, vals):
"""Verify connection to the UniFi controller
Args:
vals (dict): Values to use for connection test
Returns:
bool: True if connection successful, False otherwise
"""
try:
# Build base URL based on controller type
base_url = f"https://{vals['host']}:{vals.get('port', 443)}"
if vals.get('controller_type') == 'software':
base_url += '/api'
else:
base_url += '/proxy/network'
# Disable SSL verification warning
import urllib3
urllib3.disable_warnings()
# Try to authenticate
session = requests.Session()
response = session.post(
f"{base_url}/api/auth/login",
json={
'username': vals['username'],
'password': vals['password'],
'remember': True,
'token': vals.get('mfa_token', '')
},
verify=False
)
if response.status_code == 200:
return True
_logger.error(
"Authentication failed for UniFi controller at %s: %s",
vals['host'],
response.text
)
return False
except Exception as e:
_logger.error(
"Error connecting to UniFi controller at %s: %s",
vals['host'],
str(e)
)
return False

View file

@ -3,11 +3,12 @@
from odoo import models, fields, api, _
class UdmSystemInfo(models.Model):
"""Informations système de l'UDM Pro"""
"""System information of the UDM Pro"""
_name = 'udm.system.info'
_description = 'UDM Pro System Information'
config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade')
# Site this system information belongs to
site_id = fields.Many2one('udm.site', string='Site', required=True, ondelete='cascade')
hostname = fields.Char(string='Hostname')
version = fields.Char(string='Firmware Version')
model = fields.Char(string='Model')

View file

@ -1,23 +1,105 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from datetime import timedelta
class UdmUser(models.Model):
"""Représentation d'un utilisateur dans l'UDM Pro"""
"""Represents a user in the UniFi system
This model stores information about users who have access to the UniFi
network, including their roles and permissions. Users can be administrators,
operators, or read-only viewers.
Users are linked to a specific site and are automatically deleted when
the site is deleted (cascade).
"""
_name = 'udm.user'
_description = 'UDM Pro User'
_description = 'UniFi User'
config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade')
name = fields.Char(string='Name', required=True)
email = fields.Char(string='Email')
role = fields.Char(string='Role')
enabled = fields.Boolean(string='Enabled', default=True)
raw_data = fields.Text(string='Raw Data')
# Basic fields
site_id = fields.Many2one('udm.site', string='Site', required=True,
ondelete='cascade',
help='Site this user belongs to')
name = fields.Char(string='Name', required=True,
help='User full name')
email = fields.Char(string='Email',
help='User email address')
mac_address = fields.Char(string='MAC Address',
help='Device MAC address')
ip_address = fields.Char(string='IP Address',
help='Current IP address')
network_id = fields.Many2one('udm.network', string='Network',
ondelete='set null',
help='Network this user is connected to')
# Champs calculés pour des statistiques ou filtrage
is_admin = fields.Boolean(string='Is Admin', compute='_compute_is_admin', store=True)
# Role and permissions
role = fields.Selection([
('admin', 'Administrator'),
('operator', 'Operator'),
('viewer', 'Viewer')
], string='Role', required=True, default='viewer',
help='User role and permissions level')
enabled = fields.Boolean(string='Enabled', default=True,
help='Whether the user account is active')
# Connection status
status = fields.Selection([
('connected', 'Connected'),
('disconnected', 'Disconnected'),
('idle', 'Idle')
], string='Status', default='disconnected',
help='Current connection status of the user')
last_seen = fields.Datetime(string='Last Seen',
help='Last time the user was seen online')
# Technical fields
raw_data = fields.Text(string='Raw Data',
help='Raw user data in JSON format')
# Computed fields
is_admin = fields.Boolean(string='Is Admin', compute='_compute_is_admin', store=True,
help='Whether this user has administrator privileges')
is_connected = fields.Boolean(string='Is Connected', compute='_compute_is_connected',
store=True, help='Whether this user is currently connected')
@api.depends('role')
def _compute_is_admin(self):
"""Compute whether the user has administrator privileges
This is a convenience field that makes it easy to filter for or check
if a user has admin rights without parsing the role field.
"""
for record in self:
record.is_admin = record.role and 'admin' in record.role.lower() or False
record.is_admin = record.role == 'admin'
@api.depends('status')
def _compute_is_connected(self):
"""Compute whether the user is currently connected
This is a convenience field that makes it easy to filter for or check
if a user is currently connected without parsing the status field.
"""
for record in self:
record.is_connected = record.status == 'connected'
def _update_status(self):
"""Update the user's connection status based on last seen time
This method is called periodically to update the user's status:
- If last seen within 5 minutes: Connected
- If last seen within 30 minutes: Idle
- Otherwise: Disconnected
"""
now = fields.Datetime.now()
for record in self:
if not record.last_seen:
record.status = 'disconnected'
continue
delta = now - record.last_seen
if delta <= timedelta(minutes=5):
record.status = 'connected'
elif delta <= timedelta(minutes=30):
record.status = 'idle'
else:
record.status = 'disconnected'

View file

@ -1,22 +1,33 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_udm_configuration_user,udm.configuration.user,model_udm_configuration,udm_pro_docs.group_udm_pro_user,1,0,0,0
access_udm_configuration_manager,udm.configuration.manager,model_udm_configuration,udm_pro_docs.group_udm_pro_manager,1,1,1,1
access_udm_system_info_user,udm.system.info.user,model_udm_system_info,udm_pro_docs.group_udm_pro_user,1,0,0,0
access_udm_system_info_manager,udm.system.info.manager,model_udm_system_info,udm_pro_docs.group_udm_pro_manager,1,1,1,1
access_udm_network_user,udm.network.user,model_udm_network,udm_pro_docs.group_udm_pro_user,1,0,0,0
access_udm_network_manager,udm.network.manager,model_udm_network,udm_pro_docs.group_udm_pro_manager,1,1,1,1
access_udm_vlan_user,udm.vlan.user,model_udm_vlan,udm_pro_docs.group_udm_pro_user,1,0,0,0
access_udm_vlan_manager,udm.vlan.manager,model_udm_vlan,udm_pro_docs.group_udm_pro_manager,1,1,1,1
access_udm_device_user,udm.device.user,model_udm_device,udm_pro_docs.group_udm_pro_user,1,0,0,0
access_udm_device_manager,udm.device.manager,model_udm_device,udm_pro_docs.group_udm_pro_manager,1,1,1,1
access_udm_user_user,udm.user.user,model_udm_user,udm_pro_docs.group_udm_pro_user,1,0,0,0
access_udm_user_manager,udm.user.manager,model_udm_user,udm_pro_docs.group_udm_pro_manager,1,1,1,1
access_udm_settings_user,udm.settings.user,model_udm_settings,udm_pro_docs.group_udm_pro_user,1,0,0,0
access_udm_settings_manager,udm.settings.manager,model_udm_settings,udm_pro_docs.group_udm_pro_manager,1,1,1,1
access_udm_firewall_rule_user,udm.firewall.rule.user,model_udm_firewall_rule,udm_pro_docs.group_udm_pro_user,1,0,0,0
access_udm_firewall_rule_manager,udm.firewall.rule.manager,model_udm_firewall_rule,udm_pro_docs.group_udm_pro_manager,1,1,1,1
# Nouveaux modèles pour le tableau de bord et la gestion multi-sites
access_udm_site_user,udm.site.user,model_udm_site,udm_pro_docs.group_udm_pro_user,1,0,0,0
access_udm_site_manager,udm.site.manager,model_udm_site,udm_pro_docs.group_udm_pro_manager,1,1,1,1
access_udm_dashboard_metric_user,udm.dashboard.metric.user,model_udm_dashboard_metric,udm_pro_docs.group_udm_pro_user,1,0,0,0
access_udm_dashboard_metric_manager,udm.dashboard.metric.manager,model_udm_dashboard_metric,udm_pro_docs.group_udm_pro_manager,1,1,1,1
access_udm_site_user,udm.site.user,unifi_integration.model_udm_site,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_site_manager,udm.site.manager,unifi_integration.model_udm_site,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_configuration_user,udm.configuration.user,unifi_integration.model_udm_configuration,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_configuration_manager,udm.configuration.manager,unifi_integration.model_udm_configuration,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_settings_user,udm.settings.user,unifi_integration.model_udm_settings,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_settings_manager,udm.settings.manager,unifi_integration.model_udm_settings,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_system_info_user,udm.system.info.user,unifi_integration.model_udm_system_info,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_system_info_manager,udm.system.info.manager,unifi_integration.model_udm_system_info,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_network_user,udm.network.user,unifi_integration.model_udm_network,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_network_manager,udm.network.manager,unifi_integration.model_udm_network,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_vlan_user,udm.vlan.user,unifi_integration.model_udm_vlan,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_vlan_manager,udm.vlan.manager,unifi_integration.model_udm_vlan,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_device_user,udm.device.user,unifi_integration.model_udm_device,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_device_manager,udm.device.manager,unifi_integration.model_udm_device,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_user_user,udm.user.user,unifi_integration.model_udm_user,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_user_manager,udm.user.manager,unifi_integration.model_udm_user,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_firewall_rule_user,udm.firewall.rule.user,unifi_integration.model_udm_firewall_rule,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_firewall_rule_manager,udm.firewall.rule.manager,unifi_integration.model_udm_firewall_rule,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_dashboard_metric_user,udm.dashboard.metric.user,unifi_integration.model_udm_dashboard_metric,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_dashboard_metric_manager,udm.dashboard.metric.manager,unifi_integration.model_udm_dashboard_metric,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_dashboard_stat_user,udm.dashboard.stat.user,unifi_integration.model_udm_dashboard_stat,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_dashboard_stat_manager,udm.dashboard.stat.manager,unifi_integration.model_udm_dashboard_stat,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_site_import_wizard_user,udm.site.import.wizard.user,unifi_integration.model_udm_site_import_wizard,unifi_integration.group_udm_pro_user,1,1,1,0
access_udm_site_import_wizard_manager,udm.site.import.wizard.manager,unifi_integration.model_udm_site_import_wizard,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_mfa_wizard_user,udm.mfa.wizard.user,unifi_integration.model_udm_mfa_wizard,unifi_integration.group_udm_pro_user,1,1,1,0
access_udm_mfa_wizard_manager,udm.mfa.wizard.manager,unifi_integration.model_udm_mfa_wizard,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_port_forward_user,udm.port.forward.user,unifi_integration.model_udm_port_forward,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_port_forward_manager,udm.port.forward.manager,unifi_integration.model_udm_port_forward,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_dns_config_user,udm.dns.config.user,unifi_integration.model_udm_dns_config,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_dns_config_manager,udm.dns.config.manager,unifi_integration.model_udm_dns_config,unifi_integration.group_udm_pro_manager,1,1,1,1
access_udm_routing_config_user,udm.routing.config.user,unifi_integration.model_udm_routing_config,unifi_integration.group_udm_pro_user,1,0,0,0
access_udm_routing_config_manager,udm.routing.config.manager,unifi_integration.model_udm_routing_config,unifi_integration.group_udm_pro_manager,1,1,1,1

Can't render this file because it has a wrong number of fields in line 18.

View file

@ -1,50 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!-- UDM Pro Documentation Security Groups -->
<!-- Security Groups for UDM Pro Integration -->
<!-- Defines two groups: Users (read-only) and Managers (full access) -->
<record id="module_category_udm_pro" model="ir.module.category">
<field name="name">UDM Pro Documentation</field>
<field name="description">Manage UDM Pro configurations and documentation</field>
<field name="name">UDM Pro Integration</field>
<field name="description">Manage UDM Pro configurations and network settings</field>
<field name="sequence">50</field>
</record>
<!-- User Group: can only view configurations -->
<!-- User Group: Read-only access to configurations and network data -->
<!-- Members can view settings but cannot modify them -->
<record id="group_udm_pro_user" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="module_category_udm_pro"/>
<field name="category_id" ref="unifi_integration.module_category_udm_pro"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<!-- Manager Group: can create, edit, and import configurations -->
<!-- Manager Group: Full access to all UDM Pro features -->
<!-- Members can create, edit, delete configurations and perform network operations -->
<record id="group_udm_pro_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id" ref="module_category_udm_pro"/>
<field name="implied_ids" eval="[(4, ref('group_udm_pro_user'))]"/>
<field name="category_id" ref="unifi_integration.module_category_udm_pro"/>
<field name="implied_ids" eval="[(4, ref('unifi_integration.group_udm_pro_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
</data>
<data noupdate="1">
<!-- Record Rules -->
<!-- Record-Level Access Rules -->
<!-- Define permissions for different user groups -->
<!-- Users can only view records -->
<!-- User Group Rule: Read-only access -->
<!-- Allows viewing but prevents any modifications -->
<record id="rule_udm_configuration_user" model="ir.rule">
<field name="name">UDM Pro Configuration User Access</field>
<field name="model_id" ref="model_udm_configuration"/>
<field name="model_id" ref="unifi_integration.model_udm_configuration"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_udm_pro_user'))]"/>
<field name="groups" eval="[(4, ref('unifi_integration.group_udm_pro_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
<!-- Managers can create, edit, and delete records -->
<!-- Manager Group Rule: Full access -->
<!-- Allows all operations including create, read, update, and delete -->
<record id="rule_udm_configuration_manager" model="ir.rule">
<field name="name">UDM Pro Configuration Manager Access</field>
<field name="model_id" ref="model_udm_configuration"/>
<field name="model_id" ref="unifi_integration.model_udm_configuration"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_udm_pro_manager'))]"/>
<field name="groups" eval="[(4, ref('unifi_integration.group_udm_pro_manager'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 B

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512">
<path d="M0 0 C1.27091568 -0.00226556 2.54183137 -0.00453111 3.85125965 -0.00686532 C7.34476346 -0.01292804 10.83824314 -0.01285632 14.33175099 -0.01167095 C18.1131286 -0.01146552 21.89449874 -0.01699916 25.6758728 -0.02172852 C33.07210029 -0.03000555 40.46831981 -0.03278203 47.86455155 -0.03334089 C53.88288677 -0.03381752 59.90121937 -0.03587956 65.91955376 -0.03904152 C83.01771857 -0.04784057 100.11587713 -0.05246215 117.21404422 -0.05171105 C118.59429327 -0.05165111 118.59429327 -0.05165111 120.00242615 -0.05158997 C121.38437899 -0.05152864 121.38437899 -0.05152864 122.79425006 -0.05146609 C137.71187188 -0.05105998 152.6294684 -0.0606226 167.54708291 -0.07472833 C182.89877481 -0.08912886 198.25045442 -0.09599648 213.60215324 -0.09513456 C222.20706529 -0.09480382 230.81195084 -0.09749634 239.41685677 -0.10831261 C246.74547615 -0.11746621 254.07405703 -0.11955335 261.40267969 -0.11280318 C265.13365979 -0.10956212 268.86456623 -0.10939261 272.59553909 -0.11815643 C309.54080544 -0.19929666 343.40948187 2.22050447 371.12913513 29.29386902 C393.77009446 53.01141575 399.01606619 81.45718414 398.9737854 113.29364014 C398.97718374 115.20001366 398.97718374 115.20001366 398.98065072 117.14489979 C398.98671344 120.63840359 398.98664172 124.13188327 398.98545635 127.62539113 C398.98525092 131.40676873 398.99078456 135.18813888 398.99551392 138.96951294 C399.00379095 146.36574043 399.00656743 153.76195995 399.00712629 161.15819169 C399.00760292 167.1765269 399.00966496 173.19485951 399.01282692 179.21319389 C399.02162597 196.31135871 399.02624755 213.40951726 399.02549645 230.50768436 C399.02543651 231.88793341 399.02543651 231.88793341 399.02537537 233.29606628 C399.02531405 234.67801912 399.02531405 234.67801912 399.02525149 236.0878902 C399.02484538 251.00551201 399.034408 265.92310854 399.04851373 280.84072305 C399.06291426 296.19241495 399.06978188 311.54409455 399.06891996 326.89579338 C399.06858922 335.50070543 399.07128174 344.10559098 399.08209801 352.7104969 C399.09125161 360.03911629 399.09333875 367.36769717 399.08658858 374.69631983 C399.08334752 378.42729993 399.08317801 382.15820636 399.09194183 385.88917923 C399.17308206 422.83444557 396.75328093 456.70312201 369.67991638 484.42277527 C345.96236965 507.0637346 317.51660126 512.30970633 285.68014526 512.26742554 C283.77377174 512.27082387 283.77377174 512.27082387 281.82888561 512.27429086 C278.33538181 512.28035357 274.84190213 512.28028186 271.34839427 512.27909648 C267.56701667 512.27889106 263.78564652 512.2844247 260.00427246 512.28915405 C252.60804497 512.29743109 245.21182545 512.30020757 237.81559371 512.30076643 C231.7972585 512.30124306 225.77892589 512.3033051 219.76059151 512.30646706 C202.66242669 512.31526611 185.56426814 512.31988768 168.46610104 512.31913658 C167.54593501 512.31909663 166.62576897 512.31905667 165.67771912 512.3190155 C164.75641722 512.31897462 163.83511533 512.31893374 162.8858952 512.31889162 C147.96827339 512.31848551 133.05067686 512.32804814 118.13306236 512.34215387 C102.78137045 512.3565544 87.42969085 512.36342201 72.07799202 512.36256009 C63.47307997 512.36222936 54.86819442 512.36492188 46.2632885 512.37573814 C38.93466911 512.38489174 31.60608823 512.38697888 24.27746557 512.38022872 C20.54648547 512.37698766 16.81557904 512.37681815 13.08460617 512.38558197 C-23.86066017 512.46672219 -57.72933661 510.04692106 -85.44898987 482.97355652 C-108.0899492 459.25600979 -113.33592092 430.81024139 -113.29364014 398.9737854 C-113.29590569 397.70286972 -113.29817125 396.43195403 -113.30050546 395.12252575 C-113.30656817 391.62902195 -113.30649646 388.13554226 -113.30531108 384.64203441 C-113.30510566 380.8606568 -113.3106393 377.07928666 -113.31536865 373.2979126 C-113.32364569 365.90168511 -113.32642217 358.50546559 -113.32698103 351.10923385 C-113.32745766 345.09089863 -113.3295197 339.07256603 -113.33268166 333.05423164 C-113.34148071 315.95606683 -113.34610228 298.85790827 -113.34535118 281.75974118 C-113.34531123 280.83957514 -113.34527127 279.91940911 -113.3452301 278.97135925 C-113.34518922 278.05005736 -113.34514834 277.12875547 -113.34510622 276.17953534 C-113.34470011 261.26191353 -113.35426274 246.344317 -113.36836847 231.42670249 C-113.38276899 216.07501059 -113.38963661 200.72333098 -113.38877469 185.37163216 C-113.38844396 176.76672011 -113.39113648 168.16183456 -113.40195274 159.55692863 C-113.41110634 152.22830925 -113.41319348 144.89972837 -113.40644332 137.57110571 C-113.40320226 133.84012561 -113.40303275 130.10921917 -113.41179657 126.37824631 C-113.49293679 89.43297996 -111.07313566 55.56430353 -83.99977112 27.84465027 C-60.28222439 5.20369094 -31.83645599 -0.04228079 0 0 Z " fill="#0050C5" transform="translate(113.15992736816406,-0.1337127685546875)"/>
<path d="M0 0 C21.12 0 42.24 0 64 0 C64.04898438 9.41273438 64.09796875 18.82546875 64.1484375 28.5234375 C64.18383101 34.49349867 64.21971551 40.46355023 64.2578125 46.43359375 C64.31816453 55.90541375 64.37653601 65.37721574 64.421875 74.84912109 C64.45490884 81.74839107 64.49480221 88.64758222 64.54279006 95.54676497 C64.56794231 99.1967854 64.58975625 102.8467543 64.60334396 106.49683762 C64.61867474 110.58052567 64.6493258 114.66396575 64.68115234 118.74755859 C64.68331253 119.94590332 64.68547272 121.14424805 64.68769836 122.37890625 C64.87873988 141.62110372 70.38428347 160.32199607 84.04296875 174.234375 C96.41235283 186.18617581 113.13309488 191.46547739 129.99365234 191.50634766 C146.46008424 191.13108713 161.62981619 183.838499 173.54296875 172.67578125 C187.69937896 157.25902292 191.19801217 138.56515288 191.4140625 118.13671875 C191.43342865 116.97872269 191.4527948 115.82072662 191.4727478 114.62763977 C191.53184279 110.98099806 191.57864514 107.33432357 191.625 103.6875 C191.66324444 101.1933467 191.70229977 98.69920569 191.7421875 96.20507812 C191.83898974 90.13678797 191.91911364 84.06860592 192 78 C202.56 78 213.12 78 224 78 C224 88.56 224 99.12 224 110 C234.56 110 245.12 110 256 110 C256.46628602 150.33374099 249.99851134 185.94314883 221 216 C219.34123834 217.67453436 217.67438855 219.34109116 216 221 C215.23042969 221.76699219 214.46085938 222.53398437 213.66796875 223.32421875 C199.12575944 237.17939455 180.48003919 247.16114165 161 252 C160.27135742 252.18191895 159.54271484 252.36383789 158.79199219 252.55126953 C125.5327954 260.5777138 91.54317854 255.67203142 62.08642578 238.2890625 C34.81062868 221.43717756 11.50670415 193.69497306 4 162 C0.88536857 146.99604654 -0.1605994 131.89477306 -0.11352539 116.59057617 C-0.11367142 115.32071365 -0.11381744 114.05085114 -0.1139679 112.74250793 C-0.11425142 109.30535619 -0.10842757 105.8682403 -0.10139394 102.43109679 C-0.09508542 98.82233732 -0.09455073 95.21357676 -0.09336853 91.60481262 C-0.09027711 84.79120564 -0.08209362 77.97761496 -0.07201904 71.16401511 C-0.05868414 61.94259134 -0.05334029 52.72116302 -0.04751682 43.49973202 C-0.03815194 28.99980917 -0.01821852 14.49992069 0 0 Z " fill="#EBECF1" transform="translate(128,136)"/>
<path d="M0 0 C10.56 0 21.12 0 32 0 C32 10.56 32 21.12 32 32 C21.44 32 10.88 32 0 32 C0 21.44 0 10.88 0 0 Z " fill="#FEFEFF" transform="translate(352,182)"/>
<path d="M0 0 C7.92 0 15.84 0 24 0 C24 7.92 24 15.84 24 24 C16.08 24 8.16 24 0 24 C0 16.08 0 8.16 0 0 Z " fill="#FEFEFF" transform="translate(328,158)"/>
<path d="M0 0 C5.28 0 10.56 0 16 0 C16 5.28 16 10.56 16 16 C10.72 16 5.44 16 0 16 C0 10.72 0 5.44 0 0 Z " fill="#FEFEFF" transform="translate(360,134)"/>
</svg>

After

Width:  |  Height:  |  Size: 7.4 KiB

View file

@ -122,7 +122,7 @@
</field>
</page>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
@ -172,7 +172,7 @@
</group>
<notebook>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
@ -208,7 +208,7 @@
</group>
<notebook>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
@ -245,7 +245,7 @@
</field>
</page>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
@ -280,7 +280,7 @@
</group>
<notebook>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
@ -313,7 +313,7 @@
</group>
<notebook>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
@ -340,7 +340,7 @@
</group>
<notebook>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
@ -379,7 +379,7 @@
</group>
<notebook>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>

View file

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Configuration Tree View -->
<record id="view_udm_configuration_tree" model="ir.ui.view">
<field name="name">udm.configuration.tree</field>
<!-- Configuration List View -->
<record id="view_udm_configuration_list" model="ir.ui.view">
<field name="name">udm.configuration.list</field>
<field name="model">udm.configuration</field>
<field name="type">list</field>
<field name="arch" type="xml">
<tree string="UDM Pro Configurations">
<list string="UDM Pro Configurations">
<field name="name"/>
<field name="timestamp"/>
<field name="system_info_id" optional="show"/>
@ -13,7 +14,7 @@
<field name="device_count" optional="show"/>
<field name="user_count" optional="show"/>
<field name="firewall_rule_count" optional="show"/>
</tree>
</list>
</field>
</record>
@ -21,6 +22,7 @@
<record id="view_udm_configuration_form" model="ir.ui.view">
<field name="name">udm.configuration.form</field>
<field name="model">udm.configuration</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="UDM Pro Configuration">
<sheet>
@ -38,13 +40,24 @@
<field name="firewall_rule_count" widget="statinfo" string="Firewall Rules"/>
</button>
</div>
<header>
<button name="action_import_configuration" string="Import Configuration" type="object" class="oe_highlight" invisible="not host"/>
</header>
<div class="oe_title">
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group>
<group string="Connection Settings">
<field name="host" required="1"/>
<field name="port" required="1"/>
<field name="username" required="1"/>
<field name="password" password="True" required="1"/>
<field name="mfa_token" placeholder="Code reçu par email"/>
<field name="active"/>
</group>
<group string="Configuration Info">
<field name="timestamp"/>
<field name="system_info_id"/>
<field name="settings_id"/>
@ -53,27 +66,27 @@
<notebook>
<page string="Networks" name="networks">
<field name="network_ids" nolabel="1">
<tree>
<list>
<field name="name"/>
<field name="purpose"/>
<field name="subnet"/>
<field name="vlan_id"/>
<field name="dhcp_enabled"/>
</tree>
</list>
</field>
</page>
<page string="VLANs" name="vlans">
<field name="vlan_ids" nolabel="1">
<tree>
<list>
<field name="vlan_id"/>
<field name="name"/>
<field name="enabled"/>
</tree>
</list>
</field>
</page>
<page string="Devices" name="devices">
<field name="device_ids" nolabel="1">
<tree>
<list>
<field name="name"/>
<field name="mac"/>
<field name="ip"/>
@ -81,23 +94,23 @@
<field name="model" optional="show"/>
<field name="last_seen"/>
<field name="status" widget="badge" decoration-success="status == 'online'" decoration-danger="status == 'offline'" decoration-info="status == 'unknown'"/>
</tree>
</list>
</field>
</page>
<page string="Users" name="users">
<field name="user_ids" nolabel="1">
<tree>
<list>
<field name="name"/>
<field name="email"/>
<field name="role"/>
<field name="enabled"/>
<field name="is_admin"/>
</tree>
</list>
</field>
</page>
<page string="Firewall Rules" name="firewall_rules">
<field name="firewall_rule_ids" nolabel="1">
<tree>
<list>
<field name="name"/>
<field name="enabled"/>
<field name="action"/>
@ -105,7 +118,7 @@
<field name="src_address"/>
<field name="dst_address"/>
<field name="rule_summary" optional="show"/>
</tree>
</list>
</field>
</page>
<page string="Raw Data" name="raw_data">
@ -121,6 +134,7 @@
<record id="view_udm_configuration_search" model="ir.ui.view">
<field name="name">udm.configuration.search</field>
<field name="model">udm.configuration</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Search UDM Pro Configurations">
<field name="name"/>
@ -135,4 +149,19 @@
</search>
</field>
</record>
<!-- Configuration Action Window -->
<record id="action_udm_configuration" model="ir.actions.act_window">
<field name="name">UDM Pro Configurations</field>
<field name="res_model">udm.configuration</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No configurations found
</p>
<p>
Configurations are automatically created when you refresh your UDM Pro settings.
</p>
</field>
</record>
</odoo>

View file

@ -1,18 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Dashboard Metric Tree View -->
<record id="view_udm_dashboard_metric_tree" model="ir.ui.view">
<field name="name">udm.dashboard.metric.tree</field>
<!-- Dashboard Metric List View -->
<record id="view_udm_dashboard_metric_list" model="ir.ui.view">
<field name="name">udm.dashboard.metric.list</field>
<field name="model">udm.dashboard.metric</field>
<field name="type">list</field>
<field name="arch" type="xml">
<tree string="UDM Pro Dashboard Metrics">
<field name="name"/>
<field name="value"/>
<field name="unit"/>
<field name="status" widget="badge" decoration-success="status == 'normal'" decoration-warning="status == 'warning'" decoration-danger="status == 'danger'"/>
<field name="last_update"/>
<list string="UDM Pro Dashboard Metrics">
<field name="site_id"/>
</tree>
<field name="metric_type"/>
<field name="formatted_value"/>
<field name="status" widget="badge" decoration-success="status == 'normal'" decoration-warning="status == 'warning'" decoration-danger="status == 'critical'"/>
<field name="last_update"/>
</list>
</field>
</record>
@ -20,21 +20,22 @@
<record id="view_udm_dashboard_metric_form" model="ir.ui.view">
<field name="name">udm.dashboard.metric.form</field>
<field name="model">udm.dashboard.metric</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="UDM Pro Dashboard Metric">
<sheet>
<group>
<group>
<field name="name"/>
<field name="display_name"/>
<field name="value"/>
<field name="unit"/>
<field name="site_id"/>
<field name="metric_type"/>
<field name="current_value"/>
<field name="max_value"/>
</group>
<group>
<field name="status" widget="badge" decoration-success="status == 'normal'" decoration-warning="status == 'warning'" decoration-danger="status == 'danger'"/>
<field name="description"/>
<field name="formatted_value"/>
<field name="status" widget="badge" decoration-success="status == 'normal'" decoration-warning="status == 'warning'" decoration-danger="status == 'critical'"/>
<field name="last_update"/>
<field name="site_id"/>
<field name="history_data" widget="ace" options="{'mode': 'json'}"/>
</group>
</group>
</sheet>
@ -46,16 +47,18 @@
<record id="view_udm_dashboard_metric_search" model="ir.ui.view">
<field name="name">udm.dashboard.metric.search</field>
<field name="model">udm.dashboard.metric</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Search Dashboard Metrics">
<field name="name"/>
<field name="value"/>
<field name="site_id"/>
<field name="metric_type"/>
<field name="current_value"/>
<filter string="Normal Status" name="status_normal" domain="[('status', '=', 'normal')]"/>
<filter string="Warning Status" name="status_warning" domain="[('status', '=', 'warning')]"/>
<filter string="Danger Status" name="status_danger" domain="[('status', '=', 'danger')]"/>
<filter string="Critical Status" name="status_critical" domain="[('status', '=', 'critical')]"/>
<group expand="0" string="Group By">
<filter string="Site" name="group_by_site" domain="[]" context="{'group_by': 'site_id'}"/>
<filter string="Metric Type" name="group_by_type" domain="[]" context="{'group_by': 'metric_type'}"/>
<filter string="Status" name="group_by_status" domain="[]" context="{'group_by': 'status'}"/>
</group>
</search>
@ -66,7 +69,7 @@
<record id="action_udm_dashboard_metric" model="ir.actions.act_window">
<field name="name">Dashboard Metrics</field>
<field name="res_model">udm.dashboard.metric</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No dashboard metrics found
@ -76,4 +79,88 @@
</p>
</field>
</record>
<!-- Dashboard Stat List View -->
<record id="view_udm_dashboard_stat_list" model="ir.ui.view">
<field name="name">udm.dashboard.stat.list</field>
<field name="model">udm.dashboard.stat</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="UDM Pro Dashboard Statistics">
<field name="site_id"/>
<field name="date"/>
<field name="stat_type"/>
<field name="value"/>
<field name="unit"/>
<field name="is_aggregate"/>
<field name="aggregate_type" attrs="{'invisible': [('is_aggregate', '=', False)]}"/>
</list>
</field>
</record>
<!-- Dashboard Stat Form View -->
<record id="view_udm_dashboard_stat_form" model="ir.ui.view">
<field name="name">udm.dashboard.stat.form</field>
<field name="model">udm.dashboard.stat</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="UDM Pro Dashboard Statistic">
<sheet>
<group>
<group>
<field name="site_id"/>
<field name="date"/>
<field name="stat_type"/>
<field name="value"/>
<field name="unit"/>
</group>
<group>
<field name="time_start"/>
<field name="time_end"/>
<field name="is_aggregate"/>
<field name="aggregate_type" attrs="{'invisible': [('is_aggregate', '=', False)]}"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Dashboard Stat Search View -->
<record id="view_udm_dashboard_stat_search" model="ir.ui.view">
<field name="name">udm.dashboard.stat.search</field>
<field name="model">udm.dashboard.stat</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Search Dashboard Statistics">
<field name="site_id"/>
<field name="date"/>
<field name="stat_type"/>
<field name="value"/>
<filter string="Aggregated" name="is_aggregate" domain="[('is_aggregate', '=', True)]"/>
<filter string="Last 7 Days" name="last_7_days" domain="[('date','>=', (context_today() - relativedelta(days=7)))]" help="Show statistics from the last 7 days"/>
<filter string="Last 30 Days" name="last_30_days" domain="[('date','>=', (context_today() - relativedelta(days=30)))]" help="Show statistics from the last 30 days"/>
<group expand="0" string="Group By">
<filter string="Site" name="group_by_site" domain="[]" context="{'group_by': 'site_id'}"/>
<filter string="Statistic Type" name="group_by_type" domain="[]" context="{'group_by': 'stat_type'}"/>
<filter string="Date" name="group_by_date" domain="[]" context="{'group_by': 'date:day'}"/>
</group>
</search>
</field>
</record>
<!-- Dashboard Stat Action -->
<record id="action_udm_dashboard_stat" model="ir.actions.act_window">
<field name="name">Dashboard Statistics</field>
<field name="res_model">udm.dashboard.stat</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No dashboard statistics found
</p>
<p>
Dashboard statistics are automatically created and updated when you refresh site metrics.
</p>
</field>
</record>
</odoo>

View file

@ -1,9 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Device List View -->
<record id="view_udm_device_list" model="ir.ui.view">
<field name="name">udm.device.list</field>
<field name="model">udm.device</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="UDM Pro Devices">
<field name="name"/>
<field name="mac"/>
<field name="ip"/>
<field name="device_type"/>
<field name="model" optional="show"/>
<field name="last_seen"/>
<field name="status" widget="badge" decoration-success="status == 'online'" decoration-danger="status == 'offline'" decoration-info="status == 'unknown'"/>
</list>
</field>
</record>
<!-- Device Form View -->
<record id="view_udm_device_form" model="ir.ui.view">
<field name="name">udm.device.form</field>
<field name="model">udm.device</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="UDM Pro Device">
<sheet>
@ -34,4 +53,26 @@
</form>
</field>
</record>
<!-- Device Search View -->
<record id="view_udm_device_search" model="ir.ui.view">
<field name="name">udm.device.search</field>
<field name="model">udm.device</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Search Devices">
<field name="name"/>
<field name="mac"/>
<field name="ip"/>
<field name="device_type"/>
<field name="model"/>
<filter string="Online" name="online" domain="[('status', '=', 'online')]"/>
<filter string="Offline" name="offline" domain="[('status', '=', 'offline')]"/>
<group expand="0" string="Group By">
<filter string="Device Type" name="group_by_type" domain="[]" context="{'group_by': 'device_type'}"/>
<filter string="Status" name="group_by_status" domain="[]" context="{'group_by': 'status'}"/>
<filter string="Configuration" name="group_by_config" domain="[]" context="{'group_by': 'config_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View file

@ -1,9 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Firewall Rule List View -->
<record id="view_udm_firewall_rule_list" model="ir.ui.view">
<field name="name">udm.firewall.rule.list</field>
<field name="model">udm.firewall.rule</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="UDM Pro Firewall Rules">
<field name="name"/>
<field name="enabled"/>
<field name="action"/>
<field name="protocol"/>
<field name="src_address"/>
<field name="dst_address"/>
<field name="rule_summary" optional="show"/>
</list>
</field>
</record>
<!-- Firewall Rule Form View -->
<record id="view_udm_firewall_rule_form" model="ir.ui.view">
<field name="name">udm.firewall.rule.form</field>
<field name="model">udm.firewall.rule</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="UDM Pro Firewall Rule">
<sheet>
@ -26,16 +45,39 @@
</group>
</group>
<group>
<field name="rule_summary" readonly="1" force_save="1" help="Résumé de la règle en langage naturel"/>
<field name="rule_summary" readonly="1" force_save="1" help="Natural language summary of the rule"/>
<field name="config_id"/>
</group>
<notebook>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Firewall Rule Search View -->
<record id="view_udm_firewall_rule_search" model="ir.ui.view">
<field name="name">udm.firewall.rule.search</field>
<field name="model">udm.firewall.rule</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Search Firewall Rules">
<field name="name"/>
<field name="action"/>
<field name="protocol"/>
<field name="src_address"/>
<field name="dst_address"/>
<filter string="Enabled" name="enabled" domain="[('enabled', '=', True)]"/>
<filter string="Allow" name="allow" domain="[('action', '=', 'accept')]"/>
<filter string="Drop" name="drop" domain="[('action', '=', 'drop')]"/>
<group expand="0" string="Group By">
<filter string="Action" name="group_by_action" domain="[]" context="{'group_by': 'action'}"/>
<filter string="Protocol" name="group_by_protocol" domain="[]" context="{'group_by': 'protocol'}"/>
<filter string="Configuration" name="group_by_config" domain="[]" context="{'group_by': 'config_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View file

@ -1,170 +1,256 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- UDM Pro Documentation Main Menu -->
<!-- UniFi Dream Machine Pro Integration Main Menu -->
<menuitem id="menu_udm_pro_root"
name="UDM Pro"
web_icon="udm_pro_docs,static/description/icon.png"
sequence="90"/>
name="UniFi Integration"
web_icon="unifi_integration,static/description/icon.png"
sequence="90"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- UDM Pro Main Submenu: Configurations -->
<menuitem id="menu_udm_pro_main"
name="Configurations"
parent="menu_udm_pro_root"
sequence="10"/>
sequence="10"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Import Configuration Form View -->
<record id="view_udm_pro_import_form" model="ir.ui.view">
<field name="name">udm.configuration.import.form</field>
<field name="model">udm.configuration</field>
<field name="arch" type="xml">
<form string="Import UDM Pro Configuration">
<group>
<group string="Connection Information">
<field name="site_id" required="1"/>
<field name="host" required="1" placeholder="192.168.1.1 or udm.example.com"/>
<field name="port" required="1"/>
<field name="username" required="1"/>
<field name="password" required="1" password="1"/>
</group>
<group string="Import Options">
<field name="active"/>
</group>
</group>
<footer>
<button string="Import" name="action_import_configuration" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- Import Configuration Action -->
<record id="action_udm_configuration_import" model="ir.actions.act_window">
<field name="name">Import Configuration</field>
<field name="res_model">udm.configuration</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_udm_pro_import_form"/>
<field name="target">new</field>
<field name="context">{'default_active': True}</field>
</record>
<!-- Import Configuration Menu -->
<menuitem id="menu_udm_import_config"
name="Import Configuration"
parent="menu_udm_pro_main"
action="udm_pro_docs.action_udm_pro_import"
action="action_udm_configuration_import"
sequence="5"
groups="udm_pro_docs.group_udm_pro_manager"/>
<!-- Configurations Menu Action -->
<record id="action_udm_configuration" model="ir.actions.act_window">
<field name="name">Configurations</field>
<field name="res_model">udm.configuration</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first UDM Pro configuration
</p>
<p>
You can import configurations directly from your UDM Pro device
or manually create them for documentation purposes.
</p>
</field>
</record>
groups="unifi_integration.group_udm_pro_manager"/>
<menuitem id="menu_udm_configuration"
name="Configurations"
parent="menu_udm_pro_main"
action="action_udm_configuration"
sequence="10"/>
action="unifi_integration.action_udm_configuration"
sequence="10"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Network Components Menu -->
<menuitem id="menu_udm_pro_network"
name="Network"
parent="menu_udm_pro_root"
sequence="20"/>
sequence="20"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Networks Menu Action -->
<record id="action_udm_network" model="ir.actions.act_window">
<field name="name">Networks</field>
<field name="res_model">udm.network</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_group_by_config_id': 1}</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_group_by_config_id': 1, 'search_default_active': 1}</field>
</record>
<menuitem id="menu_udm_network"
name="Networks"
parent="menu_udm_pro_network"
action="action_udm_network"
sequence="10"/>
sequence="10"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- VLANs Menu Action -->
<record id="action_udm_vlan" model="ir.actions.act_window">
<field name="name">VLANs</field>
<field name="res_model">udm.vlan</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_group_by_config_id': 1}</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_group_by_config_id': 1, 'search_default_active': 1}</field>
</record>
<menuitem id="menu_udm_vlan"
name="VLANs"
parent="menu_udm_pro_network"
action="action_udm_vlan"
sequence="20"/>
sequence="20"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Devices Menu -->
<menuitem id="menu_udm_pro_devices"
name="Devices"
parent="menu_udm_pro_root"
sequence="30"/>
sequence="30"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Devices Menu Action -->
<record id="action_udm_device" model="ir.actions.act_window">
<field name="name">Devices</field>
<field name="res_model">udm.device</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_group_by_config_id': 1}</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_group_by_config_id': 1, 'search_default_active': 1}</field>
</record>
<menuitem id="menu_udm_device"
name="Devices"
parent="menu_udm_pro_devices"
action="action_udm_device"
sequence="10"/>
sequence="10"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Security Menu -->
<menuitem id="menu_udm_pro_security"
name="Security"
parent="menu_udm_pro_root"
sequence="40"/>
sequence="40"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- UDM Users Menu Action -->
<record id="action_udm_user" model="ir.actions.act_window">
<field name="name">Users</field>
<field name="res_model">udm.user</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_group_by_config_id': 1}</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_group_by_config_id': 1, 'search_default_active': 1}</field>
</record>
<menuitem id="menu_udm_user"
name="Users"
parent="menu_udm_pro_security"
action="action_udm_user"
sequence="10"/>
sequence="10"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Firewall Rules Menu Action -->
<record id="action_udm_firewall_rule" model="ir.actions.act_window">
<field name="name">Firewall Rules</field>
<field name="res_model">udm.firewall.rule</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_group_by_config_id': 1}</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_group_by_config_id': 1, 'search_default_active': 1}</field>
</record>
<menuitem id="menu_udm_firewall_rule"
name="Firewall Rules"
parent="menu_udm_pro_security"
action="action_udm_firewall_rule"
sequence="20"/>
sequence="20"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Dashboard Menu -->
<menuitem id="menu_udm_pro_dashboard"
name="Dashboard"
parent="menu_udm_pro_root"
sequence="5"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Dashboard Metrics Menu Action -->
<record id="action_udm_dashboard_metric" model="ir.actions.act_window">
<field name="name">Dashboard Metrics</field>
<field name="res_model">udm.dashboard.metric</field>
<field name="view_mode">list,form</field>
<field name="context">{}</field>
</record>
<menuitem id="menu_udm_dashboard_metric"
name="Metrics"
parent="menu_udm_pro_dashboard"
action="action_udm_dashboard_metric"
sequence="10"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Dashboard Stats Menu Action -->
<record id="action_udm_dashboard_stat" model="ir.actions.act_window">
<field name="name">Dashboard Statistics</field>
<field name="res_model">udm.dashboard.stat</field>
<field name="view_mode">list,form</field>
<field name="context">{}</field>
</record>
<menuitem id="menu_udm_dashboard_stat"
name="Statistics"
parent="menu_udm_pro_dashboard"
action="action_udm_dashboard_stat"
sequence="20"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Sites Menu Action -->
<record id="action_udm_site" model="ir.actions.act_window">
<field name="name">Sites</field>
<field name="res_model">udm.site</field>
<field name="view_mode">list,form</field>
<field name="context">{}</field>
</record>
<menuitem id="menu_udm_site"
name="Sites"
parent="menu_udm_pro_network"
action="action_udm_site"
sequence="5"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Settings Menu -->
<menuitem id="menu_udm_pro_settings"
name="Settings"
parent="menu_udm_pro_root"
sequence="50"/>
sequence="50"
groups="unifi_integration.group_udm_pro_manager"/>
<!-- System Info Menu Action -->
<record id="action_udm_system_info" model="ir.actions.act_window">
<field name="name">System Info</field>
<field name="res_model">udm.system.info</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_udm_system_info"
name="System Info"
parent="menu_udm_pro_settings"
action="action_udm_system_info"
sequence="10"/>
sequence="10"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- UDM Settings Menu Action -->
<record id="action_udm_settings" model="ir.actions.act_window">
<field name="name">UDM Settings</field>
<field name="name">UniFi Settings</field>
<field name="res_model">udm.settings</field>
<field name="view_mode">tree,form</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_udm_settings"
name="UDM Settings"
name="UniFi Settings"
parent="menu_udm_pro_settings"
action="action_udm_settings"
sequence="20"/>
sequence="20"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<!-- Import URL Action -->
<record id="action_udm_pro_import" model="ir.actions.act_url">
<field name="name">Import UDM Pro Configuration</field>
<field name="name">Import UniFi Configuration</field>
<field name="url">/udm_pro/import_config</field>
<field name="target">self</field>
</record>

View file

@ -0,0 +1,158 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Port Forward Views -->
<record id="view_udm_port_forward_list" model="ir.ui.view">
<field name="name">udm.port.forward.list</field>
<field name="model">udm.port.forward</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="enabled"/>
<field name="src_port"/>
<field name="dst_port"/>
<field name="protocol"/>
<field name="dst_address"/>
</list>
</field>
</record>
<record id="view_udm_port_forward_form" model="ir.ui.view">
<field name="name">udm.port.forward.form</field>
<field name="model">udm.port.forward</field>
<field name="arch" type="xml">
<form string="Port Forward">
<sheet>
<group>
<group string="Identification">
<field name="name"/>
<field name="enabled"/>
<field name="protocol"/>
</group>
<group string="Redirection">
<field name="src_port" string="Port source"/>
<field name="dst_port" string="Port de destination"/>
<field name="dst_address" string="Adresse de destination"/>
</group>
</group>
<group string="Technical">
<field name="raw_data" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- DNS Configuration Views -->
<record id="view_udm_dns_config_list" model="ir.ui.view">
<field name="name">udm.dns.config.list</field>
<field name="model">udm.dns.config</field>
<field name="arch" type="xml">
<list>
<field name="enabled"/>
<field name="filters_enabled"/>
<field name="custom_dns"/>
</list>
</field>
</record>
<record id="view_udm_dns_config_form" model="ir.ui.view">
<field name="name">udm.dns.config.form</field>
<field name="model">udm.dns.config</field>
<field name="arch" type="xml">
<form string="DNS Configuration">
<sheet>
<group>
<group string="État">
<field name="enabled" string="DNS activé"/>
<field name="filters_enabled" string="Filtrage activé"/>
</group>
<group string="Configuration">
<field name="custom_dns" string="Serveurs DNS personnalisés"/>
</group>
</group>
<group string="Technical">
<field name="raw_data" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- Routing Configuration Views -->
<record id="view_udm_routing_config_list" model="ir.ui.view">
<field name="name">udm.routing.config.list</field>
<field name="model">udm.routing.config</field>
<field name="arch" type="xml">
<list>
<field name="ospf_enabled"/>
<field name="static_routes"/>
</list>
</field>
</record>
<record id="view_udm_routing_config_form" model="ir.ui.view">
<field name="name">udm.routing.config.form</field>
<field name="model">udm.routing.config</field>
<field name="arch" type="xml">
<form string="Routing Configuration">
<sheet>
<group>
<group string="Protocoles de routage">
<field name="ospf_enabled" string="OSPF activé"/>
</group>
<group string="Routes">
<field name="static_routes" string="Routes statiques"/>
</group>
</group>
<group string="Technical">
<field name="raw_data" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- Actions -->
<record id="action_udm_port_forward" model="ir.actions.act_window">
<field name="name">Port Forwards</field>
<field name="res_model">udm.port.forward</field>
<field name="view_mode">list,form</field>
<field name="view_id" ref="view_udm_port_forward_list"/>
</record>
<record id="action_udm_dns_config" model="ir.actions.act_window">
<field name="name">DNS Configuration</field>
<field name="res_model">udm.dns.config</field>
<field name="view_mode">list,form</field>
<field name="view_id" ref="view_udm_dns_config_list"/>
</record>
<record id="action_udm_routing_config" model="ir.actions.act_window">
<field name="name">Routing Configuration</field>
<field name="res_model">udm.routing.config</field>
<field name="view_mode">list,form</field>
<field name="view_id" ref="view_udm_routing_config_list"/>
</record>
<!-- Menu Items -->
<menuitem id="menu_udm_port_forward"
name="Port Forwards"
parent="menu_udm_pro_security"
action="action_udm_port_forward"
sequence="30"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<menuitem id="menu_udm_dns_config"
name="DNS Configuration"
parent="menu_udm_pro_security"
action="action_udm_dns_config"
sequence="40"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
<menuitem id="menu_udm_routing_config"
name="Routing Configuration"
parent="menu_udm_pro_security"
action="action_udm_routing_config"
sequence="50"
groups="unifi_integration.group_udm_pro_user,unifi_integration.group_udm_pro_manager"/>
</odoo>

View file

@ -1,9 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Network List View -->
<record id="view_udm_network_list" model="ir.ui.view">
<field name="name">udm.network.list</field>
<field name="model">udm.network</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="UDM Pro Networks">
<field name="name"/>
<field name="purpose"/>
<field name="subnet"/>
<field name="vlan_id_number" optional="show"/>
<field name="vlan_id" optional="show"/>
<field name="dhcp_enabled"/>
<field name="domain_name" optional="show"/>
</list>
</field>
</record>
<!-- Network Form View -->
<record id="view_udm_network_form" model="ir.ui.view">
<field name="name">udm.network.form</field>
<field name="model">udm.network</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="UDM Pro Network">
<sheet>
@ -28,11 +47,30 @@
</group>
<notebook>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Network Search View -->
<record id="view_udm_network_search" model="ir.ui.view">
<field name="name">udm.network.search</field>
<field name="model">udm.network</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Search Networks">
<field name="name"/>
<field name="purpose"/>
<field name="subnet"/>
<field name="vlan_id"/>
<filter string="DHCP Enabled" name="dhcp_enabled" domain="[('dhcp_enabled', '=', True)]"/>
<group expand="0" string="Group By">
<filter string="Purpose" name="group_by_purpose" domain="[]" context="{'group_by': 'purpose'}"/>
<filter string="VLAN" name="group_by_vlan" domain="[]" context="{'group_by': 'vlan_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View file

@ -0,0 +1,222 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Advanced Options Form Template -->
<template id="advanced_options_form">
<t t-call="web.layout">
<t t-set="title">Advanced Options</t>
<div class="container">
<h1>Advanced UDM Pro Options</h1>
<form method="post" t-att-action="'/udm_pro/advanced_options'">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group">
<label for="site">Site</label>
<input type="text" class="form-control" name="site" t-att-value="default_site"/>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="fixed_only" name="fixed_only" t-att-checked="fixed_only"/>
<label class="custom-control-label" for="fixed_only">Fixed IP Only</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="lowercase_hostnames" name="lowercase_hostnames" t-att-checked="lowercase_hostnames"/>
<label class="custom-control-label" for="lowercase_hostnames">Lowercase Hostnames</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Apply</button>
</form>
</div>
</t>
</template>
<!-- Restart Device Form Template -->
<template id="restart_device_form">
<t t-call="web.layout">
<t t-set="title">Restart Device</t>
<div class="container">
<h1>Restart UDM Pro Device</h1>
<form method="post" t-att-action="'/udm_pro/restart_device'">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group">
<label for="config_id">Configuration</label>
<select class="form-control" name="config_id" required="required">
<option value="">Select a Configuration</option>
<t t-foreach="configs" t-as="config">
<option t-att-value="config.id">
<t t-esc="config.name"/>
</option>
</t>
</select>
</div>
<div class="form-group">
<label for="mac_address">Device MAC Address</label>
<input type="text" class="form-control" name="mac_address" required="required" placeholder="00:11:22:33:44:55"/>
</div>
<button type="submit" class="btn btn-primary">Restart Device</button>
</form>
</div>
</t>
</template>
<!-- Generate Hosts Form Template -->
<template id="generate_hosts_form">
<t t-call="web.layout">
<t t-set="title">Generate Hosts File</t>
<div class="container">
<h1>Generate Hosts File from UDM Pro</h1>
<form method="post" t-att-action="'/udm_pro/generate_hosts'">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group">
<label for="config_id">Configuration</label>
<select class="form-control" name="config_id" required="required">
<option value="">Select a Configuration</option>
<t t-foreach="configs" t-as="config">
<option t-att-value="config.id">
<t t-esc="config.name"/>
</option>
</t>
</select>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="fixed_only" name="fixed_only" t-att-checked="fixed_only"/>
<label class="custom-control-label" for="fixed_only">Fixed IP Only</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="lowercase_hostnames" name="lowercase_hostnames" t-att-checked="lowercase_hostnames"/>
<label class="custom-control-label" for="lowercase_hostnames">Lowercase Hostnames</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Generate Hosts File</button>
</form>
</div>
</t>
</template>
<!-- Network Clients Form Template -->
<template id="network_clients_form">
<t t-call="web.layout">
<t t-set="title">Network Clients</t>
<div class="container">
<h1>UDM Pro Network Clients</h1>
<form method="post" t-att-action="'/udm_pro/get_network_clients'">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group">
<label for="config_id">Configuration</label>
<select class="form-control" name="config_id" required="required">
<option value="">Select a Configuration</option>
<t t-foreach="configs" t-as="config">
<option t-att-value="config.id">
<t t-esc="config.name"/>
</option>
</t>
</select>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="fixed_only" name="fixed_only" t-att-checked="fixed_only"/>
<label class="custom-control-label" for="fixed_only">Fixed IP Only</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="lowercase_hostnames" name="lowercase_hostnames" t-att-checked="lowercase_hostnames"/>
<label class="custom-control-label" for="lowercase_hostnames">Lowercase Hostnames</label>
</div>
</div>
<button type="submit" class="btn btn-primary">Get Network Clients</button>
</form>
</div>
</t>
</template>
<!-- Network Clients Result Template -->
<template id="network_clients_result">
<t t-call="web.layout">
<t t-set="title">Network Clients Result</t>
<div class="container">
<h1>UDM Pro Network Clients</h1>
<div t-if="error_message" class="alert alert-danger" role="alert">
<t t-esc="error_message"/>
</div>
<div t-if="clients" class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Hostname</th>
<th>IP Address</th>
<th>MAC Address</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<t t-foreach="clients" t-as="client">
<tr>
<td><t t-esc="client.get('hostname', '')"/></td>
<td><t t-esc="client.get('ip', '')"/></td>
<td><t t-esc="client.get('mac', '')"/></td>
<td><t t-esc="client.get('status', '')"/></td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
</t>
</template>
<!-- Import Config Form Template -->
<template id="import_config_form">
<t t-call="web.layout">
<t t-set="title">Import Configuration</t>
<div class="container">
<h1>Import UDM Pro Configuration</h1>
<form method="post" t-att-action="'/udm_pro/import_config'">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group">
<label for="host">Host</label>
<input type="text" class="form-control" name="host" required="required" t-att-value="host or ''"/>
</div>
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" name="username" required="required" t-att-value="username or ''"/>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" required="required"/>
</div>
<div class="form-group">
<label for="port">Port</label>
<input type="number" class="form-control" name="port" value="443"/>
</div>
<button type="submit" class="btn btn-primary">Import Configuration</button>
</form>
</div>
</t>
</template>
<!-- Restart Success Template -->
<template id="restart_success">
<t t-call="web.layout">
<t t-set="title">Restart Success</t>
<div class="container">
<h1>Device Restart Successful</h1>
<p>The device has been successfully restarted.</p>
</div>
</t>
</template>
<!-- Access Denied Template -->
<template id="access_denied">
<t t-call="web.layout">
<t t-set="title">Access Denied</t>
<div class="container">
<h1>Access Denied</h1>
<p>You do not have permission to access this page.</p>
</div>
</t>
</template>
</odoo>

View file

@ -1,9 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Settings List View -->
<record id="view_udm_settings_list" model="ir.ui.view">
<field name="name">udm.settings.list</field>
<field name="model">udm.settings</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="UDM Pro Settings">
<field name="timezone"/>
<field name="ntp_servers"/>
<field name="dns_servers"/>
<field name="config_id" optional="show"/>
</list>
</field>
</record>
<!-- Settings Form View -->
<record id="view_udm_settings_form" model="ir.ui.view">
<field name="name">udm.settings.form</field>
<field name="model">udm.settings</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="UDM Pro Settings">
<sheet>
@ -19,11 +35,28 @@
</group>
<notebook>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Settings Search View -->
<record id="view_udm_settings_search" model="ir.ui.view">
<field name="name">udm.settings.search</field>
<field name="model">udm.settings</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Search Settings">
<field name="timezone"/>
<field name="ntp_servers"/>
<field name="dns_servers"/>
<group expand="0" string="Group By">
<filter string="Timezone" name="group_by_timezone" domain="[]" context="{'group_by': 'timezone'}"/>
<filter string="Configuration" name="group_by_config" domain="[]" context="{'group_by': 'config_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View file

@ -1,16 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- UDM Site Tree View -->
<record id="view_udm_site_tree" model="ir.ui.view">
<field name="name">udm.site.tree</field>
<field name="model">udm.site</field>
<!-- Import Site Action -->
<record id="action_import_udm_site" model="ir.actions.act_window">
<field name="name">Import UDM Pro Site</field>
<field name="res_model">udm.site.import.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- Import Site Wizard Form View -->
<record id="view_udm_site_import_wizard_form" model="ir.ui.view">
<field name="name">udm.site.import.wizard.form</field>
<field name="model">udm.site.import.wizard</field>
<field name="arch" type="xml">
<tree string="UDM Pro Sites">
<form string="Import UDM Pro Site">
<sheet>
<group>
<group>
<field name="name" placeholder="e.g., Bureau Principal"/>
<field name="site_id"/>
</group>
<group>
<field name="host" placeholder="e.g., 192.168.1.1"/>
<field name="port"/>
</group>
<group>
<field name="username" placeholder="Admin username"/>
<field name="password" password="True" placeholder="Admin password"/>
</group>
</group>
</sheet>
<footer>
<button name="action_import_site" string="Import" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- UDM Site List View -->
<record id="view_udm_site_list" model="ir.ui.view">
<field name="name">udm.site.list</field>
<field name="model">udm.site</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="UDM Pro Sites">
<header>
<button name="%(action_import_udm_site)d" string="Import UDM Pro Site" type="action" class="btn-primary"/>
</header>
<field name="name"/>
<field name="site_id"/>
<field name="configuration_count"/>
<field name="active"/>
</tree>
<field name="create_date" optional="show"/>
</list>
</field>
</record>
@ -18,6 +60,7 @@
<record id="view_udm_site_form" model="ir.ui.view">
<field name="name">udm.site.form</field>
<field name="model">udm.site</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="UDM Pro Site">
<header>
@ -27,7 +70,9 @@
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_configurations" type="object" class="oe_stat_button" icon="fa-cog">
<field name="configuration_count" widget="statinfo" string="Configurations"/>
<div class="o_stat_info">
<span class="o_stat_text">Configurations</span>
</div>
</button>
</div>
<div class="oe_title">
@ -42,7 +87,7 @@
</group>
<group>
<field name="create_date" readonly="1"/>
<field name="last_refresh" readonly="1"/>
</group>
</group>
<notebook>
@ -58,14 +103,25 @@
<page string="Dashboard Metrics" name="dashboard_metrics">
<field name="dashboard_ids">
<tree>
<field name="name"/>
<field name="value"/>
<field name="unit"/>
<field name="metric_type"/>
<field name="formatted_value"/>
<field name="status"/>
<field name="last_update"/>
</tree>
</field>
</page>
<page string="Statistics" name="statistics">
<field name="statistic_ids">
<tree>
<field name="date"/>
<field name="stat_type"/>
<field name="value"/>
<field name="unit"/>
<field name="is_aggregate"/>
<field name="aggregate_type" invisible="not is_aggregate"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
@ -78,7 +134,7 @@
<field name="model">udm.site</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<dashboard string="UDM Pro Dashboard" sample="1">
<form string="UDM Pro Dashboard" create="false" edit="false" delete="false">
<div class="d-flex flex-column">
<!-- Dashboard Header -->
<div class="d-flex justify-content-between align-items-center pb-2 mb-3 border-bottom">
@ -103,16 +159,15 @@
<i class="fa fa-microchip"></i>
</div>
<div class="card-body d-flex flex-column align-items-center justify-content-center text-center">
<t t-set="cpu_metric" t-value="dashboard_ids.filtered(lambda m: m.name == 'cpu_usage')"/>
<h3 class="mb-0" t-att-class="cpu_metric and cpu_metric.status == 'warning' and 'text-warning' or cpu_metric.status == 'danger' and 'text-danger' or ''">
<t t-esc="cpu_metric and cpu_metric.value or '0'"/>
<small><t t-esc="cpu_metric and cpu_metric.unit or '%'"/></small>
<t t-set="cpu_metric" t-value="dashboard_ids.filtered(lambda m: m.metric_type == 'cpu_usage')"/>
<h3 class="mb-0" t-att-class="cpu_metric and cpu_metric.status == 'warning' and 'text-warning' or cpu_metric.status == 'critical' and 'text-danger' or ''">
<t t-esc="cpu_metric and cpu_metric.formatted_value or '0%'"/>
</h3>
<div class="progress w-75 mt-2">
<div class="progress-bar" role="progressbar"
t-att-style="'width: ' + (cpu_metric and str(cpu_metric.value) or '0') + '%;'"
t-att-class="cpu_metric and cpu_metric.status == 'warning' and 'bg-warning' or cpu_metric.status == 'danger' and 'bg-danger' or 'bg-success'"
t-att-aria-valuenow="cpu_metric and cpu_metric.value or 0" aria-valuemin="0" aria-valuemax="100">
t-att-style="'width: ' + (cpu_metric and cpu_metric.current_value or '0') + '%;'"
t-att-class="cpu_metric and cpu_metric.status == 'warning' and 'bg-warning' or cpu_metric.status == 'critical' and 'bg-danger' or 'bg-success'"
t-att-aria-valuenow="cpu_metric and cpu_metric.current_value or 0" aria-valuemin="0" aria-valuemax="100">
</div>
</div>
</div>
@ -127,16 +182,15 @@
<i class="fa fa-memory"></i>
</div>
<div class="card-body d-flex flex-column align-items-center justify-content-center text-center">
<t t-set="memory_metric" t-value="dashboard_ids.filtered(lambda m: m.name == 'memory_usage')"/>
<h3 class="mb-0" t-att-class="memory_metric and memory_metric.status == 'warning' and 'text-warning' or memory_metric.status == 'danger' and 'text-danger' or ''">
<t t-esc="memory_metric and memory_metric.value or '0'"/>
<small><t t-esc="memory_metric and memory_metric.unit or '%'"/></small>
<t t-set="memory_metric" t-value="dashboard_ids.filtered(lambda m: m.metric_type == 'memory_usage')"/>
<h3 class="mb-0" t-att-class="memory_metric and memory_metric.status == 'warning' and 'text-warning' or memory_metric.status == 'critical' and 'text-danger' or ''">
<t t-esc="memory_metric and memory_metric.formatted_value or '0%'"/>
</h3>
<div class="progress w-75 mt-2">
<div class="progress-bar" role="progressbar"
t-att-style="'width: ' + (memory_metric and str(memory_metric.value) or '0') + '%;'"
t-att-class="memory_metric and memory_metric.status == 'warning' and 'bg-warning' or memory_metric.status == 'danger' and 'bg-danger' or 'bg-success'"
t-att-aria-valuenow="memory_metric and memory_metric.value or 0" aria-valuemin="0" aria-valuemax="100">
t-att-style="'width: ' + (memory_metric and memory_metric.current_value or '0') + '%;'"
t-att-class="memory_metric and memory_metric.status == 'warning' and 'bg-warning' or memory_metric.status == 'critical' and 'bg-danger' or 'bg-success'"
t-att-aria-valuenow="memory_metric and memory_metric.current_value or 0" aria-valuemin="0" aria-valuemax="100">
</div>
</div>
</div>
@ -151,10 +205,9 @@
<i class="fa fa-network-wired"></i>
</div>
<div class="card-body d-flex flex-column align-items-center justify-content-center text-center">
<t t-set="bandwidth_metric" t-value="dashboard_ids.filtered(lambda m: m.name == 'bandwidth_usage')"/>
<h3 class="mb-0" t-att-class="bandwidth_metric and bandwidth_metric.status == 'warning' and 'text-warning' or bandwidth_metric.status == 'danger' and 'text-danger' or ''">
<t t-esc="bandwidth_metric and bandwidth_metric.value or '0'"/>
<small><t t-esc="bandwidth_metric and bandwidth_metric.unit or 'Mbps'"/></small>
<t t-set="bandwidth_metric" t-value="dashboard_ids.filtered(lambda m: m.metric_type == 'bandwidth_usage')"/>
<h3 class="mb-0" t-att-class="bandwidth_metric and bandwidth_metric.status == 'warning' and 'text-warning' or bandwidth_metric.status == 'critical' and 'text-danger' or ''">
<t t-esc="bandwidth_metric and bandwidth_metric.formatted_value or '0 Mbps'"/>
</h3>
<div class="text-muted">Current Network Load</div>
</div>
@ -235,6 +288,7 @@
<!-- UDM Site Search View -->
<record id="view_udm_site_search" model="ir.ui.view">
<field name="name">udm.site.search</field>
<field name="type">search</field>
<field name="model">udm.site</field>
<field name="arch" type="xml">
<search string="Search UDM Pro Sites">

View file

@ -1,9 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- System Info List View -->
<record id="view_udm_system_info_list" model="ir.ui.view">
<field name="name">udm.system.info.list</field>
<field name="model">udm.system.info</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="UDM Pro System Information">
<field name="hostname"/>
<field name="version"/>
<field name="model"/>
<field name="serial" optional="show"/>
<field name="mac_address" optional="show"/>
<field name="uptime_human"/>
</list>
</field>
</record>
<!-- System Info Form View -->
<record id="view_udm_system_info_form" model="ir.ui.view">
<field name="name">udm.system.info.form</field>
<field name="model">udm.system.info</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="UDM Pro System Information">
<sheet>
@ -23,11 +41,31 @@
</group>
<notebook>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- System Info Search View -->
<record id="view_udm_system_info_search" model="ir.ui.view">
<field name="name">udm.system.info.search</field>
<field name="model">udm.system.info</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Search System Information">
<field name="hostname"/>
<field name="version"/>
<field name="model"/>
<field name="serial"/>
<field name="mac_address"/>
<group expand="0" string="Group By">
<filter string="Model" name="group_by_model" domain="[]" context="{'group_by': 'model'}"/>
<filter string="Version" name="group_by_version" domain="[]" context="{'group_by': 'version'}"/>
<filter string="Configuration" name="group_by_config" domain="[]" context="{'group_by': 'config_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View file

@ -1,9 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- User List View -->
<record id="view_udm_user_list" model="ir.ui.view">
<field name="name">udm.user.list</field>
<field name="model">udm.user</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="UDM Pro Users">
<field name="name"/>
<field name="email"/>
<field name="role"/>
<field name="enabled"/>
<field name="is_admin"/>
<field name="config_id" optional="show"/>
</list>
</field>
</record>
<!-- User Form View -->
<record id="view_udm_user_form" model="ir.ui.view">
<field name="name">udm.user.form</field>
<field name="model">udm.user</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="UDM Pro User">
<sheet>
@ -25,11 +43,30 @@
</group>
<notebook>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- User Search View -->
<record id="view_udm_user_search" model="ir.ui.view">
<field name="name">udm.user.search</field>
<field name="model">udm.user</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Search Users">
<field name="name"/>
<field name="email"/>
<field name="role"/>
<filter string="Enabled" name="enabled" domain="[('enabled', '=', True)]"/>
<filter string="Administrators" name="is_admin" domain="[('is_admin', '=', True)]"/>
<group expand="0" string="Group By">
<filter string="Role" name="group_by_role" domain="[]" context="{'group_by': 'role'}"/>
<filter string="Configuration" name="group_by_config" domain="[]" context="{'group_by': 'config_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View file

@ -1,9 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- VLAN List View -->
<record id="view_udm_vlan_list" model="ir.ui.view">
<field name="name">udm.vlan.list</field>
<field name="model">udm.vlan</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="UDM Pro VLANs">
<field name="vlan_id"/>
<field name="name"/>
<field name="enabled"/>
<field name="config_id" optional="show"/>
<field name="network_count" optional="show"/>
</list>
</field>
</record>
<!-- VLAN Form View -->
<record id="view_udm_vlan_form" model="ir.ui.view">
<field name="name">udm.vlan.form</field>
<field name="model">udm.vlan</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="UDM Pro VLAN">
<sheet>
@ -21,19 +38,35 @@
<notebook>
<page string="Networks" name="networks">
<field name="network_ids" nolabel="1">
<tree>
<list>
<field name="name"/>
<field name="purpose"/>
<field name="subnet"/>
</tree>
</list>
</field>
</page>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Données JSON brutes de la configuration"/>
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- VLAN Search View -->
<record id="view_udm_vlan_search" model="ir.ui.view">
<field name="name">udm.vlan.search</field>
<field name="model">udm.vlan</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Search VLANs">
<field name="vlan_id"/>
<field name="name"/>
<filter string="Enabled" name="enabled" domain="[('enabled', '=', True)]"/>
<group expand="0" string="Group By">
<filter string="Configuration" name="group_by_config" domain="[]" context="{'group_by': 'config_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View file

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import udm_site_import_wizard
from . import udm_mfa_wizard

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class UdmMfaWizard(models.TransientModel):
"""Assistant pour entrer le code MFA"""
_name = 'udm.mfa.wizard'
_description = 'Assistant Code MFA'
mfa_code = fields.Char(string='Code MFA', required=True, help="Entrez le code d'authentification reçu par courriel")
config_id = fields.Many2one('udm.configuration', string='Configuration')
def action_validate_mfa(self):
"""Valide le code MFA et continue l'importation"""
self.ensure_one()
if not self.config_id:
raise UserError(_("Configuration non trouvée"))
# Met à jour le code MFA dans la configuration
self.config_id.write({'mfa_token': self.mfa_code})
# Relance l'importation
return self.config_id.action_import_configuration()

View file

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import UserError
import requests
import urllib3
from requests.exceptions import RequestException
class UdmSiteImportWizard(models.TransientModel):
"""Wizard to create a new site from a UDM Pro device"""
_name = 'udm.site.import.wizard'
_description = 'Import UDM Pro Site Wizard'
name = fields.Char(string='Site Name', required=True)
host = fields.Char(string='UDM Pro IP/Hostname', required=True)
port = fields.Integer(string='Port', default=443)
username = fields.Char(string='Username', required=True)
password = fields.Char(string='Password', required=True)
site_id = fields.Char(string='Site ID', default='default',
help="Site identifier in UniFi (usually 'default' unless configured otherwise)")
def action_import_site(self):
"""Create a new site and configuration from UDM Pro"""
self.ensure_one()
try:
# Disable SSL verification warnings - UDM Pro often uses self-signed certs
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Test connection to UDM Pro
login_url = f'https://{self.host}:{self.port}/api/auth/login'
login_data = {
'username': self.username,
'password': self.password
}
headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
response = requests.post(
login_url,
json=login_data,
headers=headers,
verify=False
)
response.raise_for_status()
# Create the site
site = self.env['udm.site'].create({
'name': self.name,
'site_id': self.site_id,
'active': True
})
# Create the configuration
config = self.env['udm.configuration'].create({
'site_id': site.id,
'host': self.host,
'port': self.port,
'username': self.username,
'password': self.password,
'active': True
})
# Import the configuration
config.action_import_configuration()
# Return action to view the new site
return {
'name': _('Site'),
'view_mode': 'form',
'res_model': 'udm.site',
'res_id': site.id,
'type': 'ir.actions.act_window',
'target': 'current',
}
except RequestException as e:
raise UserError(_('Failed to connect to UDM Pro: %s') % str(e))
except Exception as e:
raise UserError(_('Failed to create site: %s') % str(e))

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Import Site Action -->
<record id="action_import_udm_site" model="ir.actions.act_window">
<field name="name">Import UDM Pro Site</field>
<field name="res_model">udm.site.import.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<!-- Import Site Wizard Form View -->
<record id="view_udm_site_import_wizard_form" model="ir.ui.view">
<field name="name">udm.site.import.wizard.form</field>
<field name="model">udm.site.import.wizard</field>
<field name="arch" type="xml">
<form string="Import UDM Pro Site">
<sheet>
<group>
<group>
<field name="name" placeholder="e.g., Bureau Principal"/>
<field name="site_id"/>
</group>
<group>
<field name="host" placeholder="e.g., 192.168.1.1"/>
<field name="port"/>
</group>
<group>
<field name="username" placeholder="Admin username"/>
<field name="password" password="True" placeholder="Admin password"/>
</group>
</group>
</sheet>
<footer>
<button name="action_import_site" string="Import" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- MFA Wizard Form View -->
<record id="view_udm_mfa_wizard_form" model="ir.ui.view">
<field name="name">udm.mfa.wizard.form</field>
<field name="model">udm.mfa.wizard</field>
<field name="arch" type="xml">
<form string="Code d'authentification à deux facteurs">
<p>Un code d'authentification a été envoyé à votre adresse courriel.</p>
<p>Veuillez entrer ce code ci-dessous pour continuer l'importation.</p>
<group>
<field name="mfa_code" placeholder="Entrez le code reçu par courriel"/>
<field name="config_id" invisible="1"/>
</group>
<footer>
<button string="Valider" name="action_validate_mfa" type="object" class="btn-primary"/>
<button string="Annuler" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>