unifi get datas

This commit is contained in:
Benoît Vézina 2025-03-27 15:34:13 -04:00
parent 5390b6308e
commit ccf211c554
41 changed files with 5601 additions and 4014 deletions

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,20 @@
{
'name': 'Odoo Rsync Backup',
'version': '18.0.1.0.0',
'category': 'Administration/Backup',
'summary': 'Rsync backup for filestore and configuration',
'sequence': 1,
'author': 'Bemade',
'website': 'https://bemade.org',
'license': 'LGPL-3',
'depends': [
'base',
'auto_database_backup'
],
'data': [
'views/rsync_config_views.xml',
],
'installable': True,
'application': True,
'auto_install': False,
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="ir_cron_rsync_backup" model="ir.cron">
<field name="name">Rsync Backup: Daily Backup</field>
<field name="model_id" ref="model_rsync_backup_config"/>
<field name="state">code</field>
<field name="code">model._perform_backup()</field>
<field name="interval_type">days</field>
<field name="interval_number">1</field>
<field name="active" eval="True"/>
</record>
</data>
</odoo>

View file

@ -0,0 +1 @@
from . import rsync_config

View file

@ -0,0 +1,185 @@
import logging
import os
import subprocess
from odoo import models, fields, tools, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class DbBackupConfigInherit(models.Model):
"""
Extend db.backup.configure to add rsync functionality
"""
_inherit = 'db.backup.configure'
# Rsync configuration fields
enable_rsync = fields.Boolean(
string='RSync the filestore',
help='Enable rsync backup for filestore when using dump format'
)
remote_host = fields.Char(string='Remote Host')
remote_user = fields.Char(string='Remote User')
remote_port = fields.Integer(string='SSH Port', default=22)
ssh_key_path = fields.Char(
string='SSH Key Path',
default='/home/odoo/.ssh/id_rsa',
help='Path to the SSH private key file'
)
filestore_dest_path = fields.Char(
string='Filestore Destination Path',
help='Remote path where filestore will be backed up'
)
def _sync_filestore_with_rsync(self):
"""
Synchronize filestore directory using rsync after backup is completed
"""
if not (self.enable_rsync and self.backup_format == 'dump'):
return
try:
# Get filestore path
filestore_path = os.path.join(tools.config['data_dir'], 'filestore', self.env.cr.dbname)
# Build rsync command
filestore_cmd = [
'rsync', '-avz', '-e',
f'ssh -p {self.remote_port} -i {self.ssh_key_path}',
filestore_path + '/', # Add trailing slash to sync contents
f'{self.remote_user}@{self.remote_host}:{self.filestore_dest_path}'
]
# Execute rsync command
subprocess.run(filestore_cmd, check=True)
_logger.info('Rsync backup completed successfully')
return True
except subprocess.CalledProcessError as e:
_logger.error('Rsync backup failed: %s', str(e))
raise UserError(_('Rsync backup failed. Check the logs for details.'))
except Exception as e:
_logger.error('Unexpected error during rsync backup: %s', str(e))
raise UserError(_('Unexpected error during rsync backup. Check the logs for details.'))
def _schedule_auto_backup(self, frequency):
"""
Override _schedule_auto_backup to add rsync synchronization after backup
"""
res = super(DbBackupConfigInherit, self)._schedule_auto_backup(frequency)
self._sync_filestore_with_rsync()
return res
name = fields.Char(
string='Name',
required=True
)
remote_host = fields.Char(
string='Remote Host',
required=True
)
remote_user = fields.Char(
string='Remote User',
required=True
)
remote_port = fields.Integer(
string='SSH Port',
default=22
)
ssh_key_path = fields.Char(
string='SSH Key Path',
default='/home/odoo/.ssh/id_rsa',
help='Path to the SSH private key file'
)
filestore_dest_path = fields.Char(
string='Filestore Destination Path',
required=True,
help='Remote path where filestore will be backed up'
)
config_dest_path = fields.Char(
string='Config Destination Path',
required=True,
help='Remote path where odoo.conf will be backed up'
)
active = fields.Boolean(
default=True
)
last_backup = fields.Datetime(
string='Last Backup',
readonly=True
)
def _run_rsync_command(self, source_path, dest_path):
"""
Execute rsync command with specified parameters
"""
try:
cmd = [
'rsync',
'-avz',
'-e', f'ssh -p {self.remote_port} -i {self.ssh_key_path}',
source_path,
f'{self.remote_user}@{self.remote_host}:{dest_path}'
]
process = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True
)
_logger.info(f'Rsync successful: {process.stdout}')
return True
except subprocess.CalledProcessError as e:
_logger.error(f'Rsync failed: {e.stderr}')
return False
def action_backup_now(self):
"""
Manually trigger the backup process
"""
self.ensure_one()
self._perform_backup()
return True
def _perform_backup(self):
"""
Perform the actual backup operation for filestore and config
"""
success = True
# Get filestore path from Odoo config
filestore_path = os.path.join(
self.env['ir.config_parameter'].get_param('data_dir'),
'filestore',
self.env.cr.dbname
)
# Get odoo.conf path
odoo_conf_path = os.environ.get('ODOO_RC', '/etc/odoo/odoo.conf')
# Backup filestore
if os.path.exists(filestore_path):
if not self._run_rsync_command(filestore_path + '/', self.filestore_dest_path):
success = False
# Backup odoo.conf
if os.path.exists(odoo_conf_path):
if not self._run_rsync_command(odoo_conf_path, self.config_dest_path):
success = False
if success:
self.write({'last_backup': fields.Datetime.now()})
return success

View file

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_rsync_backup_config_admin,rsync.backup.config.admin,model_rsync_backup_config,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_rsync_backup_config_admin rsync.backup.config.admin model_rsync_backup_config base.group_system 1 1 1 1

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Inherit Form View -->
<record id="view_db_backup_configure_form_inherit_rsync" model="ir.ui.view">
<field name="name">db.backup.configure.form.inherit.rsync</field>
<field name="model">db.backup.configure</field>
<field name="inherit_id" ref="auto_database_backup.db_backup_configure_view_form"/>
<field name="arch" type="xml">
<field name="backup_format" position="after">
<field name="enable_rsync" attrs="{'invisible': [('backup_format', '!=', 'dump')]}"/>
<field name="remote_host" attrs="{
'invisible': ['|', ('backup_format', '!=', 'dump'), ('enable_rsync', '=', False)],
'required': [('enable_rsync', '=', True)]
}"/>
<field name="remote_user" attrs="{
'invisible': ['|', ('backup_format', '!=', 'dump'), ('enable_rsync', '=', False)],
'required': [('enable_rsync', '=', True)]
}"/>
<field name="remote_port" attrs="{
'invisible': ['|', ('backup_format', '!=', 'dump'), ('enable_rsync', '=', False)]
}"/>
<field name="ssh_key_path" attrs="{
'invisible': ['|', ('backup_format', '!=', 'dump'), ('enable_rsync', '=', False)]
}"/>
<field name="filestore_dest_path" attrs="{
'invisible': ['|', ('backup_format', '!=', 'dump'), ('enable_rsync', '=', False)],
'required': [('enable_rsync', '=', True)]
}"/>
</field>
</field>
</record>
</odoo>

View file

@ -46,6 +46,8 @@ This module allows you to:
'views/unifi_dns_config_views.xml',
'views/unifi_routing_views.xml',
'views/unifi_routing_config_views.xml',
'views/unifi_wifi_views.xml',
'views/unifi_vpn_views.xml',
'views/unifi_api_config_views.xml',
'views/unifi_dashboard_views.xml',
# Templates

View file

@ -1,10 +1,11 @@
# -*- coding: utf-8 -*-
# Import module files
# Common functionality
from . import unifi_common
# UniFi models
from . import unifi_site
from . import unifi_site_controller
from . import unifi_site_manager
from . import unifi_auth_session
from . import unifi_mfa
from . import unifi_api_config
@ -21,5 +22,7 @@ from . import unifi_dns
from . import unifi_dns_config
from . import unifi_routing
from . import unifi_routing_config
from . import unifi_wifi
from . import unifi_vpn
from . import unifi_dashboard_metric
from . import unifi_dashboard_stat

View file

