ipwatch
This commit is contained in:
588
mqtt/docs/MQTT_CODING_GUIDELINES.md
Normal file
588
mqtt/docs/MQTT_CODING_GUIDELINES.md
Normal file
@@ -0,0 +1,588 @@
|
||||
# 📝 Consignes de Codage MQTT pour IPWatch
|
||||
|
||||
Ce document définit les standards de développement pour l'écosystème MQTT d'IPWatch.
|
||||
|
||||
## 1. Conventions de Nommage
|
||||
|
||||
### 1.1 Topics MQTT
|
||||
|
||||
**Format** : `ipwatch/device/{IP_ADDRESS}/{category}`
|
||||
|
||||
**Règles** :
|
||||
- ✅ Utiliser des minuscules uniquement
|
||||
- ✅ Utiliser `/` comme séparateur hiérarchique
|
||||
- ✅ Pas d'espaces ni de caractères spéciaux
|
||||
- ✅ Maximum 128 caractères
|
||||
- ❌ Éviter les wildcards (`+`, `#`) dans les noms de topics
|
||||
|
||||
**Exemples** :
|
||||
```
|
||||
✅ ipwatch/device/192.168.1.100/command
|
||||
✅ ipwatch/device/10.0.0.50/status
|
||||
❌ IPWatch/Device/192.168.1.100/Command (majuscules)
|
||||
❌ ipwatch-device-192.168.1.100-command (mauvais séparateur)
|
||||
```
|
||||
|
||||
### 1.2 Noms de Variables (Python)
|
||||
|
||||
```python
|
||||
# ✅ Bon
|
||||
mqtt_broker = "localhost"
|
||||
command_topic = f"ipwatch/device/{ip}/command"
|
||||
message_payload = json.dumps(data)
|
||||
|
||||
# ❌ Mauvais
|
||||
MQTTBroker = "localhost" # Pas de PascalCase pour variables
|
||||
commandTopic = f"ipwatch/device/{ip}/command" # Pas de camelCase
|
||||
msg = json.dumps(data) # Abréviation non claire
|
||||
```
|
||||
|
||||
### 1.3 Constantes
|
||||
|
||||
```python
|
||||
# ✅ Constantes en MAJUSCULES
|
||||
MQTT_QOS_COMMAND = 1
|
||||
MQTT_QOS_STATUS = 1
|
||||
DEFAULT_KEEPALIVE = 60
|
||||
MAX_RETRY_ATTEMPTS = 3
|
||||
|
||||
# Topics en constantes
|
||||
TOPIC_PREFIX = "ipwatch/device"
|
||||
TOPIC_COMMAND = "{}/command"
|
||||
TOPIC_STATUS = "{}/status"
|
||||
```
|
||||
|
||||
## 2. Structure du Code
|
||||
|
||||
### 2.1 Organisation des Fichiers
|
||||
|
||||
```
|
||||
mqtt/
|
||||
├── client/ # Agent MQTT côté client
|
||||
│ ├── ipwatch_mqtt_agent.py # Agent principal
|
||||
│ ├── mqtt-agent.conf.example # Configuration exemple
|
||||
│ └── requirements.txt # Dépendances Python
|
||||
├── docs/ # Documentation
|
||||
│ ├── MQTT_ARCHITECTURE.md
|
||||
│ ├── MQTT_CODING_GUIDELINES.md
|
||||
│ └── HOMEASSISTANT_SPEC.md
|
||||
└── systemd/ # Services systemd
|
||||
└── ipwatch-mqtt-agent.service
|
||||
```
|
||||
|
||||
### 2.2 Structure d'un Module Python
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Titre du module
|
||||
Description détaillée
|
||||
|
||||
Installation:
|
||||
pip install dependencies
|
||||
|
||||
Usage:
|
||||
python module.py --help
|
||||
"""
|
||||
|
||||
# Imports standard library
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# Imports third-party
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
# Imports locaux
|
||||
from backend.app.core.config import config_manager
|
||||
|
||||
# Configuration du logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Constantes globales
|
||||
MQTT_BROKER = "localhost"
|
||||
MQTT_PORT = 1883
|
||||
|
||||
# Classes
|
||||
class MyClass:
|
||||
"""Docstring de la classe"""
|
||||
pass
|
||||
|
||||
# Fonctions
|
||||
def my_function():
|
||||
"""Docstring de la fonction"""
|
||||
pass
|
||||
|
||||
# Point d'entrée
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
## 3. Gestion MQTT
|
||||
|
||||
### 3.1 Connexion au Broker
|
||||
|
||||
```python
|
||||
# ✅ Bon - Gestion robuste des erreurs
|
||||
def connect_mqtt(broker, port, username=None, password=None):
|
||||
"""
|
||||
Connecte au broker MQTT avec gestion d'erreurs
|
||||
|
||||
Args:
|
||||
broker (str): Adresse du broker
|
||||
port (int): Port MQTT
|
||||
username (str, optional): Nom d'utilisateur
|
||||
password (str, optional): Mot de passe
|
||||
|
||||
Returns:
|
||||
mqtt.Client: Client MQTT connecté
|
||||
|
||||
Raises:
|
||||
ConnectionError: Si la connexion échoue
|
||||
"""
|
||||
try:
|
||||
client = mqtt.Client(client_id=f"ipwatch-{os.getpid()}")
|
||||
|
||||
if username and password:
|
||||
client.username_pw_set(username, password)
|
||||
|
||||
client.connect(broker, port, keepalive=60)
|
||||
logger.info(f"✓ Connecté au broker {broker}:{port}")
|
||||
return client
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Erreur connexion MQTT: {e}")
|
||||
raise ConnectionError(f"Impossible de se connecter à {broker}:{port}")
|
||||
|
||||
# ❌ Mauvais - Pas de gestion d'erreur
|
||||
def connect_mqtt(broker, port):
|
||||
client = mqtt.Client()
|
||||
client.connect(broker, port)
|
||||
return client
|
||||
```
|
||||
|
||||
### 3.2 Publication de Messages
|
||||
|
||||
```python
|
||||
# ✅ Bon - QoS et vérification
|
||||
def publish_command(client, ip_address, command):
|
||||
"""
|
||||
Publie une commande MQTT
|
||||
|
||||
Args:
|
||||
client: Client MQTT
|
||||
ip_address (str): IP de destination
|
||||
command (str): Commande à envoyer
|
||||
|
||||
Returns:
|
||||
bool: True si publié avec succès
|
||||
"""
|
||||
topic = f"ipwatch/device/{ip_address}/command"
|
||||
payload = json.dumps({
|
||||
"command": command,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
result = client.publish(topic, payload, qos=1)
|
||||
result.wait_for_publish(timeout=5)
|
||||
|
||||
if result.is_published():
|
||||
logger.info(f"✓ Commande '{command}' envoyée à {ip_address}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"✗ Échec publication commande vers {ip_address}")
|
||||
return False
|
||||
|
||||
# ❌ Mauvais - Pas de QoS, pas de vérification
|
||||
def publish_command(client, ip, cmd):
|
||||
client.publish(f"ipwatch/device/{ip}/command", cmd)
|
||||
```
|
||||
|
||||
### 3.3 Souscription et Callbacks
|
||||
|
||||
```python
|
||||
# ✅ Bon - Callbacks avec gestion d'erreur
|
||||
class MQTTAgent:
|
||||
def __init__(self):
|
||||
self.client = mqtt.Client()
|
||||
self.client.on_connect = self.on_connect
|
||||
self.client.on_message = self.on_message
|
||||
self.client.on_disconnect = self.on_disconnect
|
||||
|
||||
def on_connect(self, client, userdata, flags, rc):
|
||||
"""Callback lors de la connexion"""
|
||||
if rc == 0:
|
||||
logger.info("✓ Connecté au broker")
|
||||
client.subscribe(self.command_topic, qos=1)
|
||||
else:
|
||||
logger.error(f"✗ Échec connexion, code: {rc}")
|
||||
|
||||
def on_message(self, client, userdata, msg):
|
||||
"""Callback réception message"""
|
||||
try:
|
||||
payload = json.loads(msg.payload.decode('utf-8'))
|
||||
command = payload.get('command')
|
||||
self.execute_command(command)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"✗ JSON invalide: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Erreur traitement message: {e}")
|
||||
|
||||
def on_disconnect(self, client, userdata, rc):
|
||||
"""Callback déconnexion"""
|
||||
if rc != 0:
|
||||
logger.warning(f"⚠ Déconnexion inattendue (code {rc})")
|
||||
```
|
||||
|
||||
## 4. Gestion des Erreurs
|
||||
|
||||
### 4.1 Try/Except
|
||||
|
||||
```python
|
||||
# ✅ Bon - Gestion spécifique des erreurs
|
||||
def execute_shutdown():
|
||||
"""Exécute la commande shutdown"""
|
||||
try:
|
||||
subprocess.run(
|
||||
["sudo", "shutdown", "-h", "now"],
|
||||
check=True,
|
||||
timeout=10
|
||||
)
|
||||
logger.info("✓ Shutdown initié")
|
||||
return True
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("✗ Timeout exécution shutdown")
|
||||
return False
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"✗ Erreur shutdown: {e}")
|
||||
return False
|
||||
|
||||
except PermissionError:
|
||||
logger.error("✗ Permissions sudo insuffisantes")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Erreur inattendue: {e}")
|
||||
return False
|
||||
|
||||
# ❌ Mauvais - Catch all sans logging
|
||||
def execute_shutdown():
|
||||
try:
|
||||
subprocess.run(["sudo", "shutdown", "-h", "now"])
|
||||
except:
|
||||
pass
|
||||
```
|
||||
|
||||
### 4.2 Logging
|
||||
|
||||
```python
|
||||
# ✅ Bon - Niveaux de log appropriés
|
||||
logger.debug("Payload: %s", payload) # Détails de debug
|
||||
logger.info("✓ Connexion établie") # Opérations normales
|
||||
logger.warning("⚠ Retry connexion...") # Avertissements
|
||||
logger.error("✗ Échec publication") # Erreurs
|
||||
logger.critical("🔴 Broker inaccessible") # Erreurs critiques
|
||||
|
||||
# ❌ Mauvais - Mauvais niveaux
|
||||
logger.info("Erreur critique!") # Devrait être error/critical
|
||||
logger.error("Opération réussie") # Devrait être info
|
||||
```
|
||||
|
||||
## 5. Format des Payloads JSON
|
||||
|
||||
### 5.1 Commandes
|
||||
|
||||
```python
|
||||
# ✅ Bon - Structure claire
|
||||
{
|
||||
"command": "shutdown", # string, required
|
||||
"timestamp": "2025-12-23T10:30:00Z" # ISO 8601, required
|
||||
}
|
||||
|
||||
# Avec options
|
||||
{
|
||||
"command": "shutdown",
|
||||
"timestamp": "2025-12-23T10:30:00Z",
|
||||
"delay": 60, # secondes avant exécution
|
||||
"force": false # shutdown forcé ou non
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Statut
|
||||
|
||||
```python
|
||||
# ✅ Bon - Métriques utiles
|
||||
{
|
||||
"hostname": "server-01",
|
||||
"ip": "10.0.0.100",
|
||||
"platform": "Linux",
|
||||
"platform_version": "5.15.0-91-generic",
|
||||
"uptime": 86400, # secondes
|
||||
"cpu_percent": 45.2,
|
||||
"memory_percent": 62.5,
|
||||
"disk_percent": 78.3,
|
||||
"timestamp": "2025-12-23T10:30:00Z"
|
||||
}
|
||||
|
||||
# ❌ Mauvais - Données incomplètes
|
||||
{
|
||||
"host": "server-01",
|
||||
"status": "ok"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Réponses
|
||||
|
||||
```python
|
||||
# ✅ Bon - Succès/échec clair
|
||||
{
|
||||
"success": true,
|
||||
"message": "Shutdown en cours...",
|
||||
"timestamp": "2025-12-23T10:30:05Z"
|
||||
}
|
||||
|
||||
# En cas d'erreur
|
||||
{
|
||||
"success": false,
|
||||
"message": "Permissions insuffisantes",
|
||||
"error_code": "PERMISSION_DENIED",
|
||||
"timestamp": "2025-12-23T10:30:05Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Tests
|
||||
|
||||
### 6.1 Tests Unitaires
|
||||
|
||||
```python
|
||||
# tests/test_mqtt_agent.py
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from ipwatch_mqtt_agent import IPWatchMQTTAgent
|
||||
|
||||
def test_get_ip_address():
|
||||
"""Test récupération adresse IP"""
|
||||
agent = IPWatchMQTTAgent()
|
||||
ip = agent.get_ip_address()
|
||||
assert isinstance(ip, str)
|
||||
assert len(ip.split('.')) == 4
|
||||
|
||||
def test_publish_status():
|
||||
"""Test publication du statut"""
|
||||
agent = IPWatchMQTTAgent()
|
||||
agent.client = MagicMock()
|
||||
|
||||
agent.publish_status()
|
||||
|
||||
agent.client.publish.assert_called_once()
|
||||
args = agent.client.publish.call_args
|
||||
assert "ipwatch/device" in args[0][0]
|
||||
|
||||
@patch('subprocess.run')
|
||||
def test_shutdown(mock_subprocess):
|
||||
"""Test commande shutdown"""
|
||||
agent = IPWatchMQTTAgent()
|
||||
agent.shutdown()
|
||||
|
||||
mock_subprocess.assert_called_once()
|
||||
assert "shutdown" in mock_subprocess.call_args[0][0]
|
||||
```
|
||||
|
||||
### 6.2 Tests d'Intégration
|
||||
|
||||
```python
|
||||
# tests/test_mqtt_integration.py
|
||||
import time
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
def test_command_flow():
|
||||
"""Test flux complet command → response"""
|
||||
received_messages = []
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
received_messages.append(msg.payload.decode())
|
||||
|
||||
# Setup client test
|
||||
client = mqtt.Client("test-client")
|
||||
client.on_message = on_message
|
||||
client.connect("localhost", 1883)
|
||||
client.subscribe("ipwatch/device/+/response")
|
||||
client.loop_start()
|
||||
|
||||
# Envoyer commande
|
||||
client.publish(
|
||||
"ipwatch/device/10.0.0.100/command",
|
||||
'{"command": "status"}',
|
||||
qos=1
|
||||
)
|
||||
|
||||
# Attendre réponse
|
||||
time.sleep(2)
|
||||
|
||||
assert len(received_messages) > 0
|
||||
assert "success" in received_messages[0]
|
||||
|
||||
client.loop_stop()
|
||||
client.disconnect()
|
||||
```
|
||||
|
||||
## 7. Sécurité
|
||||
|
||||
### 7.1 Credentials
|
||||
|
||||
```python
|
||||
# ✅ Bon - Variables d'environnement
|
||||
MQTT_USERNAME = os.getenv('MQTT_USERNAME')
|
||||
MQTT_PASSWORD = os.getenv('MQTT_PASSWORD')
|
||||
|
||||
if not MQTT_USERNAME or not MQTT_PASSWORD:
|
||||
logger.warning("⚠ MQTT sans authentification")
|
||||
|
||||
# ❌ Mauvais - Hardcodé
|
||||
MQTT_USERNAME = "ipwatch"
|
||||
MQTT_PASSWORD = "password123"
|
||||
```
|
||||
|
||||
### 7.2 Validation des Entrées
|
||||
|
||||
```python
|
||||
# ✅ Bon - Validation stricte
|
||||
def execute_command(command):
|
||||
"""Exécute une commande MQTT"""
|
||||
ALLOWED_COMMANDS = ['shutdown', 'reboot', 'status']
|
||||
|
||||
if command not in ALLOWED_COMMANDS:
|
||||
logger.error(f"✗ Commande refusée: {command}")
|
||||
return False
|
||||
|
||||
# Exécution sécurisée
|
||||
if command == "shutdown":
|
||||
return execute_shutdown()
|
||||
# ...
|
||||
|
||||
# ❌ Mauvais - Exécution directe
|
||||
def execute_command(command):
|
||||
subprocess.run(command, shell=True) # DANGER: injection!
|
||||
```
|
||||
|
||||
## 8. Performance
|
||||
|
||||
### 8.1 Optimisation des Payloads
|
||||
|
||||
```python
|
||||
# ✅ Bon - Payload compact
|
||||
payload = json.dumps({
|
||||
"cmd": "status", # Abréviation pour réduire taille
|
||||
"ts": int(time.time()) # Timestamp Unix
|
||||
}, separators=(',', ':')) # Pas d'espaces
|
||||
|
||||
# ❌ Mauvais - Payload volumineux
|
||||
payload = json.dumps({
|
||||
"command": "status",
|
||||
"timestamp": "2025-12-23T10:30:00.123456Z",
|
||||
"extra_field_1": None,
|
||||
"extra_field_2": None
|
||||
}, indent=2) # Indentation inutile
|
||||
```
|
||||
|
||||
### 8.2 Gestion de la Boucle
|
||||
|
||||
```python
|
||||
# ✅ Bon - Boucle non-bloquante
|
||||
client.loop_start() # Thread séparé
|
||||
|
||||
while True:
|
||||
time.sleep(30)
|
||||
publish_status()
|
||||
|
||||
# ❌ Mauvais - Boucle bloquante
|
||||
while True:
|
||||
client.loop() # Bloque le thread principal
|
||||
time.sleep(30)
|
||||
```
|
||||
|
||||
## 9. Documentation
|
||||
|
||||
### 9.1 Docstrings
|
||||
|
||||
```python
|
||||
# ✅ Bon - Docstring complet
|
||||
def send_mqtt_command(ip_address, command):
|
||||
"""
|
||||
Envoie une commande MQTT à un équipement
|
||||
|
||||
Cette fonction publie une commande sur le topic MQTT correspondant
|
||||
à l'adresse IP fournie. La commande est envoyée avec QoS 1 pour
|
||||
garantir la livraison.
|
||||
|
||||
Args:
|
||||
ip_address (str): Adresse IP de l'équipement cible
|
||||
command (str): Commande à envoyer (shutdown, reboot, status)
|
||||
|
||||
Returns:
|
||||
bool: True si la commande a été publiée avec succès
|
||||
|
||||
Raises:
|
||||
ValueError: Si la commande n'est pas valide
|
||||
ConnectionError: Si le broker MQTT est inaccessible
|
||||
|
||||
Example:
|
||||
>>> send_mqtt_command("192.168.1.100", "status")
|
||||
True
|
||||
"""
|
||||
pass
|
||||
|
||||
# ❌ Mauvais - Pas de documentation
|
||||
def send_mqtt_command(ip, cmd):
|
||||
pass
|
||||
```
|
||||
|
||||
## 10. Checklist Pré-commit
|
||||
|
||||
Avant de commit du code MQTT, vérifier :
|
||||
|
||||
- [ ] Code formaté avec `black` (PEP 8)
|
||||
- [ ] Pas de secrets/credentials hardcodés
|
||||
- [ ] Logging approprié (info/error/debug)
|
||||
- [ ] Gestion des erreurs avec try/except
|
||||
- [ ] Docstrings sur fonctions/classes publiques
|
||||
- [ ] Tests unitaires passent
|
||||
- [ ] QoS 1 pour messages critiques
|
||||
- [ ] Validation des payloads JSON
|
||||
- [ ] Topics respectent la convention
|
||||
- [ ] Variables d'environnement documentées
|
||||
|
||||
## 11. Outils Recommandés
|
||||
|
||||
```bash
|
||||
# Formatage code
|
||||
pip install black
|
||||
black mqtt/client/
|
||||
|
||||
# Linting
|
||||
pip install pylint
|
||||
pylint mqtt/client/ipwatch_mqtt_agent.py
|
||||
|
||||
# Tests
|
||||
pip install pytest pytest-cov
|
||||
pytest mqtt/tests/ --cov
|
||||
|
||||
# Type checking
|
||||
pip install mypy
|
||||
mypy mqtt/client/
|
||||
```
|
||||
|
||||
## 12. Exemples de Code Complet
|
||||
|
||||
Voir :
|
||||
- `mqtt/client/ipwatch_mqtt_agent.py` - Agent MQTT client
|
||||
- `backend/app/services/mqtt_client.py` - Client MQTT backend
|
||||
- `backend/app/routers/tracking.py` - Endpoints API MQTT
|
||||
Reference in New Issue
Block a user