589 lines
14 KiB
Markdown
589 lines
14 KiB
Markdown
# 📝 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
|