@ -53,6 +53,17 @@ class UnifiAuthSession(models.Model):
help='Session cookie for Controller API'
)
endpoint = fields.Char(
string='Authentication Endpoint',
help='The endpoint used for authentication (e.g. /api/login or /api/auth/login)'
)
is_udm_pro = fields.Boolean(
string='Is UDM Pro',
default=False,
help='Indicates if this session is for a UDM Pro or UDM Pro SE device'
)
expiry = fields.Datetime(
string='Expiry Date',
help='Date and time when this session expires'

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
import json
import logging
from odoo import models, fields, api, _
_logger = logging.getLogger(__name__)
class UnifiCommonMixin(models.AbstractModel):
"""Mixin for common functionality across UniFi models
This mixin provides common fields and methods that are used by multiple
UniFi models, such as formatting raw JSON data.
"""
_name = 'unifi.common.mixin'
_description = 'UniFi Common Functionality Mixin'
def format_raw_data_json(self, raw_data):
"""Format raw JSON data by removing outer braces and adjusting indentation
Args:
raw_data (str): Raw JSON data string
Returns:
str: Formatted JSON string without outer braces
"""
if not raw_data:
return ''
try:
# Load and format the JSON data
data = json.loads(raw_data)
formatted_json = json.dumps(data, indent=4)
# Remove the first and last line (the braces)
lines = formatted_json.split('\n')
if len(lines) > 2: # Ensure there are at least 3 lines
# Remove the first and last line and adjust indentation
inner_content = '\n'.join(line[4:] for line in lines[1:-1])
return inner_content
else:
return formatted_json
except (ValueError, json.JSONDecodeError):
return 'Invalid JSON'

View file

@ -4,6 +4,7 @@
# pylint: disable=import-error
from odoo import models, fields, api
from odoo.tools.translate import _
from .unifi_common import UnifiCommonMixin
# pylint: enable=import-error
import logging
@ -12,7 +13,7 @@ from datetime import datetime, timedelta
_logger = logging.getLogger(__name__)
class UnifiDevice(models.Model):
class UnifiDevice(models.Model, UnifiCommonMixin):
"""Modèle pour les appareils UniFi
Ce modèle stocke les informations sur les appareils UniFi, tels que les points d'accès,
@ -114,6 +115,17 @@ class UnifiDevice(models.Model):
help="Données JSON brutes de l'appareil provenant de l'API UniFi"
)
raw_data_json = fields.Text(
string='Données brutes (JSON)',
compute='_compute_raw_data_json',
help='Données brutes de l\'appareil au format JSON formaté'
)
@api.depends('raw_data')
def _compute_raw_data_json(self):
for record in self:
record.raw_data_json = self.format_raw_data_json(record.raw_data)
active = fields.Boolean(
string='Actif',
default=True,

View file

@ -133,6 +133,59 @@ class UnifiDns(models.Model):
record.last_sync = fields.Datetime.now()
@api.model
def create_or_update_from_data(self, site, dns_data):
"""Create or update DNS entry from UniFi API data
Args:
site (unifi.site): Site record
dns_data (dict): DNS entry data from UniFi API
Returns:
unifi.dns: Created or updated DNS entry record
"""
_logger.info("Creating or updating DNS entry from data: %s", dns_data)
# Extract required fields
hostname = dns_data.get('hostname')
ip_address = dns_data.get('ip_address')
unifi_id = dns_data.get('id') or dns_data.get('_id')
if not hostname or not ip_address:
_logger.warning("Missing required fields in DNS data: %s", dns_data)
return False
# Search for existing DNS entry
domain = [
('site_id', '=', site.id),
'|',
('hostname', '=', hostname),
('unifi_id', '=', unifi_id)
]
existing = self.search(domain, limit=1)
# Prepare values for create/write
vals = {
'hostname': hostname,
'ip_address': ip_address,
'unifi_id': unifi_id,
'description': dns_data.get('description', ''),
'enabled': dns_data.get('enabled', True),
'entry_type': dns_data.get('type', 'static'),
'last_sync': fields.Datetime.now(),
'raw_data': json.dumps(dns_data, indent=2) if dns_data else False
}
if existing:
_logger.info("Updating existing DNS entry: %s", existing.hostname)
existing.write(vals)
return existing
else:
_logger.info("Creating new DNS entry: %s", hostname)
vals['site_id'] = site.id
return self.create(vals)
def push_to_unifi(self):
"""Pousse les modifications vers le système UniFi"""
for record in self:

View file

@ -78,16 +78,56 @@ class UnifiFirewallRule(models.Model):
help='Protocole réseau auquel cette règle s\'applique'
)
source_type = fields.Selection(
selection=[
('address', 'Adresse IP'),
('network', 'Réseau'),
('object', 'Objet'),
('any', 'N\'importe où')
],
string='Type de source',
compute='_compute_source_type',
store=True,
help='Type de la source (adresse IP, réseau ou objet)'
)
source = fields.Char(
string='Source',
help='Réseau source ou adresse IP au format CIDR'
)
formatted_source = fields.Char(
string='Source formatée',
compute='_compute_formatted_source',
store=True,
help='Affichage formaté de la source en fonction de son type'
)
destination_type = fields.Selection(
selection=[
('address', 'Adresse IP'),
('network', 'Réseau'),
('object', 'Objet'),
('any', 'N\'importe où')
],
string='Type de destination',
compute='_compute_destination_type',
store=True,
help='Type de la destination (adresse IP, réseau ou objet)'
)
destination = fields.Char(
string='Destination',
help='Réseau de destination ou adresse IP au format CIDR'
)
formatted_destination = fields.Char(
string='Destination formatée',
compute='_compute_formatted_destination',
store=True,
help='Affichage formaté de la destination en fonction de son type'
)
src_port = fields.Char(
string='Port source',
help='Numéro de port source ou plage (ex: 80 ou 1024-2048)'
@ -115,6 +155,34 @@ class UnifiFirewallRule(models.Model):
help='Type de règle dans le système UniFi'
)
detailed_rule_type = fields.Selection(
selection=[
('internet-in-ipv4', 'Internet-in (IPv4)'),
('internet-out-ipv4', 'Internet-out (IPv4)'),
('internet-local-ipv4', 'Internet-local (IPv4)'),
('lan-in-ipv4', 'LAN-in (IPv4)'),
('lan-out-ipv4', 'LAN-out (IPv4)'),
('lan-local-ipv4', 'LAN-local (IPv4)'),
('guest-in-ipv4', 'Guest-in (IPv4)'),
('guest-out-ipv4', 'Guest-out (IPv4)'),
('guest-local-ipv4', 'Guest-local (IPv4)'),
('internet-in-ipv6', 'Internet-in (IPv6)'),
('internet-out-ipv6', 'Internet-out (IPv6)'),
('internet-local-ipv6', 'Internet-local (IPv6)'),
('lan-in-ipv6', 'LAN-in (IPv6)'),
('lan-out-ipv6', 'LAN-out (IPv6)'),
('lan-local-ipv6', 'LAN-local (IPv6)'),
('guest-in-ipv6', 'Guest-in (IPv6)'),
('guest-out-ipv6', 'Guest-out (IPv6)'),
('guest-local-ipv6', 'Guest-local (IPv6)'),
('other', 'Autre')
],
string='Type détaillé',
compute='_compute_detailed_rule_type',
store=True,
help='Type détaillé de la règle de pare-feu (Internet-in, LAN-out, etc.)'
)
rule_index = fields.Integer(
string='Index de la règle',
help='Position de la règle dans la liste des règles'
@ -144,13 +212,162 @@ class UnifiFirewallRule(models.Model):
help='Données brutes de la règle de pare-feu au format JSON'
)
raw_data_json = fields.Text(
string='Données brutes (JSON)',
compute='_compute_raw_data_json',
help='Données brutes de la règle de pare-feu au format JSON'
)
@api.depends('raw_data')
def _compute_raw_data_json(self):
for record in self:
if record.raw_data:
try:
# Charger le JSON, puis le formater sans les accolades externes
data = json.loads(record.raw_data)
formatted_json = json.dumps(data, indent=4)
# Enlever la première et la dernière ligne (les accolades)
lines = formatted_json.split('\n')
if len(lines) > 2: # S'assurer qu'il y a au moins 3 lignes
# Enlever la première et la dernière ligne et ajuster l'indentation
inner_content = '\n'.join(line[4:] for line in lines[1:-1])
record.raw_data_json = inner_content
else:
record.raw_data_json = formatted_json
except ValueError:
record.raw_data_json = 'Invalid JSON'
# Champs calculés
rule_summary = fields.Char(
string='Résumé de la règle',
compute='_compute_rule_summary'
)
@api.depends('action', 'protocol', 'source', 'destination', 'src_port', 'dst_port')
@api.depends('raw_data')
def _compute_detailed_rule_type(self):
"""Détermine le type détaillé de la règle de pare-feu à partir des données brutes"""
for record in self:
if not record.raw_data:
record.detailed_rule_type = 'other'
continue
try:
rule_data = json.loads(record.raw_data)
# Extraire les informations du type de règle
rule_interface = rule_data.get('ruleset', '')
direction = rule_data.get('direction', '')
ip_version = 'ipv4' if rule_data.get('ipv6', False) is False else 'ipv6'
# Construire le type détaillé
if rule_interface and direction:
detailed_type = f"{rule_interface}-{direction}-{ip_version}"
if detailed_type in dict(self._fields['detailed_rule_type'].selection).keys():
record.detailed_rule_type = detailed_type
else:
record.detailed_rule_type = 'other'
else:
record.detailed_rule_type = 'other'
except (json.JSONDecodeError, AttributeError):
record.detailed_rule_type = 'other'
@api.depends('source', 'raw_data')
def _compute_source_type(self):
"""Détermine le type de la source (adresse IP, réseau ou objet)"""
for record in self:
if not record.source:
record.source_type = 'any'
continue
try:
rule_data = json.loads(record.raw_data) if record.raw_data else {}
src_type = rule_data.get('src_type', '')
if src_type == 'network':
record.source_type = 'network'
elif src_type == 'object':
record.source_type = 'object'
elif record.source and '/' in record.source:
# Si contient un slash, c'est probablement un réseau CIDR
record.source_type = 'network'
elif record.source:
# Sinon, c'est probablement une adresse IP
record.source_type = 'address'
else:
record.source_type = 'any'
except (json.JSONDecodeError, AttributeError):
if record.source:
record.source_type = 'address'
else:
record.source_type = 'any'
@api.depends('destination', 'raw_data')
def _compute_destination_type(self):
"""Détermine le type de la destination (adresse IP, réseau ou objet)"""
for record in self:
if not record.destination:
record.destination_type = 'any'
continue
try:
rule_data = json.loads(record.raw_data) if record.raw_data else {}
dst_type = rule_data.get('dst_type', '')
if dst_type == 'network':
record.destination_type = 'network'
elif dst_type == 'object':
record.destination_type = 'object'
elif record.destination and '/' in record.destination:
# Si contient un slash, c'est probablement un réseau CIDR
record.destination_type = 'network'
elif record.destination:
# Sinon, c'est probablement une adresse IP
record.destination_type = 'address'
else:
record.destination_type = 'any'
except (json.JSONDecodeError, AttributeError):
if record.destination:
record.destination_type = 'address'
else:
record.destination_type = 'any'
@api.depends('source', 'source_type', 'raw_data')
def _compute_formatted_source(self):
"""Calcule l'affichage formaté de la source en fonction de son type"""
for record in self:
if record.source_type == 'any' or not record.source:
record.formatted_source = "N'importe où"
elif record.source_type == 'object':
try:
rule_data = json.loads(record.raw_data) if record.raw_data else {}
object_name = rule_data.get('src_object_name', record.source)
record.formatted_source = f"Objet: {object_name}"
except (json.JSONDecodeError, AttributeError):
record.formatted_source = record.source
elif record.source_type == 'network':
record.formatted_source = f"Réseau: {record.source}"
else:
record.formatted_source = f"IP: {record.source}"
@api.depends('destination', 'destination_type', 'raw_data')
def _compute_formatted_destination(self):
"""Calcule l'affichage formaté de la destination en fonction de son type"""
for record in self:
if record.destination_type == 'any' or not record.destination:
record.formatted_destination = "N'importe où"
elif record.destination_type == 'object':
try:
rule_data = json.loads(record.raw_data) if record.raw_data else {}
object_name = rule_data.get('dst_object_name', record.destination)
record.formatted_destination = f"Objet: {object_name}"
except (json.JSONDecodeError, AttributeError):
record.formatted_destination = record.destination
elif record.destination_type == 'network':
record.formatted_destination = f"Réseau: {record.destination}"
else:
record.formatted_destination = f"IP: {record.destination}"
@api.depends('action', 'protocol', 'formatted_source', 'formatted_destination', 'src_port', 'dst_port')
def _compute_rule_summary(self):
"""Calcule un résumé lisible de la règle de pare-feu
@ -173,8 +390,8 @@ class UnifiFirewallRule(models.Model):
if record.protocol:
parts.append(record.protocol.upper())
src_addr = record.source or 'n\'importe où'
dst_addr = record.destination or 'n\'importe où'
src_addr = record.formatted_source or "N'importe où"
dst_addr = record.formatted_destination or "N'importe où"
src = f"de {src_addr}"
if record.src_port:

View file

@ -3,6 +3,7 @@
# These imports will work in an Odoo environment, even if your IDE marks them as not found
# pylint: disable=import-error
from odoo import models, fields, api, _
from .unifi_common import UnifiCommonMixin
from odoo.exceptions import UserError, ValidationError
# pylint: enable=import-error
@ -12,7 +13,7 @@ from datetime import datetime
_logger = logging.getLogger(__name__)
class UnifiNetwork(models.Model):
class UnifiNetwork(models.Model, UnifiCommonMixin):
"""Modèle pour gérer les réseaux UniFi
Ce modèle représente les réseaux configurés dans les sites UniFi,
@ -43,6 +44,8 @@ class UnifiNetwork(models.Model):
('wan', 'WAN'),
('lan', 'LAN'),
('vpn', 'VPN'),
('site-vpn', 'Site VPN'),
('remote-user-vpn', 'Remote User VPN'),
('vlan-only', 'VLAN Only'),
('other', 'Autre')
],
@ -135,6 +138,17 @@ class UnifiNetwork(models.Model):
help='Données brutes du réseau au format JSON'
)
raw_data_json = fields.Text(
string='Données brutes (JSON)',
compute='_compute_raw_data_json',
help='Données brutes du réseau au format JSON formaté'
)
@api.depends('raw_data')
def _compute_raw_data_json(self):
for record in self:
record.raw_data_json = self.format_raw_data_json(record.raw_data)
# Relations
site_id = fields.Many2one(
comodel_name='unifi.site',
@ -243,25 +257,149 @@ class UnifiNetwork(models.Model):
# Création d'un nouveau réseau
return self.create(vals)
def sync_networks(self, site):
def sync_networks(self):
"""Synchronise les réseaux depuis l'API UniFi
Args:
site: L'enregistrement du site UniFi
Cette méthode est conçue pour être appelée depuis un bouton dans l'interface utilisateur.
Elle utilise le site associé à l'enregistrement en cours.
Returns:
bool: True si la synchronisation a réussi, False sinon
"""
self.ensure_one()
# Utiliser le site associé à ce réseau
site = self.site_id
if not site:
_logger.error("Impossible de synchroniser: aucun site associé à ce réseau")
return False
# Déterminer quelle méthode utiliser en fonction du type d'API
if site.api_type == 'controller':
return self._sync_networks_controller(site)
elif site.api_type == 'site_manager':
return self._sync_networks_site_manager(site)
def sync_networks_from_list(self):
"""Synchronise tous les réseaux du site actuel depuis la vue liste
Cette méthode vérifie si tous les réseaux affichés appartiennent au même site,
puis appelle la méthode de synchronisation appropriée.
Returns:
dict: Action de rafraîchissement de la vue ou notification d'erreur
"""
# Vérifier si tous les réseaux appartiennent au même site
sites = self.mapped('site_id')
if not sites:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Aucun site associé aux réseaux sélectionnés.'),
'sticky': False,
'type': 'danger',
}
}
if len(sites) > 1:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Les réseaux sélectionnés appartiennent à différents sites. Veuillez filtrer par site.'),
'sticky': False,
'type': 'danger',
}
}
# Tous les réseaux appartiennent au même site, appeler la méthode de synchronisation
site = sites[0]
# Déterminer quelle méthode utiliser en fonction du type d'API
try:
if site.api_type == 'controller':
self._sync_networks_controller(site)
elif site.api_type == 'site_manager':
self._sync_networks_site_manager(site)
else:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Type d\'API non pris en charge.'),
'sticky': False,
'type': 'danger',
}
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Succès'),
'message': _('Synchronisation des réseaux terminée avec succès.'),
'sticky': False,
'type': 'success',
}
}
except Exception as e:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _(f'Erreur lors de la synchronisation: {str(e)}'),
'sticky': False,
'type': 'danger',
}
}
@api.model
def sync_site_networks(self, site_id):
"""Synchronise tous les réseaux d'un site spécifique
Cette méthode est conçue pour être appelée depuis un bouton dans la vue liste
lorsque tous les réseaux affichés appartiennent au même site.
Args:
site_id: ID du site dont les réseaux doivent être synchronisés
Returns:
dict: Action de rafraîchissement de la vue
"""
# Récupérer le site
site = self.env['unifi.site'].browse(site_id)
if not site.exists():
raise UserError(_("Le site sélectionné n'existe pas."))
# Déterminer quelle méthode utiliser en fonction du type d'API
if site.api_type == 'controller':
self._sync_networks_controller(site)
elif site.api_type == 'site_manager':
self._sync_networks_site_manager(site)
else:
_logger.error(f"Type d'API non pris en charge: {site.api_type}")
return False
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Type d\'API non pris en charge.'),
'sticky': False,
'type': 'danger',
}
}
# Retourner une action pour rafraîchir la vue
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
def _sync_networks_controller(self, site):
"""Synchronise les réseaux depuis l'API Controller
@ -272,8 +410,8 @@ class UnifiNetwork(models.Model):
Returns:
bool: True si la synchronisation a réussi, False sinon
"""
# Obtenir les données des réseaux depuis l'API Controller
networks_data = self.env['unifi.site.controller'].get_network_data(site)
# Obtenir les données des réseaux directement depuis le site
networks_data = site._get_controller_network_data()
if not networks_data:
return False
@ -292,8 +430,8 @@ class UnifiNetwork(models.Model):
Returns:
bool: True si la synchronisation a réussi, False sinon
"""
# Obtenir les données des réseaux depuis l'API Site Manager
networks_data = self.env['unifi.site.manager'].get_network_data(site)
# Obtenir les données des réseaux directement depuis le site
networks_data = site._get_site_manager_network_data()
if not networks_data:
return False

View file

@ -4,10 +4,11 @@ import json
import logging
from odoo import models, fields, api
from .unifi_common import UnifiCommonMixin
_logger = logging.getLogger(__name__)
class UnifiPortForward(models.Model):
class UnifiPortForward(models.Model, UnifiCommonMixin):
"""Représente une règle de redirection de port dans le système UniFi
Ce modèle stocke les règles de redirection de port qui permettent un accès externe
@ -122,6 +123,17 @@ class UnifiPortForward(models.Model):
help='Données brutes de la redirection de port au format JSON'
)
raw_data_json = fields.Text(
string='Données brutes (JSON)',
compute='_compute_raw_data_json',
help='Données brutes de la redirection de port au format JSON formaté'
)
@api.depends('raw_data')
def _compute_raw_data_json(self):
for record in self:
record.raw_data_json = self.format_raw_data_json(record.raw_data)
# Champs calculés
rule_summary = fields.Char(
string='Résumé de la règle',

View file

@ -27,6 +27,12 @@ class UnifiRoutingConfig(models.Model):
help='Site this routing configuration belongs to'
)
name = fields.Char(
string='Name',
required=True,
help='Name of the routing configuration'
)
ospf_enabled = fields.Boolean(
string='OSPF Enabled',
default=False,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -62,6 +62,7 @@ class UnifiSyncJob(models.Model):
('pending', 'Pending'),
('running', 'Running'),
('completed', 'Completed'),
('partial', 'Partially Completed'),
('failed', 'Failed'),
('cancelled', 'Cancelled')
],
@ -237,7 +238,7 @@ class UnifiSyncJob(models.Model):
return {
'name': _('API Logs'),
'view_mode': 'tree,form',
'view_mode': 'list,form',
'res_model': 'unifi.api.log',
'domain': [('sync_job_id', '=', self.id)],
'type': 'ir.actions.act_window',

View file

@ -4,10 +4,11 @@ import json
import logging
from odoo import models, fields, api
from .unifi_common import UnifiCommonMixin
_logger = logging.getLogger(__name__)
class UnifiSystemInfo(models.Model):
class UnifiSystemInfo(models.Model, UnifiCommonMixin):
"""System information of the UniFi device"""
_name = 'unifi.system.info'
_description = 'UniFi System Information'
@ -51,6 +52,17 @@ class UnifiSystemInfo(models.Model):
# Raw data for debugging and future extensions
raw_data = fields.Text(string='Raw Data')
raw_data_json = fields.Text(
string='Données brutes (JSON)',
compute='_compute_raw_data_json',
help='Données brutes du système au format JSON formaté'
)
@api.depends('raw_data')
def _compute_raw_data_json(self):
for record in self:
record.raw_data_json = self.format_raw_data_json(record.raw_data)
@api.depends('uptime')
def _compute_uptime_human(self):
for record in self:

View file

@ -3,6 +3,7 @@
# These imports will work in an Odoo environment, even if your IDE marks them as not found
# pylint: disable=import-error
from odoo import models, fields, api, _
from .unifi_common import UnifiCommonMixin
from odoo.exceptions import UserError, ValidationError
# pylint: enable=import-error
@ -12,7 +13,7 @@ from datetime import datetime
_logger = logging.getLogger(__name__)
class UnifiUser(models.Model):
class UnifiUser(models.Model, UnifiCommonMixin):
"""Modèle pour gérer les utilisateurs UniFi
Ce modèle représente les utilisateurs configurés dans les sites UniFi,
@ -113,6 +114,17 @@ class UnifiUser(models.Model):
help='Données brutes de l\'utilisateur au format JSON'
)
raw_data_json = fields.Text(
string='Données brutes (JSON)',
compute='_compute_raw_data_json',
help='Données brutes de l\'utilisateur au format JSON formaté'
)
@api.depends('raw_data')
def _compute_raw_data_json(self):
for record in self:
record.raw_data_json = self.format_raw_data_json(record.raw_data)
# Relations
site_id = fields.Many2one(
comodel_name='unifi.site',

View file

@ -3,11 +3,12 @@
import json
import logging
from odoo import models, fields, api, _
from .unifi_common import UnifiCommonMixin
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class UnifiVLAN(models.Model):
class UnifiVLAN(models.Model, UnifiCommonMixin):
"""Modèle pour gérer les VLANs UniFi
Ce modèle représente les VLANs configurés dans les sites UniFi.
@ -99,6 +100,17 @@ class UnifiVLAN(models.Model):
help='Données brutes JSON du VLAN depuis l\'API'
)
raw_data_json = fields.Text(
string='Données brutes (JSON)',
compute='_compute_raw_data_json',
help='Données brutes du VLAN au format JSON formaté'
)
@api.depends('raw_data')
def _compute_raw_data_json(self):
for record in self:
record.raw_data_json = self.format_raw_data_json(record.raw_data)
_sql_constraints = [
('vlan_id_site_uniq', 'unique(vlan_id, site_id)', 'L\'ID VLAN doit être unique par site!')
]

