ipwatch
This commit is contained in:
337
mqtt/README.md
Normal file
337
mqtt/README.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# 🔌 IPWatch MQTT - Contrôle à distance des équipements
|
||||
|
||||
Ce module permet de contrôler les équipements du réseau via MQTT (shutdown, reboot) de manière centralisée depuis IPWatch.
|
||||
|
||||
## 📋 Vue d'ensemble
|
||||
|
||||
L'architecture MQTT d'IPWatch permet de :
|
||||
- **Éteindre** des machines à distance via commande MQTT
|
||||
- **Redémarrer** des machines à distance via commande MQTT
|
||||
- **Démarrer** des machines via Wake-on-LAN (WOL)
|
||||
- **Monitorer** l'état des équipements en temps réel
|
||||
- **Intégrer** avec Home Assistant pour la domotique
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ MQTT ┌──────────────────┐
|
||||
│ IPWatch Web UI │ ──────────────────────► │ MQTT Broker │
|
||||
│ (Frontend) │ │ (Mosquitto) │
|
||||
└─────────────────┘ └──────────────────┘
|
||||
│ │
|
||||
│ HTTP API │ MQTT Topics
|
||||
▼ ▼
|
||||
┌─────────────────┐ ┌──────────────────┐
|
||||
│ IPWatch Backend │ │ MQTT Agents │
|
||||
│ (FastAPI) │ │ (sur machines) │
|
||||
└─────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
### Topics MQTT
|
||||
|
||||
- **Commandes** : `ipwatch/device/{IP_ADDRESS}/command`
|
||||
- **Statut** : `ipwatch/device/{IP_ADDRESS}/status`
|
||||
- **Disponibilité** : `ipwatch/device/{IP_ADDRESS}/availability`
|
||||
- **Réponses** : `ipwatch/device/{IP_ADDRESS}/response`
|
||||
|
||||
### Format des messages
|
||||
|
||||
**Commande** :
|
||||
```json
|
||||
{
|
||||
"command": "shutdown", // ou "reboot", "status"
|
||||
"timestamp": "2025-12-23T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Statut** :
|
||||
```json
|
||||
{
|
||||
"hostname": "server-01",
|
||||
"ip": "192.168.1.100",
|
||||
"platform": "Linux",
|
||||
"uptime": 86400,
|
||||
"cpu_percent": 45.2,
|
||||
"memory_percent": 62.5,
|
||||
"timestamp": "2025-12-23T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### 1. Broker MQTT (serveur central)
|
||||
|
||||
```bash
|
||||
# Installer Mosquitto
|
||||
sudo apt update
|
||||
sudo apt install mosquitto mosquitto-clients
|
||||
|
||||
# Démarrer le service
|
||||
sudo systemctl enable mosquitto
|
||||
sudo systemctl start mosquitto
|
||||
|
||||
# Vérifier le statut
|
||||
sudo systemctl status mosquitto
|
||||
```
|
||||
|
||||
### 2. Agent MQTT (sur chaque machine à contrôler)
|
||||
|
||||
```bash
|
||||
# Copier l'agent
|
||||
sudo cp mqtt/client/ipwatch_mqtt_agent.py /usr/local/bin/
|
||||
sudo chmod +x /usr/local/bin/ipwatch_mqtt_agent.py
|
||||
|
||||
# Installer les dépendances Python
|
||||
pip3 install paho-mqtt psutil netifaces
|
||||
|
||||
# Créer le dossier de configuration
|
||||
sudo mkdir -p /etc/ipwatch
|
||||
|
||||
# Copier et éditer la configuration
|
||||
sudo cp mqtt/client/mqtt-agent.conf.example /etc/ipwatch/mqtt-agent.conf
|
||||
sudo nano /etc/ipwatch/mqtt-agent.conf
|
||||
```
|
||||
|
||||
**Configuration** (`/etc/ipwatch/mqtt-agent.conf`) :
|
||||
```ini
|
||||
[mqtt]
|
||||
broker = 192.168.1.10 # IP du serveur IPWatch
|
||||
port = 1883
|
||||
username = # Optionnel
|
||||
password = # Optionnel
|
||||
|
||||
[agent]
|
||||
hostname = auto
|
||||
check_interval = 30
|
||||
```
|
||||
|
||||
### 3. Service systemd
|
||||
|
||||
```bash
|
||||
# Copier le service systemd
|
||||
sudo cp mqtt/systemd/ipwatch-mqtt-agent.service /etc/systemd/system/
|
||||
|
||||
# Recharger systemd
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# Activer et démarrer le service
|
||||
sudo systemctl enable ipwatch-mqtt-agent
|
||||
sudo systemctl start ipwatch-mqtt-agent
|
||||
|
||||
# Vérifier le statut
|
||||
sudo systemctl status ipwatch-mqtt-agent
|
||||
|
||||
# Voir les logs
|
||||
sudo journalctl -u ipwatch-mqtt-agent -f
|
||||
```
|
||||
|
||||
### 4. Configuration IPWatch Backend
|
||||
|
||||
Dans le fichier `config.yaml` ou via variables d'environnement Docker :
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
broker: localhost
|
||||
port: 1883
|
||||
username: ""
|
||||
password: ""
|
||||
```
|
||||
|
||||
Ou dans `docker-compose.yml` :
|
||||
```yaml
|
||||
environment:
|
||||
- MQTT_BROKER=localhost
|
||||
- MQTT_PORT=1883
|
||||
- MQTT_USERNAME=
|
||||
- MQTT_PASSWORD=
|
||||
```
|
||||
|
||||
## 🔧 Configuration sudo (IMPORTANT)
|
||||
|
||||
Pour que l'agent puisse exécuter shutdown/reboot sans mot de passe :
|
||||
|
||||
```bash
|
||||
# Éditer sudoers
|
||||
sudo visudo
|
||||
|
||||
# Ajouter cette ligne (remplacer 'username' par l'utilisateur qui lance l'agent)
|
||||
username ALL=(ALL) NOPASSWD: /sbin/shutdown, /sbin/reboot
|
||||
```
|
||||
|
||||
Ou créer un fichier dédié :
|
||||
```bash
|
||||
echo "username ALL=(ALL) NOPASSWD: /sbin/shutdown, /sbin/reboot" | sudo tee /etc/sudoers.d/ipwatch-agent
|
||||
sudo chmod 440 /etc/sudoers.d/ipwatch-agent
|
||||
```
|
||||
|
||||
## 🧪 Tests
|
||||
|
||||
### Test de l'agent
|
||||
|
||||
```bash
|
||||
# Mode test (affiche la config sans démarrer)
|
||||
python3 /usr/local/bin/ipwatch_mqtt_agent.py --test
|
||||
|
||||
# Démarrage manuel (pour debug)
|
||||
python3 /usr/local/bin/ipwatch_mqtt_agent.py
|
||||
```
|
||||
|
||||
### Test manuel des commandes MQTT
|
||||
|
||||
```bash
|
||||
# Publier une commande shutdown
|
||||
mosquitto_pub -h localhost -t "ipwatch/device/192.168.1.100/command" -m '{"command":"shutdown"}'
|
||||
|
||||
# Publier une commande reboot
|
||||
mosquitto_pub -h localhost -t "ipwatch/device/192.168.1.100/command" -m '{"command":"reboot"}'
|
||||
|
||||
# Demander le statut
|
||||
mosquitto_pub -h localhost -t "ipwatch/device/192.168.1.100/command" -m '{"command":"status"}'
|
||||
|
||||
# Écouter les réponses
|
||||
mosquitto_sub -h localhost -t "ipwatch/device/192.168.1.100/#" -v
|
||||
```
|
||||
|
||||
## 📊 Utilisation depuis IPWatch
|
||||
|
||||
### Via l'interface Web
|
||||
|
||||
1. Accédez à la page **"Suivi"** (`/tracking`)
|
||||
2. Cliquez sur le bouton **"Éteindre"** (rose) pour shutdown
|
||||
3. Cliquez sur le bouton **"WOL"** (vert) pour démarrer
|
||||
|
||||
### Ajouter un bouton Reboot (optionnel)
|
||||
|
||||
Modifier `frontend/src/views/TrackingView.vue` pour ajouter un bouton reboot :
|
||||
|
||||
```vue
|
||||
<button
|
||||
@click="rebootDevice(ip)"
|
||||
:disabled="ip.last_status === 'offline' || actionLoading[ip.ip]"
|
||||
class="flex-1 px-3 py-2 rounded bg-monokai-orange text-monokai-bg font-bold hover:bg-yellow-600 transition-colors disabled:opacity-30"
|
||||
title="Redémarrer l'équipement"
|
||||
>
|
||||
<span class="mdi mdi-restart"></span> Reboot
|
||||
</button>
|
||||
```
|
||||
|
||||
Et la fonction JavaScript :
|
||||
```javascript
|
||||
async function rebootDevice(ip) {
|
||||
if (!confirm(`Voulez-vous vraiment redémarrer ${ip.name || ip.ip} ?`)) return
|
||||
|
||||
actionLoading.value[ip.ip] = 'reboot'
|
||||
try {
|
||||
await axios.post(`/api/tracking/reboot/${ip.ip}`)
|
||||
alert(`✓ Commande reboot envoyée à ${ip.name || ip.ip}`)
|
||||
setTimeout(() => fetchTrackedIPs(), 3000)
|
||||
} catch (error) {
|
||||
alert(`✗ Erreur reboot: ${error.response?.data?.detail || error.message}`)
|
||||
} finally {
|
||||
delete actionLoading.value[ip.ip]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔐 Sécurité
|
||||
|
||||
### Authentification MQTT
|
||||
|
||||
```bash
|
||||
# Créer un utilisateur MQTT
|
||||
sudo mosquitto_passwd -c /etc/mosquitto/passwd ipwatch
|
||||
|
||||
# Configurer Mosquitto pour l'authentification
|
||||
sudo nano /etc/mosquitto/mosquitto.conf
|
||||
```
|
||||
|
||||
Ajouter :
|
||||
```
|
||||
allow_anonymous false
|
||||
password_file /etc/mosquitto/passwd
|
||||
```
|
||||
|
||||
Redémarrer Mosquitto :
|
||||
```bash
|
||||
sudo systemctl restart mosquitto
|
||||
```
|
||||
|
||||
Mettre à jour la configuration des agents :
|
||||
```ini
|
||||
[mqtt]
|
||||
broker = 192.168.1.10
|
||||
port = 1883
|
||||
username = ipwatch
|
||||
password = VotreMotDePasse
|
||||
```
|
||||
|
||||
### SSL/TLS (optionnel)
|
||||
|
||||
Pour sécuriser les communications MQTT :
|
||||
|
||||
```bash
|
||||
# Générer un certificat
|
||||
sudo openssl req -new -x509 -days 365 -extensions v3_ca -keyout /etc/mosquitto/ca.key -out /etc/mosquitto/ca.crt
|
||||
```
|
||||
|
||||
Configuration Mosquitto avec SSL :
|
||||
```
|
||||
listener 8883
|
||||
cafile /etc/mosquitto/ca.crt
|
||||
certfile /etc/mosquitto/server.crt
|
||||
keyfile /etc/mosquitto/server.key
|
||||
```
|
||||
|
||||
## 📖 Documentation complémentaire
|
||||
|
||||
- [MQTT_ARCHITECTURE.md](docs/MQTT_ARCHITECTURE.md) - Architecture détaillée
|
||||
- [MQTT_CODING_GUIDELINES.md](docs/MQTT_CODING_GUIDELINES.md) - Consignes de développement
|
||||
- [HOMEASSISTANT_SPEC.md](docs/HOMEASSISTANT_SPEC.md) - Intégration Home Assistant
|
||||
|
||||
## 🐛 Dépannage
|
||||
|
||||
### L'agent ne se connecte pas
|
||||
|
||||
```bash
|
||||
# Vérifier que Mosquitto écoute
|
||||
sudo netstat -tlnp | grep 1883
|
||||
|
||||
# Vérifier les logs
|
||||
sudo journalctl -u mosquitto -f
|
||||
|
||||
# Tester la connexion
|
||||
mosquitto_sub -h localhost -t "test" -v
|
||||
```
|
||||
|
||||
### Les commandes ne sont pas exécutées
|
||||
|
||||
```bash
|
||||
# Vérifier les permissions sudo
|
||||
sudo -l
|
||||
|
||||
# Vérifier les logs de l'agent
|
||||
sudo journalctl -u ipwatch-mqtt-agent -f
|
||||
|
||||
# Tester manuellement
|
||||
python3 /usr/local/bin/ipwatch_mqtt_agent.py
|
||||
```
|
||||
|
||||
### Erreur "ModuleNotFoundError: No module named 'paho'"
|
||||
|
||||
```bash
|
||||
# Réinstaller paho-mqtt
|
||||
pip3 install --upgrade paho-mqtt
|
||||
```
|
||||
|
||||
## 📞 Support
|
||||
|
||||
Pour toute question ou problème :
|
||||
1. Consultez les logs : `sudo journalctl -u ipwatch-mqtt-agent -f`
|
||||
2. Vérifiez la configuration : `python3 /usr/local/bin/ipwatch_mqtt_agent.py --test`
|
||||
3. Testez manuellement avec `mosquitto_pub/sub`
|
||||
|
||||
## 🔄 Compatibilité Home Assistant
|
||||
|
||||
Ce système MQTT est conçu pour être compatible avec Home Assistant. Consultez [HOMEASSISTANT_SPEC.md](docs/HOMEASSISTANT_SPEC.md) pour l'intégration.
|
||||
|
||||
Les topics MQTT suivent la convention MQTT Discovery de Home Assistant pour une auto-découverte automatique.
|
||||
337
mqtt/client/ipwatch_mqtt_agent.py
Normal file
337
mqtt/client/ipwatch_mqtt_agent.py
Normal file
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
IPWatch MQTT Agent
|
||||
Agent à installer sur chaque machine pour recevoir les commandes shutdown/reboot via MQTT
|
||||
|
||||
Installation:
|
||||
pip install paho-mqtt psutil netifaces
|
||||
|
||||
Configuration:
|
||||
Créer /etc/ipwatch/mqtt-agent.conf avec:
|
||||
[mqtt]
|
||||
broker = localhost
|
||||
port = 1883
|
||||
username =
|
||||
password =
|
||||
|
||||
[agent]
|
||||
hostname = auto
|
||||
check_interval = 30
|
||||
"""
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import platform
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
import time
|
||||
import socket
|
||||
import configparser
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Configuration du logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('/var/log/ipwatch-mqtt-agent.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
logger = logging.getLogger('ipwatch-mqtt-agent')
|
||||
|
||||
|
||||
class IPWatchMQTTAgent:
|
||||
"""Agent MQTT pour recevoir les commandes de contrôle système"""
|
||||
|
||||
def __init__(self, config_file='/etc/ipwatch/mqtt-agent.conf'):
|
||||
self.config = self.load_config(config_file)
|
||||
self.hostname = self.get_hostname()
|
||||
self.ip_address = self.get_ip_address()
|
||||
|
||||
# Topics MQTT
|
||||
self.base_topic = f"ipwatch/device/{self.ip_address}"
|
||||
self.command_topic = f"{self.base_topic}/command"
|
||||
self.status_topic = f"{self.base_topic}/status"
|
||||
self.availability_topic = f"{self.base_topic}/availability"
|
||||
|
||||
# Client MQTT
|
||||
self.client = mqtt.Client(client_id=f"ipwatch-agent-{self.hostname}")
|
||||
self.client.on_connect = self.on_connect
|
||||
self.client.on_message = self.on_message
|
||||
self.client.on_disconnect = self.on_disconnect
|
||||
|
||||
# Will message (si l'agent se déconnecte brutalement)
|
||||
self.client.will_set(
|
||||
self.availability_topic,
|
||||
payload="offline",
|
||||
qos=1,
|
||||
retain=True
|
||||
)
|
||||
|
||||
def load_config(self, config_file):
|
||||
"""Charge la configuration depuis le fichier"""
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
if not Path(config_file).exists():
|
||||
logger.warning(f"Fichier de configuration {config_file} introuvable, utilisation des valeurs par défaut")
|
||||
return {
|
||||
'broker': 'localhost',
|
||||
'port': 1883,
|
||||
'username': None,
|
||||
'password': None,
|
||||
'check_interval': 30
|
||||
}
|
||||
|
||||
config.read(config_file)
|
||||
|
||||
return {
|
||||
'broker': config.get('mqtt', 'broker', fallback='localhost'),
|
||||
'port': config.getint('mqtt', 'port', fallback=1883),
|
||||
'username': config.get('mqtt', 'username', fallback=None),
|
||||
'password': config.get('mqtt', 'password', fallback=None),
|
||||
'check_interval': config.getint('agent', 'check_interval', fallback=30)
|
||||
}
|
||||
|
||||
def get_hostname(self):
|
||||
"""Récupère le hostname de la machine"""
|
||||
return platform.node()
|
||||
|
||||
def get_ip_address(self):
|
||||
"""Récupère l'adresse IP principale de la machine"""
|
||||
try:
|
||||
# Créer une socket pour déterminer l'IP locale
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.connect(("8.8.8.8", 80))
|
||||
ip = s.getsockname()[0]
|
||||
s.close()
|
||||
return ip
|
||||
except Exception as e:
|
||||
logger.error(f"Erreur récupération IP: {e}")
|
||||
return "127.0.0.1"
|
||||
|
||||
def on_connect(self, client, userdata, flags, rc):
|
||||
"""Callback lors de la connexion au broker MQTT"""
|
||||
if rc == 0:
|
||||
logger.info(f"✓ Connecté au broker MQTT {self.config['broker']}:{self.config['port']}")
|
||||
|
||||
# S'abonner au topic des commandes
|
||||
client.subscribe(self.command_topic)
|
||||
client.subscribe(f"ipwatch/device/+/command") # Pour broadcast
|
||||
logger.info(f"✓ Abonné à {self.command_topic}")
|
||||
|
||||
# Publier la disponibilité
|
||||
client.publish(self.availability_topic, "online", qos=1, retain=True)
|
||||
|
||||
# Publier le statut initial
|
||||
self.publish_status()
|
||||
else:
|
||||
logger.error(f"✗ Échec connexion MQTT, code: {rc}")
|
||||
|
||||
def on_disconnect(self, client, userdata, rc):
|
||||
"""Callback lors de la déconnexion"""
|
||||
if rc != 0:
|
||||
logger.warning(f"⚠ Déconnexion inattendue du broker MQTT (code {rc})")
|
||||
|
||||
def on_message(self, client, userdata, msg):
|
||||
"""Callback lors de la réception d'un message"""
|
||||
try:
|
||||
payload = msg.payload.decode('utf-8')
|
||||
logger.info(f"→ Message reçu sur {msg.topic}: {payload}")
|
||||
|
||||
# Parser le message JSON
|
||||
try:
|
||||
command_data = json.loads(payload)
|
||||
command = command_data.get('command', payload) # Support format simple ou JSON
|
||||
except json.JSONDecodeError:
|
||||
command = payload # Format texte simple
|
||||
|
||||
# Exécuter la commande
|
||||
self.execute_command(command)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Erreur traitement message: {e}")
|
||||
|
||||
def execute_command(self, command):
|
||||
"""Exécute une commande système"""
|
||||
logger.info(f"⚙ Exécution commande: {command}")
|
||||
|
||||
try:
|
||||
if command == "shutdown":
|
||||
self.shutdown()
|
||||
elif command == "reboot":
|
||||
self.reboot()
|
||||
elif command == "status":
|
||||
self.publish_status()
|
||||
else:
|
||||
logger.warning(f"⚠ Commande inconnue: {command}")
|
||||
self.publish_response(f"Commande inconnue: {command}", success=False)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Erreur exécution commande: {e}")
|
||||
self.publish_response(str(e), success=False)
|
||||
|
||||
def shutdown(self):
|
||||
"""Éteint la machine"""
|
||||
logger.warning("🔴 Shutdown demandé, arrêt dans 5 secondes...")
|
||||
|
||||
self.publish_response("Shutdown en cours...", success=True)
|
||||
time.sleep(1)
|
||||
|
||||
# Publier offline avant l'arrêt
|
||||
self.client.publish(self.availability_topic, "offline", qos=1, retain=True)
|
||||
time.sleep(1)
|
||||
|
||||
# Commande d'arrêt selon l'OS
|
||||
if platform.system() == "Windows":
|
||||
subprocess.run(["shutdown", "/s", "/t", "5"])
|
||||
else:
|
||||
subprocess.run(["sudo", "shutdown", "-h", "+0"])
|
||||
|
||||
def reboot(self):
|
||||
"""Redémarre la machine"""
|
||||
logger.warning("🔄 Reboot demandé, redémarrage dans 5 secondes...")
|
||||
|
||||
self.publish_response("Reboot en cours...", success=True)
|
||||
time.sleep(1)
|
||||
|
||||
# Publier offline avant le redémarrage
|
||||
self.client.publish(self.availability_topic, "offline", qos=1, retain=True)
|
||||
time.sleep(1)
|
||||
|
||||
# Commande de redémarrage selon l'OS
|
||||
if platform.system() == "Windows":
|
||||
subprocess.run(["shutdown", "/r", "/t", "5"])
|
||||
else:
|
||||
subprocess.run(["sudo", "reboot"])
|
||||
|
||||
def publish_status(self):
|
||||
"""Publie le statut de la machine"""
|
||||
try:
|
||||
import psutil
|
||||
|
||||
status = {
|
||||
"hostname": self.hostname,
|
||||
"ip": self.ip_address,
|
||||
"platform": platform.system(),
|
||||
"platform_version": platform.version(),
|
||||
"uptime": time.time() - psutil.boot_time(),
|
||||
"cpu_percent": psutil.cpu_percent(interval=1),
|
||||
"memory_percent": psutil.virtual_memory().percent,
|
||||
"disk_percent": psutil.disk_usage('/').percent,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
self.client.publish(
|
||||
self.status_topic,
|
||||
json.dumps(status),
|
||||
qos=1,
|
||||
retain=False
|
||||
)
|
||||
logger.info(f"✓ Statut publié")
|
||||
|
||||
except ImportError:
|
||||
logger.warning("psutil non installé, statut limité")
|
||||
status = {
|
||||
"hostname": self.hostname,
|
||||
"ip": self.ip_address,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
self.client.publish(self.status_topic, json.dumps(status), qos=1)
|
||||
|
||||
def publish_response(self, message, success=True):
|
||||
"""Publie une réponse sur le topic de statut"""
|
||||
response = {
|
||||
"success": success,
|
||||
"message": message,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
self.client.publish(
|
||||
f"{self.base_topic}/response",
|
||||
json.dumps(response),
|
||||
qos=1
|
||||
)
|
||||
|
||||
def run(self):
|
||||
"""Démarre l'agent"""
|
||||
try:
|
||||
# Authentification si configurée
|
||||
if self.config['username'] and self.config['password']:
|
||||
self.client.username_pw_set(
|
||||
self.config['username'],
|
||||
self.config['password']
|
||||
)
|
||||
|
||||
# Connexion au broker
|
||||
logger.info(f"→ Connexion au broker MQTT {self.config['broker']}:{self.config['port']}...")
|
||||
self.client.connect(
|
||||
self.config['broker'],
|
||||
self.config['port'],
|
||||
keepalive=60
|
||||
)
|
||||
|
||||
# Démarrer la boucle MQTT
|
||||
self.client.loop_start()
|
||||
|
||||
# Publier le statut périodiquement
|
||||
logger.info(f"✓ Agent IPWatch MQTT démarré sur {self.ip_address}")
|
||||
logger.info(f" Topics: {self.command_topic} | {self.status_topic}")
|
||||
|
||||
while True:
|
||||
time.sleep(self.config['check_interval'])
|
||||
self.publish_status()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("\n→ Arrêt demandé par l'utilisateur")
|
||||
self.stop()
|
||||
except Exception as e:
|
||||
logger.error(f"✗ Erreur fatale: {e}")
|
||||
self.stop()
|
||||
sys.exit(1)
|
||||
|
||||
def stop(self):
|
||||
"""Arrête l'agent proprement"""
|
||||
logger.info("→ Arrêt de l'agent...")
|
||||
self.client.publish(self.availability_topic, "offline", qos=1, retain=True)
|
||||
self.client.loop_stop()
|
||||
self.client.disconnect()
|
||||
logger.info("✓ Agent arrêté")
|
||||
|
||||
|
||||
def main():
|
||||
"""Point d'entrée principal"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='IPWatch MQTT Agent')
|
||||
parser.add_argument(
|
||||
'-c', '--config',
|
||||
default='/etc/ipwatch/mqtt-agent.conf',
|
||||
help='Fichier de configuration (défaut: /etc/ipwatch/mqtt-agent.conf)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--test',
|
||||
action='store_true',
|
||||
help='Mode test (affiche la config et quitte)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
agent = IPWatchMQTTAgent(config_file=args.config)
|
||||
|
||||
if args.test:
|
||||
print(f"Configuration:")
|
||||
print(f" Broker: {agent.config['broker']}:{agent.config['port']}")
|
||||
print(f" Hostname: {agent.hostname}")
|
||||
print(f" IP: {agent.ip_address}")
|
||||
print(f" Command topic: {agent.command_topic}")
|
||||
print(f" Status topic: {agent.status_topic}")
|
||||
return
|
||||
|
||||
agent.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
17
mqtt/client/mqtt-agent.conf.example
Normal file
17
mqtt/client/mqtt-agent.conf.example
Normal file
@@ -0,0 +1,17 @@
|
||||
[mqtt]
|
||||
# Adresse du broker MQTT
|
||||
broker = localhost
|
||||
|
||||
# Port MQTT (défaut: 1883, SSL: 8883)
|
||||
port = 1883
|
||||
|
||||
# Authentification (laisser vide si non utilisée)
|
||||
username =
|
||||
password =
|
||||
|
||||
[agent]
|
||||
# Hostname (auto = détection automatique)
|
||||
hostname = auto
|
||||
|
||||
# Intervalle de publication du statut (en secondes)
|
||||
check_interval = 30
|
||||
935
mqtt/docs/HOMEASSISTANT_SPEC.md
Normal file
935
mqtt/docs/HOMEASSISTANT_SPEC.md
Normal file
@@ -0,0 +1,935 @@
|
||||
# 🏠 Spécifications Home Assistant pour IPWatch MQTT
|
||||
|
||||
Ce document définit les spécifications pour intégrer les équipements IPWatch dans Home Assistant via MQTT Discovery.
|
||||
|
||||
## 1. Vue d'ensemble
|
||||
|
||||
L'intégration Home Assistant permet de :
|
||||
- **Détecter automatiquement** les équipements IPWatch via MQTT Discovery
|
||||
- **Visualiser** l'état des équipements (online/offline)
|
||||
- **Contrôler** les équipements (shutdown, reboot)
|
||||
- **Monitorer** les métriques système (CPU, RAM, Disk, Uptime)
|
||||
- **Créer des automatisations** basées sur l'état des équipements
|
||||
|
||||
## 2. MQTT Discovery
|
||||
|
||||
### 2.1 Principe
|
||||
|
||||
MQTT Discovery permet à Home Assistant de détecter automatiquement les entités sans configuration manuelle.
|
||||
|
||||
**Topic de découverte** : `homeassistant/{component}/ipwatch_{unique_id}/{object_id}/config`
|
||||
|
||||
**Composants supportés** :
|
||||
- `sensor` : Métriques système
|
||||
- `binary_sensor` : État de disponibilité
|
||||
- `button` : Actions (shutdown, reboot)
|
||||
|
||||
### 2.2 Configuration Discovery pour Sensor
|
||||
|
||||
L'agent MQTT publie automatiquement sa configuration au démarrage :
|
||||
|
||||
**Topic** : `homeassistant/sensor/ipwatch_10_0_0_100/system/config`
|
||||
|
||||
**Payload** :
|
||||
```json
|
||||
{
|
||||
"name": "Server 01 - Système",
|
||||
"unique_id": "ipwatch_10_0_0_100_system",
|
||||
"state_topic": "ipwatch/device/10.0.0.100/status",
|
||||
"value_template": "{{ value_json.hostname }}",
|
||||
"json_attributes_topic": "ipwatch/device/10.0.0.100/status",
|
||||
"availability": {
|
||||
"topic": "ipwatch/device/10.0.0.100/availability",
|
||||
"payload_available": "online",
|
||||
"payload_not_available": "offline"
|
||||
},
|
||||
"device": {
|
||||
"identifiers": ["ipwatch_10_0_0_100"],
|
||||
"name": "Server 01",
|
||||
"model": "IPWatch MQTT Agent v1.0",
|
||||
"manufacturer": "IPWatch",
|
||||
"sw_version": "1.0.0"
|
||||
},
|
||||
"icon": "mdi:desktop-tower",
|
||||
"qos": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Configuration Discovery pour Binary Sensor (Availability)
|
||||
|
||||
**Topic** : `homeassistant/binary_sensor/ipwatch_10_0_0_100/availability/config`
|
||||
|
||||
**Payload** :
|
||||
```json
|
||||
{
|
||||
"name": "Server 01 - Disponibilité",
|
||||
"unique_id": "ipwatch_10_0_0_100_availability",
|
||||
"state_topic": "ipwatch/device/10.0.0.100/availability",
|
||||
"payload_on": "online",
|
||||
"payload_off": "offline",
|
||||
"device_class": "connectivity",
|
||||
"device": {
|
||||
"identifiers": ["ipwatch_10_0_0_100"],
|
||||
"name": "Server 01"
|
||||
},
|
||||
"icon": "mdi:lan-connect",
|
||||
"qos": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Configuration Discovery pour Buttons
|
||||
|
||||
**Shutdown Button** :
|
||||
|
||||
**Topic** : `homeassistant/button/ipwatch_10_0_0_100/shutdown/config`
|
||||
|
||||
**Payload** :
|
||||
```json
|
||||
{
|
||||
"name": "Server 01 - Shutdown",
|
||||
"unique_id": "ipwatch_10_0_0_100_shutdown",
|
||||
"command_topic": "ipwatch/device/10.0.0.100/command",
|
||||
"payload_press": "{\"command\":\"shutdown\",\"timestamp\":\"{{ now().isoformat() }}\"}",
|
||||
"availability": {
|
||||
"topic": "ipwatch/device/10.0.0.100/availability",
|
||||
"payload_available": "online",
|
||||
"payload_not_available": "offline"
|
||||
},
|
||||
"device": {
|
||||
"identifiers": ["ipwatch_10_0_0_100"],
|
||||
"name": "Server 01"
|
||||
},
|
||||
"icon": "mdi:power-off",
|
||||
"qos": 1
|
||||
}
|
||||
```
|
||||
|
||||
**Reboot Button** :
|
||||
|
||||
**Topic** : `homeassistant/button/ipwatch_10_0_0_100/reboot/config`
|
||||
|
||||
**Payload** :
|
||||
```json
|
||||
{
|
||||
"name": "Server 01 - Reboot",
|
||||
"unique_id": "ipwatch_10_0_0_100_reboot",
|
||||
"command_topic": "ipwatch/device/10.0.0.100/command",
|
||||
"payload_press": "{\"command\":\"reboot\",\"timestamp\":\"{{ now().isoformat() }}\"}",
|
||||
"availability": {
|
||||
"topic": "ipwatch/device/10.0.0.100/availability",
|
||||
"payload_available": "online",
|
||||
"payload_not_available": "offline"
|
||||
},
|
||||
"device": {
|
||||
"identifiers": ["ipwatch_10_0_0_100"],
|
||||
"name": "Server 01"
|
||||
},
|
||||
"icon": "mdi:restart",
|
||||
"qos": 1
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Entités Créées dans Home Assistant
|
||||
|
||||
### 3.1 Sensors (Métriques Système)
|
||||
|
||||
Chaque équipement IPWatch crée automatiquement ces sensors :
|
||||
|
||||
| Entity ID | Nom | Description | Attribut JSON |
|
||||
|-----------|-----|-------------|---------------|
|
||||
| `sensor.server_01_systeme` | Server 01 - Système | État général | Tous les attributs |
|
||||
| `sensor.server_01_cpu` | Server 01 - CPU | Usage CPU | `cpu_percent` |
|
||||
| `sensor.server_01_memory` | Server 01 - RAM | Usage mémoire | `memory_percent` |
|
||||
| `sensor.server_01_disk` | Server 01 - Disk | Usage disque | `disk_percent` |
|
||||
| `sensor.server_01_uptime` | Server 01 - Uptime | Temps de fonctionnement | `uptime` |
|
||||
|
||||
**Exemple de valeur du sensor système** :
|
||||
```json
|
||||
{
|
||||
"hostname": "server-01",
|
||||
"ip": "10.0.0.100",
|
||||
"platform": "Linux",
|
||||
"platform_version": "5.15.0-91-generic",
|
||||
"uptime": 86400,
|
||||
"cpu_percent": 45.2,
|
||||
"memory_percent": 62.5,
|
||||
"disk_percent": 78.3,
|
||||
"timestamp": "2025-12-23T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Binary Sensors
|
||||
|
||||
| Entity ID | Nom | État | Icon |
|
||||
|-----------|-----|------|------|
|
||||
| `binary_sensor.server_01_disponibilite` | Server 01 - Disponibilité | on/off | `mdi:lan-connect` |
|
||||
|
||||
### 3.3 Buttons
|
||||
|
||||
| Entity ID | Nom | Action | Icon |
|
||||
|-----------|-----|--------|------|
|
||||
| `button.server_01_shutdown` | Server 01 - Shutdown | Éteindre | `mdi:power-off` |
|
||||
| `button.server_01_reboot` | Server 01 - Reboot | Redémarrer | `mdi:restart` |
|
||||
|
||||
## 4. Configuration Home Assistant
|
||||
|
||||
### 4.1 Configuration MQTT
|
||||
|
||||
Dans `configuration.yaml` :
|
||||
|
||||
```yaml
|
||||
# Configuration MQTT
|
||||
mqtt:
|
||||
broker: localhost
|
||||
port: 1883
|
||||
username: !secret mqtt_username
|
||||
password: !secret mqtt_password
|
||||
discovery: true
|
||||
discovery_prefix: homeassistant
|
||||
birth_message:
|
||||
topic: 'homeassistant/status'
|
||||
payload: 'online'
|
||||
will_message:
|
||||
topic: 'homeassistant/status'
|
||||
payload: 'offline'
|
||||
```
|
||||
|
||||
### 4.2 Secrets
|
||||
|
||||
Dans `secrets.yaml` :
|
||||
|
||||
```yaml
|
||||
mqtt_username: ipwatch
|
||||
mqtt_password: VotreMotDePasse
|
||||
```
|
||||
|
||||
### 4.3 Templates pour Sensors Individuels (Optionnel)
|
||||
|
||||
Si vous souhaitez créer des sensors séparés pour chaque métrique :
|
||||
|
||||
```yaml
|
||||
# Sensor CPU
|
||||
mqtt:
|
||||
- sensor:
|
||||
name: "Server 01 CPU"
|
||||
unique_id: ipwatch_10_0_0_100_cpu
|
||||
state_topic: "ipwatch/device/10.0.0.100/status"
|
||||
value_template: "{{ value_json.cpu_percent }}"
|
||||
unit_of_measurement: "%"
|
||||
icon: mdi:cpu-64-bit
|
||||
availability:
|
||||
topic: "ipwatch/device/10.0.0.100/availability"
|
||||
payload_available: "online"
|
||||
payload_not_available: "offline"
|
||||
device:
|
||||
identifiers: ["ipwatch_10_0_0_100"]
|
||||
name: "Server 01"
|
||||
|
||||
# Sensor RAM
|
||||
- sensor:
|
||||
name: "Server 01 Memory"
|
||||
unique_id: ipwatch_10_0_0_100_memory
|
||||
state_topic: "ipwatch/device/10.0.0.100/status"
|
||||
value_template: "{{ value_json.memory_percent }}"
|
||||
unit_of_measurement: "%"
|
||||
icon: mdi:memory
|
||||
availability:
|
||||
topic: "ipwatch/device/10.0.0.100/availability"
|
||||
device:
|
||||
identifiers: ["ipwatch_10_0_0_100"]
|
||||
|
||||
# Sensor Disk
|
||||
- sensor:
|
||||
name: "Server 01 Disk"
|
||||
unique_id: ipwatch_10_0_0_100_disk
|
||||
state_topic: "ipwatch/device/10.0.0.100/status"
|
||||
value_template: "{{ value_json.disk_percent }}"
|
||||
unit_of_measurement: "%"
|
||||
icon: mdi:harddisk
|
||||
availability:
|
||||
topic: "ipwatch/device/10.0.0.100/availability"
|
||||
device:
|
||||
identifiers: ["ipwatch_10_0_0_100"]
|
||||
|
||||
# Sensor Uptime
|
||||
- sensor:
|
||||
name: "Server 01 Uptime"
|
||||
unique_id: ipwatch_10_0_0_100_uptime
|
||||
state_topic: "ipwatch/device/10.0.0.100/status"
|
||||
value_template: "{{ (value_json.uptime / 3600) | round(1) }}"
|
||||
unit_of_measurement: "h"
|
||||
icon: mdi:clock-outline
|
||||
availability:
|
||||
topic: "ipwatch/device/10.0.0.100/availability"
|
||||
device:
|
||||
identifiers: ["ipwatch_10_0_0_100"]
|
||||
```
|
||||
|
||||
## 5. Lovelace UI Cards
|
||||
|
||||
### 5.1 Entity Card (Simple)
|
||||
|
||||
```yaml
|
||||
type: entities
|
||||
title: Server 01
|
||||
entities:
|
||||
- entity: binary_sensor.server_01_disponibilite
|
||||
name: Disponibilité
|
||||
- entity: sensor.server_01_cpu
|
||||
name: CPU
|
||||
- entity: sensor.server_01_memory
|
||||
name: RAM
|
||||
- entity: sensor.server_01_disk
|
||||
name: Disque
|
||||
- entity: sensor.server_01_uptime
|
||||
name: Uptime
|
||||
- entity: button.server_01_shutdown
|
||||
name: Éteindre
|
||||
- entity: button.server_01_reboot
|
||||
name: Redémarrer
|
||||
```
|
||||
|
||||
### 5.2 Glance Card (Compact)
|
||||
|
||||
```yaml
|
||||
type: glance
|
||||
title: Server 01
|
||||
entities:
|
||||
- entity: binary_sensor.server_01_disponibilite
|
||||
name: État
|
||||
- entity: sensor.server_01_cpu
|
||||
name: CPU
|
||||
- entity: sensor.server_01_memory
|
||||
name: RAM
|
||||
- entity: sensor.server_01_disk
|
||||
name: Disque
|
||||
```
|
||||
|
||||
### 5.3 Gauge Card (Métriques visuelles)
|
||||
|
||||
```yaml
|
||||
type: vertical-stack
|
||||
cards:
|
||||
- type: gauge
|
||||
entity: sensor.server_01_cpu
|
||||
name: CPU
|
||||
min: 0
|
||||
max: 100
|
||||
severity:
|
||||
green: 0
|
||||
yellow: 60
|
||||
red: 80
|
||||
|
||||
- type: gauge
|
||||
entity: sensor.server_01_memory
|
||||
name: RAM
|
||||
min: 0
|
||||
max: 100
|
||||
severity:
|
||||
green: 0
|
||||
yellow: 70
|
||||
red: 90
|
||||
|
||||
- type: gauge
|
||||
entity: sensor.server_01_disk
|
||||
name: Disque
|
||||
min: 0
|
||||
max: 100
|
||||
severity:
|
||||
green: 0
|
||||
yellow: 80
|
||||
red: 95
|
||||
```
|
||||
|
||||
### 5.4 Custom Card (Markdown)
|
||||
|
||||
```yaml
|
||||
type: markdown
|
||||
content: |
|
||||
## 🖥️ Server 01
|
||||
|
||||
**État** : {{ states('binary_sensor.server_01_disponibilite') }}
|
||||
**CPU** : {{ states('sensor.server_01_cpu') }}%
|
||||
**RAM** : {{ states('sensor.server_01_memory') }}%
|
||||
**Disque** : {{ states('sensor.server_01_disk') }}%
|
||||
**Uptime** : {{ states('sensor.server_01_uptime') }}h
|
||||
|
||||
{% if is_state('binary_sensor.server_01_disponibilite', 'on') %}
|
||||
✅ Serveur en ligne
|
||||
{% else %}
|
||||
❌ Serveur hors ligne
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### 5.5 Button Card (Actions rapides)
|
||||
|
||||
```yaml
|
||||
type: horizontal-stack
|
||||
cards:
|
||||
- type: button
|
||||
entity: button.server_01_shutdown
|
||||
name: Éteindre
|
||||
icon: mdi:power-off
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: button.press
|
||||
service_data:
|
||||
entity_id: button.server_01_shutdown
|
||||
hold_action:
|
||||
action: none
|
||||
|
||||
- type: button
|
||||
entity: button.server_01_reboot
|
||||
name: Redémarrer
|
||||
icon: mdi:restart
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: button.press
|
||||
service_data:
|
||||
entity_id: button.server_01_reboot
|
||||
```
|
||||
|
||||
## 6. Automatisations
|
||||
|
||||
### 6.1 Alerte si serveur offline
|
||||
|
||||
```yaml
|
||||
alias: "Alert - Server 01 Offline"
|
||||
description: Notification si serveur hors ligne
|
||||
trigger:
|
||||
- platform: state
|
||||
entity_id: binary_sensor.server_01_disponibilite
|
||||
to: "off"
|
||||
for:
|
||||
minutes: 5
|
||||
action:
|
||||
- service: notify.mobile_app
|
||||
data:
|
||||
title: "⚠️ Serveur Offline"
|
||||
message: "Server 01 (10.0.0.100) est hors ligne depuis 5 minutes"
|
||||
data:
|
||||
priority: high
|
||||
mode: single
|
||||
```
|
||||
|
||||
### 6.2 Alerte CPU élevé
|
||||
|
||||
```yaml
|
||||
alias: "Alert - Server 01 High CPU"
|
||||
description: Notification si CPU > 90%
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.server_01_cpu
|
||||
above: 90
|
||||
for:
|
||||
minutes: 10
|
||||
condition:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.server_01_disponibilite
|
||||
state: "on"
|
||||
action:
|
||||
- service: notify.mobile_app
|
||||
data:
|
||||
title: "🔥 CPU élevé"
|
||||
message: "Server 01 - CPU à {{ states('sensor.server_01_cpu') }}%"
|
||||
mode: single
|
||||
```
|
||||
|
||||
### 6.3 Shutdown programmé (extinction nocturne)
|
||||
|
||||
```yaml
|
||||
alias: "Scheduled Shutdown - Server 01"
|
||||
description: Éteindre le serveur à 23h chaque soir
|
||||
trigger:
|
||||
- platform: time
|
||||
at: "23:00:00"
|
||||
condition:
|
||||
- condition: state
|
||||
entity_id: binary_sensor.server_01_disponibilite
|
||||
state: "on"
|
||||
action:
|
||||
- service: button.press
|
||||
target:
|
||||
entity_id: button.server_01_shutdown
|
||||
mode: single
|
||||
```
|
||||
|
||||
### 6.4 Wake-on-LAN au démarrage de HA
|
||||
|
||||
```yaml
|
||||
alias: "WOL - Server 01 on HA Start"
|
||||
description: Démarrer le serveur quand Home Assistant démarre
|
||||
trigger:
|
||||
- platform: homeassistant
|
||||
event: start
|
||||
action:
|
||||
- service: wake_on_lan.send_magic_packet
|
||||
data:
|
||||
mac: "AA:BB:CC:DD:EE:FF"
|
||||
broadcast_address: "192.168.1.255"
|
||||
mode: single
|
||||
```
|
||||
|
||||
## 7. Intégration Personnalisée (Custom Integration)
|
||||
|
||||
### 7.1 Structure du Custom Component
|
||||
|
||||
Pour créer une intégration personnalisée plus avancée :
|
||||
|
||||
```
|
||||
custom_components/
|
||||
└── ipwatch/
|
||||
├── __init__.py
|
||||
├── manifest.json
|
||||
├── config_flow.py
|
||||
├── const.py
|
||||
├── sensor.py
|
||||
├── binary_sensor.py
|
||||
├── button.py
|
||||
└── strings.json
|
||||
```
|
||||
|
||||
### 7.2 Manifest
|
||||
|
||||
**`manifest.json`** :
|
||||
```json
|
||||
{
|
||||
"domain": "ipwatch",
|
||||
"name": "IPWatch Network Monitor",
|
||||
"version": "1.0.0",
|
||||
"documentation": "https://github.com/your-repo/ipwatch-ha",
|
||||
"requirements": ["paho-mqtt==1.6.1"],
|
||||
"dependencies": ["mqtt"],
|
||||
"codeowners": ["@yourusername"],
|
||||
"iot_class": "local_push",
|
||||
"config_flow": true
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 Constants
|
||||
|
||||
**`const.py`** :
|
||||
```python
|
||||
"""Constants for IPWatch integration."""
|
||||
DOMAIN = "ipwatch"
|
||||
CONF_MQTT_BROKER = "mqtt_broker"
|
||||
CONF_MQTT_PORT = "mqtt_port"
|
||||
CONF_DEVICE_IP = "device_ip"
|
||||
CONF_DEVICE_NAME = "device_name"
|
||||
|
||||
# MQTT Topics
|
||||
TOPIC_PREFIX = "ipwatch/device"
|
||||
TOPIC_COMMAND = "command"
|
||||
TOPIC_STATUS = "status"
|
||||
TOPIC_AVAILABILITY = "availability"
|
||||
TOPIC_RESPONSE = "response"
|
||||
|
||||
# Commands
|
||||
COMMAND_SHUTDOWN = "shutdown"
|
||||
COMMAND_REBOOT = "reboot"
|
||||
COMMAND_STATUS = "status"
|
||||
|
||||
# Platforms
|
||||
PLATFORMS = ["sensor", "binary_sensor", "button"]
|
||||
```
|
||||
|
||||
### 7.4 Config Flow (UI Configuration)
|
||||
|
||||
**`config_flow.py`** :
|
||||
```python
|
||||
"""Config flow for IPWatch integration."""
|
||||
import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import callback
|
||||
from .const import DOMAIN, CONF_DEVICE_IP, CONF_DEVICE_NAME
|
||||
|
||||
class IPWatchConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for IPWatch."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
# Validate user input
|
||||
await self.async_set_unique_id(user_input[CONF_DEVICE_IP])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_DEVICE_NAME],
|
||||
data=user_input
|
||||
)
|
||||
|
||||
data_schema = vol.Schema({
|
||||
vol.Required(CONF_DEVICE_IP): str,
|
||||
vol.Required(CONF_DEVICE_NAME): str,
|
||||
})
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=data_schema,
|
||||
errors=errors
|
||||
)
|
||||
```
|
||||
|
||||
### 7.5 Sensor Platform
|
||||
|
||||
**`sensor.py`** :
|
||||
```python
|
||||
"""Sensor platform for IPWatch."""
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.core import callback
|
||||
from .const import DOMAIN
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up IPWatch sensor based on a config entry."""
|
||||
device_ip = config_entry.data[CONF_DEVICE_IP]
|
||||
device_name = config_entry.data[CONF_DEVICE_NAME]
|
||||
|
||||
sensors = [
|
||||
IPWatchCPUSensor(device_ip, device_name),
|
||||
IPWatchMemorySensor(device_ip, device_name),
|
||||
IPWatchDiskSensor(device_ip, device_name),
|
||||
IPWatchUptimeSensor(device_ip, device_name),
|
||||
]
|
||||
|
||||
async_add_entities(sensors)
|
||||
|
||||
class IPWatchCPUSensor(SensorEntity):
|
||||
"""Representation of IPWatch CPU sensor."""
|
||||
|
||||
def __init__(self, device_ip, device_name):
|
||||
"""Initialize the sensor."""
|
||||
self._device_ip = device_ip
|
||||
self._device_name = device_name
|
||||
self._state = None
|
||||
self._available = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self._device_name} CPU"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique ID."""
|
||||
return f"ipwatch_{self._device_ip.replace('.', '_')}_cpu"
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return the unit of measurement."""
|
||||
return "%"
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon."""
|
||||
return "mdi:cpu-64-bit"
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self._available
|
||||
|
||||
@callback
|
||||
def _handle_status_update(self, msg):
|
||||
"""Handle MQTT status updates."""
|
||||
import json
|
||||
payload = json.loads(msg.payload)
|
||||
self._state = payload.get("cpu_percent")
|
||||
self._available = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Subscribe to MQTT topics."""
|
||||
await self.hass.components.mqtt.async_subscribe(
|
||||
f"ipwatch/device/{self._device_ip}/status",
|
||||
self._handle_status_update
|
||||
)
|
||||
```
|
||||
|
||||
### 7.6 Button Platform
|
||||
|
||||
**`button.py`** :
|
||||
```python
|
||||
"""Button platform for IPWatch."""
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from .const import DOMAIN, COMMAND_SHUTDOWN, COMMAND_REBOOT
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up IPWatch buttons."""
|
||||
device_ip = config_entry.data[CONF_DEVICE_IP]
|
||||
device_name = config_entry.data[CONF_DEVICE_NAME]
|
||||
|
||||
buttons = [
|
||||
IPWatchShutdownButton(hass, device_ip, device_name),
|
||||
IPWatchRebootButton(hass, device_ip, device_name),
|
||||
]
|
||||
|
||||
async_add_entities(buttons)
|
||||
|
||||
class IPWatchShutdownButton(ButtonEntity):
|
||||
"""Shutdown button for IPWatch device."""
|
||||
|
||||
def __init__(self, hass, device_ip, device_name):
|
||||
"""Initialize the button."""
|
||||
self.hass = hass
|
||||
self._device_ip = device_ip
|
||||
self._device_name = device_name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name."""
|
||||
return f"{self._device_name} Shutdown"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return unique ID."""
|
||||
return f"ipwatch_{self._device_ip.replace('.', '_')}_shutdown"
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return icon."""
|
||||
return "mdi:power-off"
|
||||
|
||||
async def async_press(self):
|
||||
"""Handle button press."""
|
||||
topic = f"ipwatch/device/{self._device_ip}/command"
|
||||
payload = json.dumps({
|
||||
"command": COMMAND_SHUTDOWN,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
await self.hass.components.mqtt.async_publish(
|
||||
topic,
|
||||
payload,
|
||||
qos=1
|
||||
)
|
||||
```
|
||||
|
||||
## 8. Scripts Python pour Administration
|
||||
|
||||
### 8.1 Script de Découverte Automatique
|
||||
|
||||
**`scripts/publish_discovery.py`** :
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Publie la configuration MQTT Discovery pour tous les équipements IPWatch
|
||||
"""
|
||||
import paho.mqtt.client as mqtt
|
||||
import json
|
||||
import sys
|
||||
|
||||
def publish_discovery(broker, port, device_ip, device_name):
|
||||
"""Publie les configs Discovery pour un équipement"""
|
||||
client = mqtt.Client()
|
||||
client.connect(broker, port)
|
||||
|
||||
unique_id = f"ipwatch_{device_ip.replace('.', '_')}"
|
||||
|
||||
# Sensor système
|
||||
config = {
|
||||
"name": f"{device_name} - Système",
|
||||
"unique_id": f"{unique_id}_system",
|
||||
"state_topic": f"ipwatch/device/{device_ip}/status",
|
||||
"value_template": "{{ value_json.hostname }}",
|
||||
"json_attributes_topic": f"ipwatch/device/{device_ip}/status",
|
||||
"availability": {
|
||||
"topic": f"ipwatch/device/{device_ip}/availability",
|
||||
"payload_available": "online",
|
||||
"payload_not_available": "offline"
|
||||
},
|
||||
"device": {
|
||||
"identifiers": [unique_id],
|
||||
"name": device_name,
|
||||
"model": "IPWatch MQTT Agent v1.0",
|
||||
"manufacturer": "IPWatch"
|
||||
},
|
||||
"icon": "mdi:desktop-tower",
|
||||
"qos": 1
|
||||
}
|
||||
|
||||
topic = f"homeassistant/sensor/{unique_id}/system/config"
|
||||
client.publish(topic, json.dumps(config), qos=1, retain=True)
|
||||
print(f"✓ Published sensor config for {device_name}")
|
||||
|
||||
# Binary sensor availability
|
||||
config = {
|
||||
"name": f"{device_name} - Disponibilité",
|
||||
"unique_id": f"{unique_id}_availability",
|
||||
"state_topic": f"ipwatch/device/{device_ip}/availability",
|
||||
"payload_on": "online",
|
||||
"payload_off": "offline",
|
||||
"device_class": "connectivity",
|
||||
"device": {"identifiers": [unique_id]},
|
||||
"icon": "mdi:lan-connect",
|
||||
"qos": 1
|
||||
}
|
||||
|
||||
topic = f"homeassistant/binary_sensor/{unique_id}/availability/config"
|
||||
client.publish(topic, json.dumps(config), qos=1, retain=True)
|
||||
print(f"✓ Published binary_sensor config for {device_name}")
|
||||
|
||||
# Buttons
|
||||
for cmd in ["shutdown", "reboot"]:
|
||||
config = {
|
||||
"name": f"{device_name} - {cmd.capitalize()}",
|
||||
"unique_id": f"{unique_id}_{cmd}",
|
||||
"command_topic": f"ipwatch/device/{device_ip}/command",
|
||||
"payload_press": json.dumps({"command": cmd}),
|
||||
"availability": {
|
||||
"topic": f"ipwatch/device/{device_ip}/availability",
|
||||
"payload_available": "online",
|
||||
"payload_not_available": "offline"
|
||||
},
|
||||
"device": {"identifiers": [unique_id]},
|
||||
"icon": f"mdi:{'power-off' if cmd == 'shutdown' else 'restart'}",
|
||||
"qos": 1
|
||||
}
|
||||
|
||||
topic = f"homeassistant/button/{unique_id}/{cmd}/config"
|
||||
client.publish(topic, json.dumps(config), qos=1, retain=True)
|
||||
print(f"✓ Published button {cmd} config for {device_name}")
|
||||
|
||||
client.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 5:
|
||||
print("Usage: python publish_discovery.py <broker> <port> <device_ip> <device_name>")
|
||||
sys.exit(1)
|
||||
|
||||
publish_discovery(sys.argv[1], int(sys.argv[2]), sys.argv[3], sys.argv[4])
|
||||
```
|
||||
|
||||
**Utilisation** :
|
||||
```bash
|
||||
python3 scripts/publish_discovery.py localhost 1883 10.0.0.100 "Server 01"
|
||||
```
|
||||
|
||||
## 9. Développement de l'App Home Assistant
|
||||
|
||||
### 9.1 Recommandations
|
||||
|
||||
Pour développer une application Home Assistant dédiée à IPWatch :
|
||||
|
||||
1. **Utiliser MQTT Discovery** : Simplifie l'ajout d'équipements
|
||||
2. **Créer un Custom Component** : Intégration native dans Home Assistant
|
||||
3. **Implémenter Config Flow** : Configuration via UI (Settings → Integrations)
|
||||
4. **Supporter Multiple Devices** : Un équipement IPWatch = un device HA
|
||||
5. **Gérer les états** : Mapping clair entre MQTT et entités HA
|
||||
6. **Ajouter des diagnostics** : Logs et debugging pour faciliter le support
|
||||
|
||||
### 9.2 Fonctionnalités Avancées
|
||||
|
||||
**WOL depuis Home Assistant** :
|
||||
```yaml
|
||||
# Configuration Wake-on-LAN
|
||||
wake_on_lan:
|
||||
|
||||
# Service call
|
||||
service: wake_on_lan.send_magic_packet
|
||||
data:
|
||||
mac: "AA:BB:CC:DD:EE:FF"
|
||||
broadcast_address: "192.168.1.255"
|
||||
```
|
||||
|
||||
**Intégration avec Lovelace Dashboard** :
|
||||
- Card personnalisée pour afficher grille IPWatch
|
||||
- Actions rapides (shutdown, reboot, WOL)
|
||||
- Graphiques historiques des métriques
|
||||
|
||||
**Notifications Push** :
|
||||
- Alertes sur changements d'état
|
||||
- Notifications sur nouveaux équipements détectés
|
||||
|
||||
## 10. Tests et Validation
|
||||
|
||||
### 10.1 Tester Discovery
|
||||
|
||||
```bash
|
||||
# Écouter les messages Discovery
|
||||
mosquitto_sub -h localhost -t "homeassistant/#" -v
|
||||
|
||||
# Vérifier que Home Assistant a créé les entités
|
||||
# Developer Tools → States
|
||||
# Rechercher : sensor.server_01, binary_sensor.server_01, button.server_01
|
||||
```
|
||||
|
||||
### 10.2 Tester les Commandes
|
||||
|
||||
```bash
|
||||
# Tester shutdown via MQTT
|
||||
mosquitto_pub -h localhost \
|
||||
-t "ipwatch/device/10.0.0.100/command" \
|
||||
-m '{"command":"shutdown","timestamp":"2025-12-23T10:30:00Z"}'
|
||||
|
||||
# Vérifier la réponse
|
||||
mosquitto_sub -h localhost \
|
||||
-t "ipwatch/device/10.0.0.100/response" -v
|
||||
```
|
||||
|
||||
### 10.3 Tester les Automatisations
|
||||
|
||||
1. Créer une automatisation simple
|
||||
2. Déclencher manuellement
|
||||
3. Vérifier les logs HA : Settings → System → Logs
|
||||
|
||||
## 11. Dépannage
|
||||
|
||||
### Entités non découvertes
|
||||
|
||||
```bash
|
||||
# Vérifier que Discovery est activé
|
||||
# configuration.yaml
|
||||
mqtt:
|
||||
discovery: true
|
||||
|
||||
# Republier les configs
|
||||
python3 scripts/publish_discovery.py localhost 1883 10.0.0.100 "Server 01"
|
||||
|
||||
# Redémarrer Home Assistant
|
||||
```
|
||||
|
||||
### Boutons ne fonctionnent pas
|
||||
|
||||
- Vérifier que l'agent MQTT est en ligne
|
||||
- Vérifier les permissions sudo sur le client
|
||||
- Consulter les logs de l'agent : `sudo journalctl -u ipwatch-mqtt-agent -f`
|
||||
|
||||
### Sensors non mis à jour
|
||||
|
||||
- Vérifier que le topic status est publié toutes les 30 secondes
|
||||
- Vérifier le QoS (doit être 1)
|
||||
- Vérifier la disponibilité du broker MQTT
|
||||
|
||||
## 12. Références
|
||||
|
||||
- **Home Assistant MQTT Discovery** : https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery
|
||||
- **Home Assistant Developer Docs** : https://developers.home-assistant.io/
|
||||
- **MQTT Sensor** : https://www.home-assistant.io/integrations/sensor.mqtt/
|
||||
- **MQTT Button** : https://www.home-assistant.io/integrations/button.mqtt/
|
||||
- **Config Flow** : https://developers.home-assistant.io/docs/config_entries_config_flow_handler/
|
||||
|
||||
## 13. Checklist de Développement
|
||||
|
||||
Avant de publier l'intégration Home Assistant :
|
||||
|
||||
- [ ] MQTT Discovery implémenté pour tous les composants
|
||||
- [ ] Config Flow pour configuration via UI
|
||||
- [ ] Gestion des erreurs et retry automatique
|
||||
- [ ] Documentation complète (README, HACS)
|
||||
- [ ] Tests unitaires et d'intégration
|
||||
- [ ] Validation HACS (Home Assistant Community Store)
|
||||
- [ ] Icônes et traductions
|
||||
- [ ] Changelog et versioning sémantique
|
||||
- [ ] CI/CD pour tests automatisés
|
||||
- [ ] Support multi-langues (i18n)
|
||||
441
mqtt/docs/MQTT_ARCHITECTURE.md
Normal file
441
mqtt/docs/MQTT_ARCHITECTURE.md
Normal file
@@ -0,0 +1,441 @@
|
||||
# 📐 Architecture MQTT pour IPWatch
|
||||
|
||||
## 1. Vue d'ensemble
|
||||
|
||||
L'architecture MQTT d'IPWatch implémente un système de contrôle à distance des équipements réseau basé sur le protocole MQTT (Message Queuing Telemetry Transport).
|
||||
|
||||
### 1.1 Objectifs
|
||||
|
||||
- **Contrôle centralisé** : Gérer shutdown/reboot depuis l'interface IPWatch
|
||||
- **Légèreté** : Protocol MQTT léger et performant
|
||||
- **Fiabilité** : QoS MQTT pour garantir la livraison des messages
|
||||
- **Scalabilité** : Support de centaines d'équipements simultanément
|
||||
- **Intégration** : Compatible Home Assistant et autres systèmes domotiques
|
||||
|
||||
### 1.2 Composants
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ IPWatch Ecosystem │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌───────────────┐ ┌──────────────┐ │
|
||||
│ │ Web Interface │────────►│ Backend │ │
|
||||
│ │ (Vue.js) │ HTTP │ (FastAPI) │ │
|
||||
│ └───────────────┘ API └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ paho-mqtt (publish) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ MQTT Broker (Mosquitto) │ │
|
||||
│ │ │ │
|
||||
│ │ Topics: │ │
|
||||
│ │ • ipwatch/device/{IP}/command │ │
|
||||
│ │ • ipwatch/device/{IP}/status │ │
|
||||
│ │ • ipwatch/device/{IP}/availability │ │
|
||||
│ └─────────────────┬───────────────────────────────┘ │
|
||||
│ │ MQTT (subscribe) │
|
||||
│ │ │
|
||||
│ ┌──────────┼──────────┬──────────┬──────────┐ │
|
||||
│ ▼ ▼ ▼ ▼ ▼ │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ Agent 1 │ │ Agent 2 │ │ Agent 3 │ │ ... │ │ Agent N │ │
|
||||
│ │ 10.0.0.1│ │ 10.0.0.2│ │ 10.0.0.3│ │ │ │ 10.0.0.N│ │
|
||||
│ │ │ │ │ │ │ │ │ │ │ │
|
||||
│ │ Python │ │ Python │ │ Python │ │ Python │ │ Python │ │
|
||||
│ │ systemd │ │ systemd │ │ systemd │ │ systemd │ │ systemd │ │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 2. Topics MQTT
|
||||
|
||||
### 2.1 Hiérarchie des Topics
|
||||
|
||||
```
|
||||
ipwatch/
|
||||
└── device/
|
||||
└── {IP_ADDRESS}/
|
||||
├── command # Commandes envoyées par IPWatch
|
||||
├── status # Statut publié par l'agent
|
||||
├── availability # Disponibilité de l'agent (online/offline)
|
||||
└── response # Réponses/acquittements de l'agent
|
||||
```
|
||||
|
||||
### 2.2 Détail des Topics
|
||||
|
||||
#### `ipwatch/device/{IP}/command`
|
||||
- **Direction** : IPWatch Backend → Agent
|
||||
- **QoS** : 1 (at least once)
|
||||
- **Retain** : false
|
||||
- **Payload** : JSON
|
||||
- **Commandes supportées** :
|
||||
- `shutdown` : Arrêt de la machine
|
||||
- `reboot` : Redémarrage de la machine
|
||||
- `status` : Demande de statut
|
||||
|
||||
**Exemple** :
|
||||
```json
|
||||
{
|
||||
"command": "shutdown",
|
||||
"timestamp": "2025-12-23T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### `ipwatch/device/{IP}/status`
|
||||
- **Direction** : Agent → IPWatch Backend
|
||||
- **QoS** : 1
|
||||
- **Retain** : false
|
||||
- **Payload** : JSON avec métriques système
|
||||
|
||||
**Exemple** :
|
||||
```json
|
||||
{
|
||||
"hostname": "server-01",
|
||||
"ip": "10.0.0.100",
|
||||
"platform": "Linux",
|
||||
"platform_version": "5.15.0-91-generic",
|
||||
"uptime": 86400,
|
||||
"cpu_percent": 45.2,
|
||||
"memory_percent": 62.5,
|
||||
"disk_percent": 78.3,
|
||||
"timestamp": "2025-12-23T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### `ipwatch/device/{IP}/availability`
|
||||
- **Direction** : Agent → Tous
|
||||
- **QoS** : 1
|
||||
- **Retain** : true (important!)
|
||||
- **Payload** : Texte simple
|
||||
- **Valeurs** : `online` | `offline`
|
||||
|
||||
#### `ipwatch/device/{IP}/response`
|
||||
- **Direction** : Agent → IPWatch Backend
|
||||
- **QoS** : 1
|
||||
- **Retain** : false
|
||||
- **Payload** : JSON avec résultat de la commande
|
||||
|
||||
**Exemple** :
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Shutdown en cours...",
|
||||
"timestamp": "2025-12-23T10:30:05Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Flux de Communication
|
||||
|
||||
### 3.1 Shutdown/Reboot
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as IPWatch UI
|
||||
participant BE as Backend
|
||||
participant BR as MQTT Broker
|
||||
participant AG as Agent (10.0.0.100)
|
||||
|
||||
UI->>BE: POST /api/tracking/shutdown/10.0.0.100
|
||||
BE->>BR: PUBLISH ipwatch/device/10.0.0.100/command
|
||||
Note over BR: QoS 1
|
||||
BR->>AG: {"command": "shutdown"}
|
||||
AG->>BR: PUBLISH ipwatch/device/10.0.0.100/response
|
||||
Note over AG: {"success": true, "message": "Shutdown en cours..."}
|
||||
AG->>BR: PUBLISH ipwatch/device/10.0.0.100/availability
|
||||
Note over AG: "offline"
|
||||
AG->>AG: sudo shutdown -h now
|
||||
BE-->>UI: 200 OK {"message": "Commande envoyée"}
|
||||
```
|
||||
|
||||
### 3.2 Wake-on-LAN
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant UI as IPWatch UI
|
||||
participant BE as Backend
|
||||
participant AG as Agent (offline)
|
||||
|
||||
UI->>BE: POST /api/tracking/wol/10.0.0.100
|
||||
BE->>BE: send_magic_packet(MAC)
|
||||
Note over BE: Broadcast UDP
|
||||
BE-->>UI: 200 OK
|
||||
Note over AG: Machine démarre
|
||||
AG->>AG: Agent systemd démarre
|
||||
AG->>BR: CONNECT
|
||||
AG->>BR: PUBLISH availability "online"
|
||||
AG->>BR: PUBLISH status {...}
|
||||
```
|
||||
|
||||
### 3.3 Monitoring périodique
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant AG as Agent
|
||||
participant BR as MQTT Broker
|
||||
participant HA as Home Assistant
|
||||
|
||||
loop Chaque 30 secondes
|
||||
AG->>BR: PUBLISH ipwatch/device/10.0.0.100/status
|
||||
Note over AG: CPU, RAM, Disk, Uptime
|
||||
BR->>HA: Forward status
|
||||
Note over HA: Update sensors
|
||||
end
|
||||
```
|
||||
|
||||
## 4. Gestion des Erreurs
|
||||
|
||||
### 4.1 Agent déconnecté
|
||||
|
||||
**Problème** : L'agent perd la connexion MQTT
|
||||
|
||||
**Solution** :
|
||||
- **Last Will Testament** : Message `offline` publié automatiquement sur `availability`
|
||||
- **Reconnexion automatique** : L'agent tente de se reconnecter (retry avec backoff)
|
||||
- **QoS 1** : Garantit la livraison du message d'offline
|
||||
|
||||
### 4.2 Commande non livrée
|
||||
|
||||
**Problème** : La commande n'atteint pas l'agent
|
||||
|
||||
**Solution** :
|
||||
- **QoS 1** : Le broker MQTT garantit au moins une livraison
|
||||
- **Timeout** : Le backend attend 5 secondes puis retourne une erreur
|
||||
- **Retry** : L'utilisateur peut retenter l'opération
|
||||
|
||||
### 4.3 Agent en erreur
|
||||
|
||||
**Problème** : L'agent reçoit la commande mais ne peut l'exécuter
|
||||
|
||||
**Solution** :
|
||||
- **Message de réponse** : L'agent publie sur `response` avec `success: false`
|
||||
- **Logging** : Logs détaillés dans journalctl
|
||||
- **Notification** : L'UI affiche l'erreur à l'utilisateur
|
||||
|
||||
## 5. Sécurité
|
||||
|
||||
### 5.1 Authentification
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ MQTT Broker │
|
||||
│ (Mosquitto) │
|
||||
│ │
|
||||
│ password_file: │
|
||||
│ /etc/mosquitto/passwd│
|
||||
│ │
|
||||
│ Users: │
|
||||
│ • ipwatch (backend) │
|
||||
│ • agent (clients) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
**Configuration** :
|
||||
```bash
|
||||
# Créer les utilisateurs
|
||||
mosquitto_passwd -c /etc/mosquitto/passwd ipwatch
|
||||
mosquitto_passwd /etc/mosquitto/passwd agent
|
||||
|
||||
# Dans mosquitto.conf
|
||||
allow_anonymous false
|
||||
password_file /etc/mosquitto/passwd
|
||||
```
|
||||
|
||||
### 5.2 Chiffrement TLS/SSL
|
||||
|
||||
```
|
||||
listener 8883
|
||||
cafile /etc/mosquitto/ca.crt
|
||||
certfile /etc/mosquitto/server.crt
|
||||
keyfile /etc/mosquitto/server.key
|
||||
require_certificate false
|
||||
```
|
||||
|
||||
### 5.3 ACL (Access Control List)
|
||||
|
||||
```
|
||||
# /etc/mosquitto/acl
|
||||
# Backend peut publier sur command
|
||||
user ipwatch
|
||||
topic write ipwatch/device/+/command
|
||||
|
||||
# Agents peuvent publier sur status/response/availability
|
||||
user agent
|
||||
topic write ipwatch/device/+/status
|
||||
topic write ipwatch/device/+/response
|
||||
topic write ipwatch/device/+/availability
|
||||
topic read ipwatch/device/+/command
|
||||
```
|
||||
|
||||
## 6. Performance et Scalabilité
|
||||
|
||||
### 6.1 Dimensionnement
|
||||
|
||||
| Métrique | Valeur | Notes |
|
||||
|----------|--------|-------|
|
||||
| Agents max | 500 | Limité par le broker |
|
||||
| Messages/sec | 1000 | Dépend du hardware |
|
||||
| Latence | <100ms | Sur réseau local |
|
||||
| Bande passante | ~10 KB/s par agent | Pour statut 30s |
|
||||
|
||||
### 6.2 Optimisations
|
||||
|
||||
**Payload compression** :
|
||||
- JSON minifié (pas d'indentation)
|
||||
- Champs courts
|
||||
- Pas de données redondantes
|
||||
|
||||
**Keep-alive** :
|
||||
- 60 secondes (défaut)
|
||||
- Réduit le trafic réseau
|
||||
|
||||
**QoS adaptatif** :
|
||||
- QoS 1 pour commandes critiques
|
||||
- QoS 0 pour statut non-critique
|
||||
|
||||
## 7. Compatibilité Home Assistant
|
||||
|
||||
### 7.1 MQTT Discovery
|
||||
|
||||
L'agent publie automatiquement sa configuration pour Home Assistant :
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Server 01",
|
||||
"unique_id": "ipwatch_10_0_0_100",
|
||||
"state_topic": "ipwatch/device/10.0.0.100/status",
|
||||
"availability_topic": "ipwatch/device/10.0.0.100/availability",
|
||||
"json_attributes_topic": "ipwatch/device/10.0.0.100/status",
|
||||
"device": {
|
||||
"identifiers": ["ipwatch_10_0_0_100"],
|
||||
"name": "Server 01",
|
||||
"model": "IPWatch MQTT Agent",
|
||||
"manufacturer": "IPWatch"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Topic Discovery** :
|
||||
```
|
||||
homeassistant/sensor/ipwatch_10_0_0_100/config
|
||||
```
|
||||
|
||||
### 7.2 Entités créées
|
||||
|
||||
Home Assistant crée automatiquement :
|
||||
- **Sensor** : CPU, RAM, Disk, Uptime
|
||||
- **Binary Sensor** : Availability (online/offline)
|
||||
- **Button** : Shutdown, Reboot
|
||||
|
||||
## 8. Monitoring et Logs
|
||||
|
||||
### 8.1 Logs Agent
|
||||
|
||||
```bash
|
||||
# Logs en temps réel
|
||||
sudo journalctl -u ipwatch-mqtt-agent -f
|
||||
|
||||
# Logs des dernières 24h
|
||||
sudo journalctl -u ipwatch-mqtt-agent --since "24 hours ago"
|
||||
|
||||
# Recherche d'erreurs
|
||||
sudo journalctl -u ipwatch-mqtt-agent -p err
|
||||
```
|
||||
|
||||
### 8.2 Logs Broker
|
||||
|
||||
```bash
|
||||
# Logs Mosquitto
|
||||
sudo journalctl -u mosquitto -f
|
||||
|
||||
# Activer le debug dans mosquitto.conf
|
||||
log_type all
|
||||
log_dest /var/log/mosquitto/mosquitto.log
|
||||
```
|
||||
|
||||
### 8.3 Métriques
|
||||
|
||||
**Collecte via MQTT** :
|
||||
- Nombre de clients connectés
|
||||
- Messages publiés/reçus
|
||||
- Latence des messages
|
||||
|
||||
**Grafana + Prometheus** (optionnel) :
|
||||
- Dashboard pour visualiser les métriques
|
||||
- Alertes sur déconnexions
|
||||
|
||||
## 9. Diagrammes Détaillés
|
||||
|
||||
### 9.1 État de l'Agent
|
||||
|
||||
```
|
||||
┌─────────┐
|
||||
│ Start │
|
||||
└────┬────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Load Config │
|
||||
└────┬────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Connect MQTT │──┐
|
||||
│ Broker │ │
|
||||
└────┬────────────┘ │
|
||||
│ │ Retry
|
||||
│ Success │ on error
|
||||
▼ │
|
||||
┌─────────────────┐ │
|
||||
│ Subscribe to │ │
|
||||
│ command topic │ │
|
||||
└────┬────────────┘ │
|
||||
│ │
|
||||
▼ │
|
||||
┌─────────────────┐ │
|
||||
│ Publish │◄─┘
|
||||
│ availability │
|
||||
│ "online" │
|
||||
└────┬────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Main Loop │
|
||||
│ • Listen MQTT │
|
||||
│ • Publish status│
|
||||
│ every 30s │
|
||||
└────┬────────────┘
|
||||
│
|
||||
│ Receive shutdown
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Publish │
|
||||
│ "offline" │
|
||||
└────┬────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Execute │
|
||||
│ shutdown │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## 10. Références
|
||||
|
||||
- **MQTT v3.1.1 Specification** : http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html
|
||||
- **Mosquitto Documentation** : https://mosquitto.org/documentation/
|
||||
- **Paho MQTT Python** : https://github.com/eclipse/paho.mqtt.python
|
||||
- **Home Assistant MQTT Discovery** : https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery
|
||||
|
||||
## 11. TODO / Améliorations Futures
|
||||
|
||||
- [ ] Support SSL/TLS par défaut
|
||||
- [ ] Compression des payloads (gzip)
|
||||
- [ ] Authentification par certificat client
|
||||
- [ ] Métriques Prometheus
|
||||
- [ ] Dashboard Grafana
|
||||
- [ ] Support MQTT v5
|
||||
- [ ] Broadcast pour commandes multi-équipements
|
||||
- [ ] Planification de shutdown/reboot (cron-like)
|
||||
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
|
||||
26
mqtt/systemd/ipwatch-mqtt-agent.service
Normal file
26
mqtt/systemd/ipwatch-mqtt-agent.service
Normal file
@@ -0,0 +1,26 @@
|
||||
[Unit]
|
||||
Description=IPWatch MQTT Agent
|
||||
After=network.target mosquitto.service
|
||||
Wants=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/ipwatch_mqtt_agent.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Logs
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=ipwatch-mqtt-agent
|
||||
|
||||
# Sécurité
|
||||
PrivateTmp=yes
|
||||
NoNewPrivileges=false
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
ReadWritePaths=/var/log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user