View file

@ -0,0 +1,241 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from .unifi_common import UnifiCommonMixin
from odoo.exceptions import ValidationError
import logging
import json
from pprint import pformat
_logger = logging.getLogger(__name__)
class UnifiVpn(models.Model, UnifiCommonMixin):
"""Configuration VPN pour le système UniFi
Ce modèle stocke les configurations VPN pour les contrôleurs UniFi.
Il gère les tunnels VPN IPsec et autres types de VPN.
Les configurations VPN sont liées à un site spécifique et sont automatiquement supprimées
lorsque le site est supprimé (cascade).
"""
_name = 'unifi.vpn'
_description = 'UniFi VPN Configuration'
_rec_name = 'name'
_order = 'name'
site_id = fields.Many2one(
comodel_name='unifi.site',
string='Site',
required=True,
ondelete='cascade',
help='Site this VPN configuration belongs to'
)
name = fields.Char(
string='Name',
required=True,
help='Name of this VPN configuration'
)
vpn_type = fields.Selection(
selection=[
('ipsec-vpn', 'IPsec VPN'),
('l2tp-vpn', 'L2TP VPN'),
('openvpn', 'OpenVPN'),
('wireguard', 'WireGuard'),
('other', 'Other')
],
string='VPN Type',
default='ipsec-vpn',
help='Type of VPN connection'
)
enabled = fields.Boolean(
string='Enabled',
default=True,
help='Whether this VPN configuration is active'
)
peer_ip = fields.Char(
string='Peer IP',
help='IP address of the remote VPN peer'
)
local_ip = fields.Char(
string='Local IP',
help='Local IP address for this VPN connection'
)
remote_subnets = fields.Char(
string='Remote Subnets',
help='Comma-separated list of remote subnets accessible through this VPN'
)
interface = fields.Char(
string='Interface',
help='Network interface used for this VPN'
)
purpose = fields.Char(
string='Purpose',
help='Purpose of this VPN connection'
)
unifi_id = fields.Char(
string='UniFi ID',
help='ID of this VPN configuration in the UniFi system'
)
last_sync = fields.Datetime(
string='Last Synchronization',
help='Last time this VPN configuration was synchronized with the UniFi system'
)
raw_data = fields.Text(
string='Raw Data',
help='Raw VPN configuration data in JSON format'
)
raw_data_json = fields.Text(
string='Données brutes (JSON)',
compute='_compute_raw_data_json',
help='Données brutes de la configuration VPN au format JSON formaté'
)
@api.depends('raw_data')
def _compute_raw_data_json(self):
for record in self:
record.raw_data_json = self.format_raw_data_json(record.raw_data)
formatted_data = fields.Html(
string='Formatted Configuration',
compute='_compute_formatted_data',
help='Formatted view of the VPN configuration data'
)
@api.depends('raw_data')
def _compute_formatted_data(self):
"""Transforme les données brutes JSON en format HTML lisible"""
for record in self:
if not record.raw_data:
record.formatted_data = "<p><em>Aucune donnée disponible</em></p>"
continue
try:
# Analyser les données JSON
data = json.loads(record.raw_data)
# Créer une représentation HTML formatée
html = "<div style='font-family: monospace;'>"
# Informations générales
html += "<h3 style='color: #2C3E50;'>Informations générales</h3>"
html += "<table style='width: 100%; border-collapse: collapse;'>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Nom:</td><td>{data.get('name', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Type:</td><td>{data.get('vpn_type', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Activé:</td><td>{'Oui' if data.get('enabled') else 'Non'}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Interface:</td><td>{data.get('ifname', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>But:</td><td>{data.get('purpose', 'N/A')}</td></tr>"
html += "</table>"
# Configuration IPsec
if data.get('vpn_type') == 'ipsec-vpn':
html += "<h3 style='color: #2C3E50; margin-top: 15px;'>Configuration IPsec</h3>"
html += "<table style='width: 100%; border-collapse: collapse;'>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>IP Pair:</td><td>{data.get('ipsec_peer_ip', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>IP Locale:</td><td>{data.get('ipsec_local_ip', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Interface:</td><td>{data.get('ipsec_interface', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Échange de clés:</td><td>{data.get('ipsec_key_exchange', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Profil:</td><td>{data.get('ipsec_profile', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>PFS:</td><td>{'Oui' if data.get('ipsec_pfs') else 'Non'}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Routage dynamique:</td><td>{'Oui' if data.get('ipsec_dynamic_routing') else 'Non'}</td></tr>"
html += "</table>"
# Paramètres IKE
html += "<h4 style='color: #2C3E50; margin-top: 10px;'>Paramètres IKE</h4>"
html += "<table style='width: 100%; border-collapse: collapse;'>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Groupe DH:</td><td>{data.get('ipsec_ike_dh_group', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Chiffrement:</td><td>{data.get('ipsec_ike_encryption', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Hash:</td><td>{data.get('ipsec_ike_hash', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Durée de vie (s):</td><td>{data.get('ipsec_ike_lifetime', 'N/A')}</td></tr>"
html += "</table>"
# Paramètres ESP
html += "<h4 style='color: #2C3E50; margin-top: 10px;'>Paramètres ESP</h4>"
html += "<table style='width: 100%; border-collapse: collapse;'>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Groupe DH:</td><td>{data.get('ipsec_esp_dh_group', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Chiffrement:</td><td>{data.get('ipsec_esp_encryption', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Hash:</td><td>{data.get('ipsec_esp_hash', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Durée de vie (s):</td><td>{data.get('ipsec_esp_lifetime', 'N/A')}</td></tr>"
html += "</table>"
# Sous-réseaux distants
remote_subnets = data.get('remote_vpn_subnets', [])
if remote_subnets:
html += "<h3 style='color: #2C3E50; margin-top: 15px;'>Sous-réseaux distants</h3>"
html += "<ul style='margin-top: 5px;'>"
for subnet in remote_subnets:
html += f"<li>{subnet}</li>"
html += "</ul>"
html += "</div>"
record.formatted_data = html
except Exception as e:
_logger.error("Erreur lors du formatage des données VPN: %s", str(e))
record.formatted_data = f"<p><em>Erreur lors du formatage des données: {str(e)}</em></p>"
@api.model
def create_or_update_from_data(self, site, vpn_data):
"""Create or update VPN configuration from UniFi API data
Args:
site (unifi.site): Site record
vpn_data (dict): VPN configuration data from UniFi API
Returns:
unifi.vpn: Created or updated VPN configuration record
"""
_logger.info("Creating or updating VPN configuration from data")
# Extract required fields
name = vpn_data.get('name')
vpn_type = vpn_data.get('vpn_type')
unifi_id = vpn_data.get('_id')
if not name or not vpn_type:
_logger.warning("Missing required fields in VPN data")
return False
# Search for existing VPN configuration
domain = [
('site_id', '=', site.id),
'|',
('name', '=', name),
('unifi_id', '=', unifi_id)
]
existing = self.search(domain, limit=1)
# Prepare values for create/write
vals = {
'name': name,
'vpn_type': vpn_type,
'unifi_id': unifi_id,
'enabled': vpn_data.get('enabled', True),
'peer_ip': vpn_data.get('ipsec_peer_ip'),
'local_ip': vpn_data.get('ipsec_local_ip'),
'interface': vpn_data.get('ifname'),
'purpose': vpn_data.get('purpose'),
'remote_subnets': ', '.join(vpn_data.get('remote_vpn_subnets', [])),
'last_sync': fields.Datetime.now(),
'raw_data': json.dumps(vpn_data, indent=2) if vpn_data else False
}
if existing:
_logger.info("Updating existing VPN configuration: %s", existing.name)
existing.write(vals)
return existing
else:
_logger.info("Creating new VPN configuration: %s", name)
vals['site_id'] = site.id
return self.create(vals)

View file

@ -0,0 +1,446 @@
# -*- coding: utf-8 -*-
# These imports will work in an Odoo environment, even if your IDE marks them as not found
# pylint: disable=import-error
from odoo import models, fields, api, _
from .unifi_common import UnifiCommonMixin
from odoo.exceptions import UserError, ValidationError
# pylint: enable=import-error
import json
import logging
from datetime import datetime
_logger = logging.getLogger(__name__)
class UnifiWifi(models.Model, UnifiCommonMixin):
"""Model for UniFi WiFi networks
This model represents WiFi networks configured in UniFi sites.
It stores configuration details such as SSID, password, security type, etc.
"""
_name = 'unifi.wifi'
_description = 'UniFi WiFi Network'
_order = 'name'
active = fields.Boolean(
string='Active',
default=True,
help='Whether this record is active in Odoo'
)
# Basic information
name = fields.Char(
string='Name',
required=True,
help='Name of the WiFi network (SSID)'
)
wifi_id = fields.Char(
string='WiFi ID',
required=True,
help='Unique identifier for this WiFi network in the UniFi system'
)
# WiFi configuration
enabled = fields.Boolean(
string='Enabled',
default=True,
help='Whether this WiFi network is enabled'
)
hidden = fields.Boolean(
string='Hidden SSID',
default=False,
help='Whether this SSID is hidden from network scans'
)
security = fields.Selection(
selection=[
('open', 'Open'),
('wep', 'WEP'),
('wpapsk', 'WPA Personal'),
('wpa2psk', 'WPA2 Personal'),
('wpapskwpa2psk', 'WPA/WPA2 Personal'),
('wpa3', 'WPA3 Personal'),
('wpa2enterprise', 'WPA2 Enterprise'),
('wpa3enterprise', 'WPA3 Enterprise'),
('radius', 'RADIUS')
],
string='Security Type',
default='wpa2psk',
help='Security protocol used by this WiFi network'
)
password = fields.Char(
string='Password',
help='Password for this WiFi network (stored securely)'
)
# Network configuration
network_id = fields.Many2one(
comodel_name='unifi.network',
string='Network',
help='Network associated with this WiFi'
)
vlan_id = fields.Many2one(
comodel_name='unifi.vlan',
string='VLAN',
help='VLAN associated with this WiFi'
)
# WiFi settings
band = fields.Selection(
selection=[
('2g', '2.4 GHz'),
('5g', '5 GHz'),
('both', '2.4 & 5 GHz')
],
string='Band',
default='both',
help='WiFi frequency band'
)
channel = fields.Integer(
string='Channel',
help='WiFi channel (0 for auto)'
)
channel_width = fields.Selection(
selection=[
('20', '20 MHz'),
('40', '40 MHz'),
('80', '80 MHz'),
('160', '160 MHz')
],
string='Channel Width',
help='WiFi channel width'
)
tx_power = fields.Selection(
selection=[
('auto', 'Auto'),
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
('custom', 'Custom')
],
string='TX Power',
default='auto',
help='Transmission power'
)
tx_power_custom = fields.Integer(
string='Custom TX Power',
help='Custom transmission power in dBm'
)
# Guest network settings
is_guest = fields.Boolean(
string='Guest Network',
default=False,
help='Whether this is a guest WiFi network'
)
guest_policy = fields.Selection(
selection=[
('none', 'No restrictions'),
('mac', 'MAC filtering'),
('auth', 'Authentication required')
],
string='Guest Policy',
default='none',
help='Access policy for guest networks'
)
# Advanced settings
wpa_mode = fields.Selection(
selection=[
('wpa', 'WPA'),
('wpa2', 'WPA2'),
('wpa3', 'WPA3'),
('wpa/wpa2', 'WPA/WPA2'),
('wpa2/wpa3', 'WPA2/WPA3')
],
string='WPA Mode',
help='WPA mode for this network'
)
wpa_encryption = fields.Selection(
selection=[
('auto', 'Auto'),
('ccmp', 'CCMP (AES)'),
('tkip', 'TKIP'),
('tkip-ccmp', 'TKIP/CCMP')
],
string='WPA Encryption',
default='auto',
help='Encryption method used for WPA'
)
pmf_mode = fields.Selection(
selection=[
('disabled', 'Disabled'),
('optional', 'Optional'),
('required', 'Required')
],
string='PMF Mode',
default='optional',
help='Protected Management Frames mode'
)
# Tracking fields
created_at = fields.Datetime(
string='Created At',
default=fields.Datetime.now,
help='When this record was created in Odoo'
)
updated_at = fields.Datetime(
string='Updated At',
help='When this record was last updated in Odoo'
)
last_sync = fields.Datetime(
string='Last Sync',
help='When this record was last synchronized with UniFi'
)
# Raw data
raw_data = fields.Text(
string='Raw Data',
help='Raw JSON data from UniFi API'
)
raw_data_json = fields.Text(
string='Données brutes (JSON)',
compute='_compute_raw_data_json',
help='Données brutes de la configuration WiFi au format JSON formaté'
)
@api.depends('raw_data')
def _compute_raw_data_json(self):
for record in self:
record.raw_data_json = self.format_raw_data_json(record.raw_data)
# Relations
site_id = fields.Many2one(
comodel_name='unifi.site',
string='Site',
required=True,
ondelete='cascade',
help='UniFi site this WiFi network belongs to'
)
# Methods
def create_or_update_from_data(self, site, wifi_data):
"""Create or update a WiFi network from API data
Args:
site: The UniFi site record
wifi_data: The WiFi network data from the API
Returns:
record: The created or updated WiFi network record
"""
# Extract the WiFi ID
wifi_id = wifi_data.get('_id') or wifi_data.get('id')
if not wifi_id:
_logger.error("Cannot create/update WiFi network: missing ID")
return False
# Search for an existing WiFi network with this ID
existing_wifi = self.search([
('wifi_id', '=', wifi_id),
('site_id', '=', site.id)
], limit=1)
# Prepare values for creation/update
vals = {
'wifi_id': wifi_id,
'name': wifi_data.get('name', wifi_data.get('ssid', f"WiFi {wifi_id}")),
'site_id': site.id,
'enabled': wifi_data.get('enabled', True),
'hidden': wifi_data.get('hide_ssid', False),
'security': self._map_security_type(wifi_data.get('security', 'wpa2psk')),
'is_guest': wifi_data.get('is_guest', False),
'band': self._map_band(wifi_data.get('band', 'both')),
'last_sync': fields.Datetime.now(),
'raw_data': json.dumps(wifi_data)
}
# Add password if available (and not empty)
if wifi_data.get('x_passphrase') and wifi_data.get('x_passphrase') != 'null':
vals['password'] = wifi_data.get('x_passphrase')
# Map network and VLAN if available
if wifi_data.get('networkconf_id'):
network = self.env['unifi.network'].search([
('network_id', '=', wifi_data.get('networkconf_id')),
('site_id', '=', site.id)
], limit=1)
if network:
vals['network_id'] = network.id
if wifi_data.get('vlan_id'):
vlan = self.env['unifi.vlan'].search([
('vlan_id', '=', wifi_data.get('vlan_id')),
('site_id', '=', site.id)
], limit=1)
if vlan:
vals['vlan_id'] = vlan.id
# Add advanced settings if available
if 'channel' in wifi_data:
vals['channel'] = wifi_data.get('channel')
if 'channel_width' in wifi_data:
vals['channel_width'] = str(wifi_data.get('channel_width', '20'))
if 'tx_power' in wifi_data:
vals['tx_power'] = self._map_tx_power(wifi_data.get('tx_power'))
if 'tx_power_mode' in wifi_data and wifi_data.get('tx_power_mode') == 'custom':
vals['tx_power'] = 'custom'
vals['tx_power_custom'] = wifi_data.get('tx_power')
# WPA settings
if 'wpa_mode' in wifi_data:
vals['wpa_mode'] = self._map_wpa_mode(wifi_data.get('wpa_mode'))
if 'wpa_enc' in wifi_data:
vals['wpa_encryption'] = self._map_wpa_encryption(wifi_data.get('wpa_enc'))
if 'pmf_mode' in wifi_data:
vals['pmf_mode'] = self._map_pmf_mode(wifi_data.get('pmf_mode'))
if existing_wifi:
# Update existing WiFi network
vals['updated_at'] = fields.Datetime.now()
existing_wifi.write(vals)
return existing_wifi
else:
# Create new WiFi network
return self.create(vals)
def _map_security_type(self, security):
"""Map UniFi security type to model selection value
Args:
security: Security type from UniFi API
Returns:
str: Mapped security type
"""
security_map = {
'open': 'open',
'wep': 'wep',
'wpapsk': 'wpapsk',
'wpa2psk': 'wpa2psk',
'wpapskwpa2psk': 'wpapskwpa2psk',
'wpa3': 'wpa3',
'wpa2enterprise': 'wpa2enterprise',
'wpa3enterprise': 'wpa3enterprise',
'radius': 'radius'
}
return security_map.get(security, 'wpa2psk')
def _map_band(self, band):
"""Map UniFi band to model selection value
Args:
band: Band from UniFi API
Returns:
str: Mapped band
"""
band_map = {
'ng': '2g',
'2g': '2g',
'na': '5g',
'5g': '5g',
'both': 'both',
'all': 'both'
}
return band_map.get(band, 'both')
def _map_tx_power(self, tx_power):
"""Map UniFi TX power to model selection value
Args:
tx_power: TX power from UniFi API
Returns:
str: Mapped TX power
"""
# If it's already a string value that matches our selection options
if tx_power in ['auto', 'low', 'medium', 'high', 'custom']:
return tx_power
# If it's a numeric value, map it to a named level
try:
power = int(tx_power)
if power <= 10:
return 'low'
elif power <= 18:
return 'medium'
else:
return 'high'
except (ValueError, TypeError):
return 'auto'
def _map_wpa_mode(self, wpa_mode):
"""Map UniFi WPA mode to model selection value
Args:
wpa_mode: WPA mode from UniFi API
Returns:
str: Mapped WPA mode
"""
wpa_mode_map = {
'1': 'wpa',
'2': 'wpa2',
'3': 'wpa3',
'12': 'wpa/wpa2',
'23': 'wpa2/wpa3'
}
return wpa_mode_map.get(str(wpa_mode), 'wpa2')
def _map_wpa_encryption(self, wpa_enc):
"""Map UniFi WPA encryption to model selection value
Args:
wpa_enc: WPA encryption from UniFi API
Returns:
str: Mapped WPA encryption
"""
wpa_enc_map = {
'auto': 'auto',
'ccmp': 'ccmp',
'tkip': 'tkip',
'tkip-ccmp': 'tkip-ccmp',
'tkip,ccmp': 'tkip-ccmp'
}
return wpa_enc_map.get(wpa_enc, 'auto')
def _map_pmf_mode(self, pmf_mode):
"""Map UniFi PMF mode to model selection value
Args:
pmf_mode: PMF mode from UniFi API
Returns:
str: Mapped PMF mode
"""
pmf_mode_map = {
'0': 'disabled',
'1': 'optional',
'2': 'required',
'disabled': 'disabled',
'optional': 'optional',
'required': 'required'
}
return pmf_mode_map.get(str(pmf_mode), 'optional')

View file

@ -1,24 +1,24 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
unifi_integration.access_unifi_site_manager,access_unifi_site_manager,unifi_integration.model_unifi_site_manager,base.group_user,1,1,1,0
unifi_integration.access_unifi_auth_session,access_unifi_auth_session,unifi_integration.model_unifi_auth_session,base.group_user,1,0,0,0
unifi_integration.access_unifi_auth_session,access_unifi_auth_session,unifi_integration.model_unifi_auth_session,base.group_user,1,1,1,1
unifi_integration.access_unifi_mfa,access_unifi_mfa,unifi_integration.model_unifi_mfa,base.group_user,1,0,0,0
unifi_integration.access_unifi_api_config,access_unifi_api_config,unifi_integration.model_unifi_api_config,base.group_user,1,0,0,0
unifi_integration.access_unifi_api_log,access_unifi_api_log,unifi_integration.model_unifi_api_log,base.group_user,1,0,0,0
unifi_integration.access_unifi_sync_job,access_unifi_sync_job,unifi_integration.model_unifi_sync_job,base.group_user,1,0,0,0
unifi_integration.access_unifi_device,access_unifi_device,unifi_integration.model_unifi_device,base.group_user,1,0,0,0
unifi_integration.access_unifi_network,access_unifi_network,unifi_integration.model_unifi_network,base.group_user,1,0,0,0
unifi_integration.access_unifi_vlan,access_unifi_vlan,unifi_integration.model_unifi_vlan,base.group_user,1,0,0,0
unifi_integration.access_unifi_user,access_unifi_user,unifi_integration.model_unifi_user,base.group_user,1,0,0,0
unifi_integration.access_unifi_firewall_rule,access_unifi_firewall_rule,unifi_integration.model_unifi_firewall_rule,base.group_user,1,0,0,0
unifi_integration.access_unifi_port_forward,access_unifi_port_forward,unifi_integration.model_unifi_port_forward,base.group_user,1,0,0,0
unifi_integration.access_unifi_system_info,access_unifi_system_info,unifi_integration.model_unifi_system_info,base.group_user,1,0,0,0
unifi_integration.access_unifi_dns,access_unifi_dns,unifi_integration.model_unifi_dns,base.group_user,1,0,0,0
unifi_integration.access_unifi_dns_config,access_unifi_dns_config,unifi_integration.model_unifi_dns_config,base.group_user,1,0,0,0
unifi_integration.access_unifi_routing,access_unifi_routing,unifi_integration.model_unifi_routing,base.group_user,1,0,0,0
unifi_integration.access_unifi_routing_config,access_unifi_routing_config,unifi_integration.model_unifi_routing_config,base.group_user,1,0,0,0
unifi_integration.access_unifi_api_config,access_unifi_api_config,unifi_integration.model_unifi_api_config,base.group_user,1,1,1,0
unifi_integration.access_unifi_api_log,access_unifi_api_log,unifi_integration.model_unifi_api_log,base.group_user,1,1,1,0
unifi_integration.access_unifi_sync_job,access_unifi_sync_job,unifi_integration.model_unifi_sync_job,base.group_user,1,1,1,0
unifi_integration.access_unifi_device,access_unifi_device,unifi_integration.model_unifi_device,base.group_user,1,1,1,1
unifi_integration.access_unifi_network,access_unifi_network,unifi_integration.model_unifi_network,base.group_user,1,1,1,1
unifi_integration.access_unifi_vlan,access_unifi_vlan,unifi_integration.model_unifi_vlan,base.group_user,1,1,1,1
unifi_integration.access_unifi_user,access_unifi_user,unifi_integration.model_unifi_user,base.group_user,1,1,1,1
unifi_integration.access_unifi_firewall_rule,access_unifi_firewall_rule,unifi_integration.model_unifi_firewall_rule,base.group_user,1,1,1,1
unifi_integration.access_unifi_port_forward,access_unifi_port_forward,unifi_integration.model_unifi_port_forward,base.group_user,1,1,1,1
unifi_integration.access_unifi_system_info,access_unifi_system_info,unifi_integration.model_unifi_system_info,base.group_user,1,1,1,1
unifi_integration.access_unifi_dns,access_unifi_dns,unifi_integration.model_unifi_dns,base.group_user,1,1,1,1
unifi_integration.access_unifi_dns_config,access_unifi_dns_config,unifi_integration.model_unifi_dns_config,base.group_user,1,1,1,1
unifi_integration.access_unifi_routing,access_unifi_routing,unifi_integration.model_unifi_routing,base.group_user,1,1,1,1
unifi_integration.access_unifi_routing_config,access_unifi_routing_config,unifi_integration.model_unifi_routing_config,base.group_user,1,1,1,1
unifi_integration.access_unifi_site_import_wizard,access_unifi_site_import_wizard,unifi_integration.model_unifi_site_import_wizard,base.group_user,1,1,1,0
unifi_integration.access_unifi_site_discovery,access_unifi_site_discovery,unifi_integration.model_unifi_site_discovery,base.group_user,1,0,0,0
unifi_integration.access_unifi_site,access_unifi_site,unifi_integration.model_unifi_site,base.group_user,1,1,1,0
unifi_integration.access_unifi_site_controller,access_unifi_site_controller,unifi_integration.model_unifi_site_controller,base.group_user,1,1,1,0
unifi_integration.access_unifi_dashboard_metric,access_unifi_dashboard_metric,unifi_integration.model_unifi_dashboard_metric,base.group_user,1,0,0,0
unifi_integration.access_unifi_wifi,access_unifi_wifi,unifi_integration.model_unifi_wifi,base.group_user,1,1,1,1
unifi_integration.access_unifi_vpn,access_unifi_vpn,unifi_integration.model_unifi_vpn,base.group_user,1,1,1,1
unifi_integration.access_unifi_dashboard_stat,access_unifi_dashboard_stat,unifi_integration.model_unifi_dashboard_stat,base.group_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 unifi_integration.access_unifi_site_manager unifi_integration.access_unifi_auth_session access_unifi_site_manager access_unifi_auth_session unifi_integration.model_unifi_site_manager unifi_integration.model_unifi_auth_session base.group_user 1 1 1 0 1
unifi_integration.access_unifi_auth_session access_unifi_auth_session unifi_integration.model_unifi_auth_session base.group_user 1 0 0 0
3 unifi_integration.access_unifi_mfa access_unifi_mfa unifi_integration.model_unifi_mfa base.group_user 1 0 0 0
4 unifi_integration.access_unifi_api_config access_unifi_api_config unifi_integration.model_unifi_api_config base.group_user 1 0 1 0 1 0
5 unifi_integration.access_unifi_api_log access_unifi_api_log unifi_integration.model_unifi_api_log base.group_user 1 0 1 0 1 0
6 unifi_integration.access_unifi_sync_job access_unifi_sync_job unifi_integration.model_unifi_sync_job base.group_user 1 0 1 0 1 0
7 unifi_integration.access_unifi_device access_unifi_device unifi_integration.model_unifi_device base.group_user 1 0 1 0 1 0 1
8 unifi_integration.access_unifi_network access_unifi_network unifi_integration.model_unifi_network base.group_user 1 0 1 0 1 0 1
9 unifi_integration.access_unifi_vlan access_unifi_vlan unifi_integration.model_unifi_vlan base.group_user 1 0 1 0 1 0 1
10 unifi_integration.access_unifi_user access_unifi_user unifi_integration.model_unifi_user base.group_user 1 0 1 0 1 0 1
11 unifi_integration.access_unifi_firewall_rule access_unifi_firewall_rule unifi_integration.model_unifi_firewall_rule base.group_user 1 0 1 0 1 0 1
12 unifi_integration.access_unifi_port_forward access_unifi_port_forward unifi_integration.model_unifi_port_forward base.group_user 1 0 1 0 1 0 1
13 unifi_integration.access_unifi_system_info access_unifi_system_info unifi_integration.model_unifi_system_info base.group_user 1 0 1 0 1 0 1
14 unifi_integration.access_unifi_dns access_unifi_dns unifi_integration.model_unifi_dns base.group_user 1 0 1 0 1 0 1
15 unifi_integration.access_unifi_dns_config access_unifi_dns_config unifi_integration.model_unifi_dns_config base.group_user 1 0 1 0 1 0 1
16 unifi_integration.access_unifi_routing access_unifi_routing unifi_integration.model_unifi_routing base.group_user 1 0 1 0 1 0 1
17 unifi_integration.access_unifi_routing_config access_unifi_routing_config unifi_integration.model_unifi_routing_config base.group_user 1 0 1 0 1 0 1
18 unifi_integration.access_unifi_site_import_wizard access_unifi_site_import_wizard unifi_integration.model_unifi_site_import_wizard base.group_user 1 1 1 0
19 unifi_integration.access_unifi_site_discovery access_unifi_site_discovery unifi_integration.model_unifi_site_discovery base.group_user 1 0 0 0
20 unifi_integration.access_unifi_site access_unifi_site unifi_integration.model_unifi_site base.group_user 1 1 1 0
unifi_integration.access_unifi_site_controller access_unifi_site_controller unifi_integration.model_unifi_site_controller base.group_user 1 1 1 0
21 unifi_integration.access_unifi_dashboard_metric access_unifi_dashboard_metric unifi_integration.model_unifi_dashboard_metric base.group_user 1 0 0 0
22 unifi_integration.access_unifi_wifi access_unifi_wifi unifi_integration.model_unifi_wifi base.group_user 1 1 1 1
23 unifi_integration.access_unifi_vpn access_unifi_vpn unifi_integration.model_unifi_vpn base.group_user 1 1 1 1
24 unifi_integration.access_unifi_dashboard_stat access_unifi_dashboard_stat unifi_integration.model_unifi_dashboard_stat base.group_user 1 0 0 0

View file

@ -56,5 +56,30 @@
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
<!-- Sync Job Rules -->
<!-- User Group Rule: Read-only access to sync jobs -->
<record id="rule_unifi_sync_job_user" model="ir.rule">
<field name="name">UniFi Sync Job User Access</field>
<field name="model_id" ref="unifi_integration.model_unifi_sync_job"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('unifi_integration.group_unifi_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>
<!-- Manager Group Rule: Full access to sync jobs -->
<record id="rule_unifi_sync_job_manager" model="ir.rule">
<field name="name">UniFi Sync Job Manager Access</field>
<field name="model_id" ref="unifi_integration.model_unifi_sync_job"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('unifi_integration.group_unifi_manager'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="True"/>
</record>
</data>
</odoo>

View file

@ -75,10 +75,10 @@
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
<chatter>
<field name="message_follower_ids"/>
<field name="message_ids"/>
</chatter>
</form>
</field>
</record>

View file

@ -49,24 +49,24 @@
<notebook>
<page string="Request" name="request">
<group>
<field name="request_headers" widget="ace" options="{'mode': 'json'}" readonly="1"/>
<field name="request_params" widget="ace" options="{'mode': 'json'}" readonly="1"/>
<field name="request_body" widget="ace" options="{'mode': 'json'}" readonly="1"/>
<field name="request_headers" widget="ace" options="{'language': 'json'}" readonly="1"/>
<field name="request_params" widget="ace" options="{'language': 'json'}" readonly="1"/>
<field name="request_body" widget="ace" options="{'language': 'json'}" readonly="1"/>
</group>
</page>
<page string="Response" name="response">
<group>
<field name="response_headers" widget="ace" options="{'mode': 'json'}" readonly="1"/>
<field name="response_body" widget="ace" options="{'mode': 'json'}" readonly="1"/>
<field name="response_headers" widget="ace" options="{'language': 'json'}" readonly="1"/>
<field name="response_body" widget="ace" options="{'language': 'json'}" readonly="1"/>
<field name="error_message" invisible="error_message == False" readonly="1"/>
</group>
</page>
<page string="Formatted" name="formatted">
<group>
<separator string="Request"/>
<field name="request_formatted" widget="ace" options="{'mode': 'text'}" readonly="1"/>
<field name="request_formatted" widget="ace" options="{'language': 'text'}" readonly="1"/>
<separator string="Response"/>
<field name="response_formatted" widget="ace" options="{'mode': 'text'}" readonly="1"/>
<field name="response_formatted" widget="ace" options="{'language': 'text'}" readonly="1"/>
</group>
</page>
</notebook>

View file

@ -54,7 +54,10 @@
</group>
<notebook>
<page string="Données brutes">
<field name="raw_data" widget="ace" options="{'mode': 'json'}"/>
<group>
<field name="raw_data" widget="ace" options="{'language': 'json'}" nolabel="1" invisible="1"/>
<field name="raw_data_json" readonly="1" nolabel="1"/>
</group>
</page>
</notebook>
</sheet>

View file

@ -21,11 +21,14 @@
<field name="protocol"/>
</group>
<group>
<field name="source"/>
<field name="src_port"/>
<field name="destination"/>
<field name="dst_port"/>
<field name="detailed_rule_type"/>
<field name="rule_type"/>
<field name="source_type"/>
<field name="formatted_source"/>
<field name="src_port"/>
<field name="destination_type"/>
<field name="formatted_destination"/>
<field name="dst_port"/>
</group>
</group>
<notebook>
@ -40,9 +43,10 @@
<field name="updated_at"/>
<field name="last_sync"/>
</group>
<group string="Données brutes">
<field name="raw_data" widget="ace" options="{'mode': 'json'}"/>
</group>
</page>
<page string="Données brutes">
<field name="raw_data" widget="ace" options="{'language': 'json'}" invisible="1"/>
<field name="raw_data_json" readonly="1"/>
</page>
</notebook>
</sheet>
@ -62,8 +66,12 @@
<field name="enabled"/>
<field name="action"/>
<field name="protocol"/>
<field name="detailed_rule_type"/>
<field name="source_type"/>
<field name="formatted_source"/>
<field name="destination_type"/>
<field name="formatted_destination"/>
<field name="rule_summary"/>
<field name="rule_type"/>
</list>
</field>
</record>
@ -76,8 +84,9 @@
<search string="Rechercher des règles de pare-feu UniFi">
<field name="name" string="Nom"/>
<field name="site_id" string="Site"/>
<field name="source" string="Source"/>
<field name="destination" string="Destination"/>
<field name="formatted_source" string="Source"/>
<field name="formatted_destination" string="Destination"/>
<field name="detailed_rule_type" string="Type détaillé"/>
<separator/>
<filter string="Activées" name="enabled" domain="[('enabled', '=', True)]"/>
<filter string="Désactivées" name="disabled" domain="[('enabled', '=', False)]"/>
@ -92,6 +101,9 @@
<filter string="Tous" name="protocol_all" domain="[('protocol', '=', 'all')]"/>
<group expand="0" string="Grouper par">
<filter string="Site" name="group_by_site" context="{'group_by': 'site_id'}"/>
<filter string="Type détaillé" name="group_by_detailed_type" context="{'group_by': 'detailed_rule_type'}"/>
<filter string="Type de source" name="group_by_source_type" context="{'group_by': 'source_type'}"/>
<filter string="Type de destination" name="group_by_destination_type" context="{'group_by': 'destination_type'}"/>
<filter string="Action" name="group_by_action" context="{'group_by': 'action'}"/>
<filter string="Protocole" name="group_by_protocol" context="{'group_by': 'protocol'}"/>
<filter string="Type de règle" name="group_by_rule_type" context="{'group_by': 'rule_type'}"/>

View file

@ -15,6 +15,9 @@
<field name="site_id"/>
<field name="enabled"/>
<field name="last_sync"/>
<header>
<button name="sync_networks_from_list" string="Synchroniser tous les réseaux" type="object" class="btn-primary"/>
</header>
</list>
</field>
</record>
@ -68,7 +71,10 @@
</group>
</page>
<page string="Données brutes">
<field name="raw_data" widget="ace" options="{'mode': 'json'}"/>
<group>
<field name="raw_data" widget="ace" options="{'language': 'json'}" nolabel="1" invisible="1"/>
<field name="raw_data_json" readonly="1" nolabel="1"/>
</group>
</page>
</notebook>
</sheet>

View file

@ -29,20 +29,25 @@
<group string="Description">
<field name="description" nolabel="1" placeholder="Description détaillée de la redirection de port..."/>
</group>
<group string="Informations techniques" groups="base.group_system">
<group>
<field name="rule_id"/>
<field name="rule_index"/>
</group>
<group>
<field name="created_at"/>
<field name="updated_at"/>
<field name="last_sync"/>
</group>
</group>
<group string="Données brutes" groups="base.group_system">
<field name="raw_data" nolabel="1"/>
</group>
<notebook>
<page string="Informations techniques" groups="base.group_system">
<group>
<group>
<field name="rule_id"/>
<field name="rule_index"/>
</group>
<group>
<field name="created_at"/>
<field name="updated_at"/>
<field name="last_sync"/>
</group>
</group>
</page>
<page string="Données brutes" groups="base.group_system">
<field name="raw_data" widget="ace" options="{'language': 'json'}" readonly="1" invisible="1"/>
<field name="raw_data_json" readonly="1"/>
</page>
</notebook>
</sheet>
</form>
</field>

View file

@ -24,10 +24,7 @@
<field name="model">unifi.site</field>
<field name="arch" type="xml">
<form string="UniFi Site">
<header>
<button name="action_sync_now" string="Synchronize Now" type="object" class="btn-primary"/>
<button name="action_test_connection" string="Test Connection" type="object" class="btn-secondary"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<!-- Boutons statistiques communs à tous les types d'API -->
@ -35,7 +32,7 @@
type="object"
class="oe_stat_button"
icon="fa-history">
<div class="o_stat_info">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">API Logs</span>
</div>
</button>
@ -43,7 +40,7 @@
type="object"
class="oe_stat_button"
icon="fa-refresh">
<div class="o_stat_info">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Sync Jobs</span>
</div>
</button>
@ -53,44 +50,79 @@
type="object"
class="oe_stat_button"
icon="fa-sitemap"
invisible="context.get('api_type') != 'controller'">
invisible="api_type != 'controller'">
<field name="network_count" widget="statinfo" string="Networks"/>
</button>
<button name="action_view_devices"
type="object"
class="oe_stat_button"
icon="fa-server"
invisible="context.get('api_type') != 'controller'">
invisible="api_type != 'controller'">
<field name="device_count" widget="statinfo" string="Devices"/>
</button>
<button name="action_view_users"
type="object"
class="oe_stat_button"
icon="fa-users"
invisible="context.get('api_type') != 'controller'">
invisible="api_type != 'controller'">
<field name="user_count" widget="statinfo" string="Users"/>
</button>
<button name="action_view_vlans"
type="object"
class="oe_stat_button"
icon="fa-tags"
invisible="api_type != 'controller'">
<field name="vlan_count" widget="statinfo" string="VLANs"/>
</button>
<button name="action_view_port_forwards"
type="object"
class="oe_stat_button"
icon="fa-exchange"
invisible="api_type != 'controller'">
<field name="port_forward_count" widget="statinfo" string="Port Forwards"/>
</button>
<button name="action_view_system_info"
type="object"
class="oe_stat_button"
icon="fa-info-circle"
invisible="api_type != 'controller'">
<field name="system_info_count" widget="statinfo" string="System Info"/>
</button>
<button name="action_view_dns"
type="object"
class="oe_stat_button"
icon="fa-globe"
invisible="api_type != 'controller'">
<field name="dns_count" widget="statinfo" string="DNS Entries"/>
</button>
<button name="action_view_vpn"
type="object"
class="oe_stat_button"
icon="fa-lock"
invisible="api_type != 'controller'">
<field name="vpn_count" widget="statinfo" string="VPN"/>
</button>
<!-- Boutons spécifiques au Site Manager -->
<button name="action_view_firewall_rules"
type="object"
class="oe_stat_button"
icon="fa-shield"
invisible="context.get('api_type') != 'site_manager'">
<field name="firewall_rule_count" widget="statinfo" string="Firewall Rules"/>
icon="fa-shield">
<field name="firewall_rule_count" widget="statinfo" string="Règles pare-feu"/>
</button>
</div>
<widget name="web_ribbon" title="Inactive" bg_color="text-bg-danger" invisible="active"/>
<div class="oe_title">
<h1>
<field name="name" placeholder="Site Name"/>
<h1 class="d-flex flex-row">
<field name="name" class="o_text_overflow" placeholder="Site Name"/>
</h1>
</div>
<group>
<group>
<field name="site_id"/>
<field name="api_type" widget="radio"/>
<field name="active"/>
<field name="active" invisible="1"/>
</group>
<group>
<field name="timestamp" readonly="1"/>
@ -100,28 +132,28 @@
</group>
<!-- Informations de connexion pour Controller API -->
<group string="Informations de connexion" invisible="context.get('api_type') != 'controller'">
<group string="Informations de connexion" invisible="api_type != 'controller'">
<group>
<field name="host" required="context.get('api_type') == 'controller'"/>
<field name="port" required="context.get('api_type') == 'controller'"/>
<field name="host" required="api_type == 'controller'"/>
<field name="port" required="api_type == 'controller'"/>
</group>
<group>
<field name="username" required="context.get('api_type') == 'controller'"/>
<field name="password" password="True" required="context.get('api_type') == 'controller'"/>
<field name="username" required="api_type == 'controller'"/>
<field name="password" password="True" required="api_type == 'controller'"/>
<field name="verify_ssl"/>
</group>
</group>
<!-- Informations de connexion pour Site Manager API -->
<group string="Informations de connexion" invisible="context.get('api_type') != 'site_manager'">
<group string="Informations de connexion" invisible="api_type != 'site_manager'">
<group>
<field name="api_key" required="context.get('api_type') == 'site_manager'"/>
<field name="api_key" required="api_type == 'site_manager'"/>
</group>
<group>
<field name="mfa_enabled" invisible="context.get('api_type') != 'site_manager'"/>
<field name="mfa_enabled" invisible="api_type != 'site_manager'"/>
<field name="mfa_token" password="True"
invisible="context.get('api_type') != 'site_manager' or not context.get('mfa_enabled')"
required="context.get('api_type') == 'site_manager' and context.get('mfa_enabled')"/>
invisible="api_type != 'site_manager' or not mfa_enabled"
required="api_type == 'site_manager' and mfa_enabled"/>
</group>
</group>
<notebook>
@ -133,113 +165,298 @@
</page>
<!-- Onglets spécifiques au Controller -->
<page string="Networks" name="networks" invisible="context.get('api_type') != 'controller'">
<div class="oe_button_box">
<button name="action_sync_networks" string="Synchronize Networks" type="object" class="btn btn-secondary"/>
</div>
<field name="network_ids" readonly="1">
<list>
<field name="name"/>
</list>
</field>
</page>
<page string="Devices" name="devices" invisible="context.get('api_type') != 'controller'">
<div class="oe_button_box">
<button name="action_sync_devices" string="Synchronize Devices" type="object" class="btn btn-secondary"/>
</div>
<field name="device_ids" readonly="1">
<list>
<field name="name"/>
</list>
</field>
</page>
<page string="Users" name="users" invisible="context.get('api_type') != 'controller'">
<div class="oe_button_box">
<button name="action_sync_users" string="Synchronize Users" type="object" class="btn btn-secondary"/>
</div>
<field name="user_ids" readonly="1">
<list>
<field name="name"/>
</list>
</field>
</page>
<!-- Onglets spécifiques au Site Manager -->
<page string="VLANs" name="vlans" invisible="context.get('api_type') != 'site_manager'">
<div class="oe_button_box">
<button name="action_sync_vlans" string="Synchronize VLANs" type="object" class="btn btn-secondary"/>
</div>
<field name="vlan_ids" readonly="1">
<list>
<field name="name"/>
</list>
</field>
</page>
<page string="Firewall Rules" name="firewall_rules" invisible="context.get('api_type') != 'site_manager'">
<div class="oe_button_box">
<button name="action_sync_firewall_rules" string="Synchronize Firewall Rules" type="object" class="btn btn-secondary"/>
</div>
<field name="firewall_rule_ids" readonly="1">
<list>
<field name="name"/>
</list>
</field>
</page>
<page string="Port Forwarding" name="port_forwarding" invisible="context.get('api_type') != 'site_manager'">
<div class="oe_button_box">
<button name="action_sync_port_forwards" string="Synchronize Port Forwards" type="object" class="btn btn-secondary"/>
</div>
<field name="port_forward_ids" readonly="1">
<list>
<field name="name"/>
</list>
</field>
</page>
<page string="Routing" name="routing" invisible="context.get('api_type') != 'site_manager'">
<div class="oe_button_box">
<button name="action_sync_routing" string="Synchronize Routing" type="object" class="btn btn-primary"/>
</div>
<field name="routing_config_ids" readonly="1"/>
</page>
<page string="Connection Information" name="connection_info">
<group invisible="context.get('api_type') != 'controller'">
<group string="Controller API Settings">
<button string="Configure Controller API" name="action_configure_controller" type="object" class="btn-primary"/>
<field name="verify_ssl"/>
</group>
</group>
<group invisible="context.get('api_type') != 'site_manager'">
<group string="Site Manager API Settings">
<button string="Configure Site Manager API" name="action_configure_site_manager" type="object" class="btn-primary"/>
<field name="verify_ssl"/>
</group>
</group>
</page>
<page string="Synchronization" name="sync_settings">
<page string="Networks" name="networks" invisible="api_type != 'controller'">
<group>
<field name="auto_sync"/>
<field name="sync_interval" invisible="not context.get('auto_sync')"/>
<field name="last_update" readonly="1"/>
</group>
<group string="Recent Sync Jobs" invisible="not context.get('sync_job_ids')">
<field name="sync_job_ids" nolabel="1" readonly="1">
<div class="w-100 mb-2">
<button name="action_sync_networks" string="Synchronize Networks" type="object" class="btn btn-secondary"/>
</div>
<field name="network_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
<field name="state"/>
<field name="start_time"/>
<field name="end_time"/>
<field name="duration" widget="float_time"/>
<field name="purpose"/>
<field name="subnet"/>
</list>
</field>
</group>
</page>
<page string="Devices" name="devices" invisible="api_type != 'controller'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_devices" string="Synchronize Devices" type="object" class="btn btn-secondary"/>
</div>
<field name="device_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
<field name="model"/>
<field name="ip_address"/>
</list>
</field>
</group>
</page>
<page string="Users" name="users" invisible="api_type != 'controller'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_users" string="Synchronize Users" type="object" class="btn btn-secondary"/>
</div>
<field name="user_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
<field name="email"/>
</list>
</field>
</group>
</page>
<page string="WiFi Networks" name="wifi_networks" invisible="api_type != 'controller'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_wifi" string="Synchronize WiFi Networks" type="object" class="btn btn-secondary"/>
</div>
<field name="wifi_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
<field name="security"/>
<field name="band"/>
<field name="is_guest"/>
<field name="enabled"/>
</list>
</field>
</group>
</page>
<page string="VLANs" name="vlans_controller" invisible="api_type != 'controller'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_vlans" string="Synchronize VLANs" type="object" class="btn btn-secondary"/>
</div>
<field name="vlan_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
<field name="vlan_id"/>
</list>
</field>
</group>
</page>
<page string="Firewall Rules" name="firewall_rules_controller" invisible="api_type != 'controller'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_firewall_rules" string="Synchronize Firewall Rules" type="object" class="btn btn-secondary"/>
</div>
<field name="firewall_rule_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
<field name="rule_type"/>
</list>
</field>
</group>
</page>
<page string="Port Forwarding" name="port_forwarding_controller" invisible="api_type != 'controller'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_port_forwards" string="Synchronize Port Forwards" type="object" class="btn btn-secondary"/>
</div>
<field name="port_forward_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
<field name="fwd_port"/>
<field name="dst_port"/>
</list>
</field>
</group>
</page>
<page string="Routing" name="routing_controller" invisible="api_type != 'controller'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_routing" string="Synchronize Routing" type="object" class="btn btn-secondary"/>
</div>
<field name="routing_config_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
</list>
</field>
</group>
</page>
<page string="DNS" name="dns_controller" invisible="api_type != 'controller'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_dns" string="Synchronize DNS" type="object" class="btn btn-secondary"/>
</div>
<field name="dns_ids" readonly="1" nolabel="1">
<list>
<field name="hostname"/>
<field name="ip_address"/>
</list>
</field>
</group>
</page>
<page string="System Info" name="system_info_controller" invisible="api_type != 'controller'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_system_info" string="Synchronize System Info" type="object" class="btn btn-secondary"/>
</div>
<field name="system_info_ids" readonly="1" nolabel="1">
<list>
<field name="hostname"/>
<field name="version"/>
</list>
</field>
</group>
</page>
<!-- Onglets spécifiques au Site Manager -->
<page string="VLANs" name="vlans" invisible="api_type != 'site_manager'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_vlans" string="Synchronize VLANs" type="object" class="btn btn-secondary"/>
</div>
<field name="vlan_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
<field name="vlan_id"/>
</list>
</field>
</group>
</page>
<page string="Firewall Rules" name="firewall_rules" invisible="api_type != 'site_manager'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_firewall_rules" string="Synchronize Firewall Rules" type="object" class="btn btn-secondary"/>
</div>
<field name="firewall_rule_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
<field name="rule_type"/>
</list>
</field>
</group>
</page>
<page string="Port Forwarding" name="port_forwarding" invisible="api_type != 'site_manager'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_port_forwards" string="Synchronize Port Forwards" type="object" class="btn btn-secondary"/>
</div>
<field name="port_forward_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
<field name="fwd_port"/>
<field name="dst_port"/>
</list>
</field>
</group>
</page>
<page string="Routing" name="routing" invisible="api_type != 'site_manager'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_routing" string="Synchronize Routing" type="object" class="btn btn-secondary"/>
</div>
<field name="routing_config_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
</list>
</field>
</group>
</page>
<page string="DNS" name="dns" invisible="api_type != 'site_manager'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_dns" string="Synchronize DNS" type="object" class="btn btn-secondary"/>
</div>
<field name="dns_ids" readonly="1" nolabel="1">
<list>
<field name="hostname"/>
<field name="ip_address"/>
</list>
</field>
</group>
</page>
<page string="System Info" name="system_info" invisible="api_type != 'site_manager'">
<group>
<div class="w-100 mb-2">
<button name="action_sync_system_info" string="Synchronize System Info" type="object" class="btn btn-secondary"/>
</div>
<field name="system_info_ids" readonly="1" nolabel="1">
<list>
<field name="hostname"/>
<field name="version"/>
</list>
</field>
</group>
</page>
<page string="DNS Config" name="dns_config">
<group>
<field name="dns_config_ids" readonly="1" nolabel="1">
<list>
<field name="site_id"/>
<field name="enabled"/>
<field name="filters_enabled"/>
<field name="custom_dns"/>
<field name="forwarding_enabled"/>
</list>
</field>
</group>
</page>
<page string="Auth Sessions" name="auth_sessions">
<group>
<field name="auth_session_id" readonly="1"/>
</group>
</page>
<page string="Settings" name="settings">
<group>
<group string="API Connection Settings" name="api_settings">
<field name="verify_ssl"/>
<field name="timeout" widget="float_time"/>
<field name="max_retries"/>
</group>
<group string="Sync Settings" name="sync_settings">
<field name="auto_sync"/>
<field name="sync_interval" invisible="not auto_sync"/>
<div class="text-muted o_row ps-1" invisible="not auto_sync">
<i class="fa fa-info-circle"/> L'automatisation s'exécutera tous les <field name="sync_interval" class="oe_inline" nolabel="1"/> minutes
</div>
<field name="last_update" readonly="1"/>
</group>
</group>
<group>
<group string="Controller API Settings" invisible="api_type != 'controller'">
<button string="Configure Controller API" name="action_configure_controller" type="object" class="btn-primary"/>
</group>
<group string="Site Manager API Settings" invisible="api_type != 'site_manager'">
<button string="Configure Site Manager API" name="action_configure_site_manager" type="object" class="btn-primary"/>
</group>
</group>
</page>
<page string="Sync Jobs" name="sync_jobs">
<field name="sync_job_ids" readonly="1" nolabel="1">
<list>
<field name="name"/>
<field name="state"/>
<field name="start_time"/>
<field name="end_time"/>
<field name="duration" widget="float_time"/>
</list>
</field>
</page>
<page string="API Logs" name="api_logs">
<field name="api_log_ids" readonly="1">
<field name="api_log_ids" readonly="1" nolabel="1">
<list>
<field name="create_date"/>
<field name="api_type"/>
@ -251,16 +468,17 @@
</list>
</field>
</page>
<page string="Raw Data" name="raw_data" groups="base.group_system">
<field name="raw_data" widget="ace" options="{'mode': 'json'}"/>
<page string="Données brutes" name="raw_data" groups="base.group_system">
<field name="raw_data" widget="ace" options="{'language': 'json'}" invisible="1"/>
<field name="raw_data_json" readonly="1"/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
</div>
<chatter>
<field name="message_follower_ids"/>
<field name="message_ids"/>
</chatter>
</form>
</field>
</record>
@ -277,13 +495,13 @@
<field name="api_type" widget="radio" required="1"/>
</group>
<group invisible="context.get('api_type') != 'controller'">
<group invisible="api_type != 'controller'">
<group string="Controller API Settings">
<button string="Configure Controller API" name="action_configure_controller" type="object" class="btn-primary"/>
</group>
</group>
<group invisible="context.get('api_type') != 'site_manager'">
<group invisible="api_type != 'site_manager'">
<group string="Site Manager API Settings">
<button string="Configure Site Manager API" name="action_configure_site_manager" type="object" class="btn-primary"/>
</group>

View file

@ -1,285 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- UniFi Site List View -->
<record id="view_unifi_site_list" model="ir.ui.view">
<field name="name">unifi.site.list</field>
<field name="model">unifi.site</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="UniFi Sites" js_class="unifi_site_list">
<field name="name"/>
<field name="site_id"/>
<field name="api_type"/>
<field name="active"/>
<field name="last_sync" optional="show"/>
<field name="create_date" optional="show"/>
<button name="action_import_site" string="Importer" type="object" icon="fa-download" class="oe_highlight"/>
</list>
</field>
</record>
<!-- UniFi Site Form View -->
<record id="view_unifi_site_form" model="ir.ui.view">
<field name="name">unifi.site.form</field>
<field name="model">unifi.site</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="UniFi Site">
<header>
<button name="action_sync_now" string="Synchronize Now" type="object" class="btn-primary"/>
<button name="action_test_connection" string="Test Connection" type="object" class="btn-secondary"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<!-- Boutons statistiques communs à tous les types d'API -->
<button name="action_view_api_logs"
type="object"
class="oe_stat_button"
icon="fa-history">
<div class="o_stat_info">
<span class="o_stat_text">API Logs</span>
</div>
</button>
<button name="action_view_sync_jobs"
type="object"
class="oe_stat_button"
icon="fa-refresh">
<div class="o_stat_info">
<span class="o_stat_text">Sync Jobs</span>
</div>
</button>
<!-- Boutons spécifiques au Controller -->
<button name="action_view_networks"
type="object"
class="oe_stat_button"
icon="fa-sitemap"
invisible="api_type != 'controller'">
<field name="network_count" widget="statinfo" string="Networks"/>
</button>
<button name="action_view_devices"
type="object"
class="oe_stat_button"
icon="fa-server"
invisible="api_type != 'controller'">
<field name="device_count" widget="statinfo" string="Devices"/>
</button>
<button name="action_view_users"
type="object"
class="oe_stat_button"
icon="fa-users"
invisible="api_type != 'controller'">
<field name="user_count" widget="statinfo" string="Users"/>
</button>
<!-- Boutons spécifiques au Site Manager -->
<button name="action_view_firewall_rules"
type="object"
class="oe_stat_button"
icon="fa-shield"
invisible="api_type != 'site_manager'">
<field name="firewall_rule_count" widget="statinfo" string="Firewall Rules"/>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" placeholder="Site Name"/>
</h1>
</div>
<group>
<group>
<field name="site_id"/>
<field name="api_type" widget="radio"/>
<field name="active"/>
</group>
<group>
<field name="timestamp" readonly="1"/>
<field name="last_sync" readonly="1"/>
<field name="import_status" readonly="1"/>
</group>
</group>
<notebook>
<page string="Site Information" name="site_info">
<group>
<field name="description" placeholder="Description of this site"/>
<field name="address" placeholder="Physical address of this site"/>
</group>
</page>
<!-- Onglets spécifiques au Controller -->
<page string="Networks" name="networks" invisible="api_type != 'controller'">
<div class="oe_button_box">
<button name="action_sync_networks" string="Synchronize Networks" type="object" class="btn btn-secondary"/>
</div>
<field name="network_ids" readonly="1">
<tree>
<field name="name"/>
</tree>
</field>
</page>
<page string="Devices" name="devices" invisible="api_type != 'controller'">
<div class="oe_button_box">
<button name="action_sync_devices" string="Synchronize Devices" type="object" class="btn btn-secondary"/>
</div>
<field name="device_ids" readonly="1">
<tree>
<field name="name"/>
</tree>
</field>
</page>
<page string="Users" name="users" invisible="api_type != 'controller'">
<div class="oe_button_box">
<button name="action_sync_users" string="Synchronize Users" type="object" class="btn btn-secondary"/>
</div>
<field name="user_ids" readonly="1">
<tree>
<field name="name"/>
</tree>
</field>
</page>
<!-- Onglets spécifiques au Site Manager -->
<page string="VLANs" name="vlans" invisible="api_type != 'site_manager'">
<div class="oe_button_box">
<button name="action_sync_vlans" string="Synchronize VLANs" type="object" class="btn btn-secondary"/>
</div>
<field name="vlan_ids" readonly="1">
<tree>
<field name="name"/>
</tree>
</field>
</page>
<page string="Firewall Rules" name="firewall_rules" invisible="api_type != 'site_manager'">
<div class="oe_button_box">
<button name="action_sync_firewall_rules" string="Synchronize Firewall Rules" type="object" class="btn btn-secondary"/>
</div>
<field name="firewall_rule_ids" readonly="1">
<tree>
<field name="name"/>
</tree>
</field>
</page>
<page string="Port Forwarding" name="port_forwarding" invisible="api_type != 'site_manager'">
<div class="oe_button_box">
<button name="action_sync_port_forwards" string="Synchronize Port Forwards" type="object" class="btn btn-secondary"/>
</div>
<field name="port_forward_ids" readonly="1">
<tree>
<field name="name"/>
</tree>
</field>
</page>
<page string="Routing" name="routing" invisible="api_type != 'site_manager'">
<div class="oe_button_box">
<button name="action_sync_routing" string="Synchronize Routing" type="object" class="btn btn-secondary"/>
</div>
<field name="routing_config_ids" readonly="1">
<tree>
<field name="ospf_enabled"/>
<field name="bgp_enabled"/>
<field name="rip_enabled"/>
<field name="static_routes"/>
</tree>
</field>
</page>
<page string="Connection Information" name="connection_info">
<group invisible="api_type != 'controller'">
<group string="Controller API Settings">
<button string="Configure Controller API" name="action_configure_controller" type="object" class="btn-primary"/>
<field name="verify_ssl" invisible="api_type != 'controller'"/>
</group>
</group>
<group invisible="api_type != 'site_manager'">
<group string="Site Manager API Settings">
<button string="Configure Site Manager API" name="action_configure_site_manager" type="object" class="btn-primary"/>
<field name="verify_ssl" invisible="api_type != 'site_manager'"/>
</group>
</group>
</page>
<page string="Synchronization" name="sync_settings">
<group>
<field name="auto_sync"/>
<field name="sync_interval" invisible="auto_sync == False"/>
<field name="last_update" readonly="1"/>
</group>
<group string="Recent Sync Jobs" invisible="sync_job_ids == []">
<field name="sync_job_ids" nolabel="1" readonly="1">
<list>
<field name="name"/>
<field name="state"/>
<field name="start_time"/>
<field name="end_time"/>
<field name="duration" widget="float_time"/>
</list>
</field>
</group>
</page>
<page string="API Logs" name="api_logs">
<field name="api_log_ids" readonly="1">
<list>
<field name="create_date"/>
<field name="api_type"/>
<field name="method"/>
<field name="endpoint"/>
<field name="status_code"/>
<field name="success"/>
<field name="duration" widget="float_time"/>
</list>
</field>
</page>
<page string="Raw Data" name="raw_data" groups="base.group_system">
<field name="raw_data" widget="ace" options="{'mode': 'json'}"/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<!-- Import UniFi Site Form View -->
<record id="view_unifi_site_import_form" model="ir.ui.view">
<field name="name">unifi.site.import.form</field>
<field name="model">unifi.site</field>
<field name="arch" type="xml">
<form string="Import UniFi Site">
<group>
<field name="name" required="1" placeholder="Site Name"/>
<field name="site_id" required="1" default="default"/>
<field name="api_type" widget="radio" required="1"/>
</group>
<group invisible="api_type != 'controller'">
<group string="Controller API Settings">
<button string="Configure Controller API" name="action_configure_controller" type="object" class="btn-primary"/>
</group>
</group>
<group invisible="api_type != 'site_manager'">
<group string="Site Manager API Settings">
<button string="Configure Site Manager API" name="action_configure_site_manager" type="object" class="btn-primary"/>
</group>
</group>
<footer>
<button string="Import" name="action_test_connection" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<!-- Actions moved to unifi_actions.xml -->
</odoo>

View file

@ -51,8 +51,9 @@
<field name="last_sync" readonly="1"/>
</group>
<notebook>
<page string="Raw Data" name="raw_data">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" help="Raw JSON data of the configuration"/>
<page string="Données brutes" name="raw_data">
<field name="raw_data" widget="ace" options="{'language': 'json'}" help="Raw JSON data of the configuration" invisible="1"/>
<field name="raw_data_json" readonly="1"/>
</page>
</notebook>
</sheet>

View file

@ -39,9 +39,10 @@
<field name="updated_at"/>
<field name="last_sync"/>
</group>
<group string="Données brutes">
<field name="raw_data" widget="ace" options="{'mode': 'json'}"/>
</group>
</page>
<page string="Données brutes">
<field name="raw_data" widget="ace" options="{'language': 'json'}" invisible="1"/>
<field name="raw_data_json" readonly="1"/>
</page>
</notebook>
</sheet>

View file

@ -49,9 +49,10 @@
<field name="created_at"/>
<field name="updated_at"/>
</group>
<group string="Données brutes">
<field name="raw_data" widget="ace" options="{'mode': 'json'}" readonly="1"/>
</group>
</page>
<page string="Données brutes">
<field name="raw_data" widget="ace" options="{'language': 'json'}" readonly="1" invisible="1"/>
<field name="raw_data_json" readonly="1"/>
</page>
</notebook>
</sheet>

View file

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- UniFi VPN List View -->
<record id="view_unifi_vpn_list" model="ir.ui.view">
<field name="name">unifi.vpn.list</field>
<field name="model">unifi.vpn</field>
<field name="arch" type="xml">
<list string="VPN Configurations">
<field name="name"/>
<field name="vpn_type"/>
<field name="site_id"/>
<field name="peer_ip"/>
<field name="enabled"/>
<field name="last_sync" optional="show"/>
</list>
</field>
</record>
<!-- UniFi VPN Form View -->
<record id="view_unifi_vpn_form" model="ir.ui.view">
<field name="name">unifi.vpn.form</field>
<field name="model">unifi.vpn</field>
<field name="arch" type="xml">
<form string="VPN Configuration">
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="VPN Name"/>
</h1>
</div>
<group>
<group>
<field name="site_id"/>
<field name="vpn_type"/>
<field name="enabled"/>
<field name="purpose"/>
</group>
<group>
<field name="peer_ip"/>
<field name="local_ip"/>
<field name="interface"/>
<field name="remote_subnets"/>
</group>
</group>
<notebook>
<page string="Configuration formatée" name="formatted_config">
<field name="formatted_data" nolabel="1"/>
</page>
<page string="Données brutes" name="raw_data">
<group>
<field name="raw_data" widget="ace" options="{'language': 'json'}" nolabel="1" invisible="1"/>
<field name="raw_data_json" readonly="1" nolabel="1"/>
</group>
</page>
<page string="Informations système" name="system_info">
<group>
<field name="unifi_id"/>
<field name="last_sync"/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- UniFi VPN Search View -->
<record id="view_unifi_vpn_search" model="ir.ui.view">
<field name="name">unifi.vpn.search</field>
<field name="model">unifi.vpn</field>
<field name="arch" type="xml">
<search string="Search VPN Configurations">
<field name="name"/>
<field name="site_id"/>
<field name="vpn_type"/>
<field name="peer_ip"/>
<field name="local_ip"/>
<separator/>
<filter string="Actifs" name="active" domain="[('enabled', '=', True)]"/>
<filter string="Inactifs" name="inactive" domain="[('enabled', '=', False)]"/>
<group expand="0" string="Grouper par">
<filter string="Site" name="group_by_site" context="{'group_by': 'site_id'}"/>
<filter string="Type" name="group_by_type" context="{'group_by': 'vpn_type'}"/>
</group>
</search>
</field>
</record>
<!-- UniFi VPN Action -->
<record id="action_unifi_vpn" model="ir.actions.act_window">
<field name="name">VPN Configurations</field>
<field name="res_model">unifi.vpn</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_unifi_vpn_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Aucune configuration VPN trouvée
</p>
<p>
Les configurations VPN sont synchronisées depuis le contrôleur UniFi.
</p>
</field>
</record>
<!-- Menu Item -->
<menuitem id="menu_unifi_vpn"
name="VPN"
parent="menu_unifi_network_config"
action="action_unifi_vpn"
sequence="40"/>
</odoo>

View file

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View -->
<record id="view_unifi_wifi_form" model="ir.ui.view">
<field name="name">unifi.wifi.form</field>
<field name="model">unifi.wifi</field>
<field name="arch" type="xml">
<form string="WiFi Network">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button" options="{'terminology': 'archive'}"/>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" placeholder="WiFi Network Name (SSID)"/>
</h1>
</div>
<group>
<group string="Basic Information">
<field name="wifi_id"/>
<field name="site_id"/>
<field name="enabled"/>
<field name="hidden"/>
<field name="is_guest"/>
</group>
<group string="Security">
<field name="security"/>
<field name="password" password="True" invisible="security == 'open'"/>
<field name="wpa_mode" invisible="security not in ('wpapsk', 'wpa2psk', 'wpapskwpa2psk', 'wpa3')"/>
<field name="wpa_encryption" invisible="security not in ('wpapsk', 'wpa2psk', 'wpapskwpa2psk', 'wpa3')"/>
<field name="pmf_mode" invisible="security not in ('wpa2psk', 'wpapskwpa2psk', 'wpa3')"/>
</group>
</group>
<notebook>
<page string="Network Configuration">
<group>
<group string="Network">
<field name="network_id"/>
<field name="vlan_id"/>
</group>
<group string="Radio">
<field name="band"/>
<field name="channel"/>
<field name="channel_width"/>
<field name="tx_power"/>
<field name="tx_power_custom" invisible="tx_power != 'custom'"/>
</group>
</group>
</page>
<page string="Guest Settings" invisible="is_guest == False">
<group>
<field name="guest_policy"/>
</group>
</page>
<page string="Technical Information">
<group>
<group string="Timestamps">
<field name="created_at"/>
<field name="updated_at"/>
<field name="last_sync"/>
</group>
<group string="Raw Data">
<field name="raw_data" widget="ace" options="{'language': 'json'}" readonly="1" nolabel="1" invisible="1"/>
<field name="raw_data_json" readonly="1" nolabel="1"/>
</group>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- List View -->
<record id="view_unifi_wifi_list" model="ir.ui.view">
<field name="name">unifi.wifi.list</field>
<field name="model">unifi.wifi</field>
<field name="arch" type="xml">
<list string="WiFi Networks">
<field name="name"/>
<field name="security"/>
<field name="band"/>
<field name="is_guest"/>
<field name="enabled"/>
<field name="hidden"/>
<field name="site_id"/>
<field name="network_id"/>
<field name="vlan_id"/>
<field name="last_sync"/>
</list>
</field>
</record>
<!-- Search View -->
<record id="view_unifi_wifi_search" model="ir.ui.view">
<field name="name">unifi.wifi.search</field>
<field name="model">unifi.wifi</field>
<field name="arch" type="xml">
<search string="Search WiFi Networks">
<field name="name"/>
<field name="wifi_id"/>
<field name="site_id"/>
<field name="network_id"/>
<field name="vlan_id"/>
<separator/>
<filter string="Enabled" name="enabled" domain="[('enabled', '=', True)]"/>
<filter string="Disabled" name="disabled" domain="[('enabled', '=', False)]"/>
<separator/>
<filter string="Guest Networks" name="guest" domain="[('is_guest', '=', True)]"/>
<filter string="Hidden Networks" name="hidden" domain="[('hidden', '=', True)]"/>
<separator/>
<filter string="2.4 GHz" name="band_2g" domain="[('band', 'in', ['2g', 'both'])]"/>
<filter string="5 GHz" name="band_5g" domain="[('band', 'in', ['5g', 'both'])]"/>
<separator/>
<filter string="Open Networks" name="open" domain="[('security', '=', 'open')]"/>
<filter string="Secured Networks" name="secured" domain="[('security', '!=', 'open')]"/>
<group expand="0" string="Group By">
<filter string="Site" name="group_by_site" context="{'group_by': 'site_id'}"/>
<filter string="Security Type" name="group_by_security" context="{'group_by': 'security'}"/>
<filter string="Band" name="group_by_band" context="{'group_by': 'band'}"/>
<filter string="Network" name="group_by_network" context="{'group_by': 'network_id'}"/>
<filter string="VLAN" name="group_by_vlan" context="{'group_by': 'vlan_id'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_unifi_wifi" model="ir.actions.act_window">
<field name="name">WiFi Networks</field>
<field name="res_model">unifi.wifi</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_unifi_wifi_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No WiFi networks found
</p>
<p>
WiFi networks will appear here after synchronizing with your UniFi controller.
</p>
</field>
</record>
<!-- Menu Item -->
<menuitem id="menu_unifi_wifi"
name="WiFi Networks"
parent="menu_unifi_networks"
action="action_unifi_wifi"
sequence="30"/>
</odoo>