Pilot v2: Core implementation + battery telemetry

Major updates:
- Complete Rust rewrite (pilot-v2/) with working MQTT client
- Fixed MQTT event loop deadlock (background task pattern)
- Battery telemetry for Linux (auto-detected via /sys/class/power_supply)
- Home Assistant auto-discovery for all sensors and switches
- Comprehensive documentation (AVANCEMENT.md, CLAUDE.md, roadmap)
- Docker test environment with Mosquitto broker
- Helper scripts for development and testing

Features working:
 MQTT connectivity with LWT
 YAML configuration with validation
 Telemetry: CPU, memory, IP, battery (Linux)
 Commands: shutdown, reboot, sleep, screen (dry-run tested)
 HA discovery and integration
 Allowlist and cooldown protection

Ready for testing on real hardware.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Gilles Soulier
2025-12-30 06:23:00 +01:00
parent b7e67c6501
commit c5381b7112
4453 changed files with 78647 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
/monenv
README.html
+95
View File
@@ -0,0 +1,95 @@
# Aorus
## Description
installe un service qui creer des entites pour home assistant:
- etat de la batterie
- button pour eteindre le pc
- reboot
- eteindre l'ecran
- cpu
- memory
- adresse ip
- cpu frequency
## Prérequis
installation de python3 et de python-env:
```
sudo apt install python3-venv
```
```
python3 -m venv monenv
```
activation de l'environnement:
```
source monenv/bin/activate
```
## Installation
```
git clone http://10.0.1.200/pilot/aorus.git
```
```
cd aorus
```
Création de l'environnement:
```
python3 -m venv monenv
```
Activation de l'environnement:
```
source monenv/bin/activate
```
Installation des paquets additionnels:
```
pip install -r requirements.txt
```
## Test du programme
```
python3 main.py
```
Sortie de l'environnement:
```
deactivate
```
## Installation en temps que service
authorisation de shutdown: ajouter a la fin du fichier
```
sudo visudo
```
```
gilles ALL=(ALL) NOPASSWD: /sbin/shutdown
gilles ALL=(ALL) NOPASSWD: /sbin/reboot
gilles ALL=(ALL) NOPASSWD: /usr/bin/tee /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
```
Installation du service en manuel:
```
sudo cp mqtt_pilot.service /etc/systemd/system/
```
```
sudo systemctl daemon-reload
```
```
sudo systemctl enable mqtt_pilot.service
```
```
sudo systemctl start mqtt_pilot.service
```
```
sudo systemctl status mqtt_pilot.service
```
lancer l'installation du service:
```
sudo ./install.sh
```
+171
View File
@@ -0,0 +1,171 @@
# PROMPT CODEX — Redev V2 : Agent MQTT de pilotage PC + Analyse V1
## 0) Rôle et objectif
Tu es **Codex**, expert senior **Python / Windows+Linux / MQTT / packaging / sécurité**.
Tu dois **analyser un projet existant** (application de pilotage dun ordinateur via messages MQTT), en comprendre larchitecture et produire une **documentation d’état des lieux**.
Ensuite, tu dois préparer une **méthode de re-développement / re-déploiement** sous forme de **consignes de prompts** (itératives), afin que lutilisateur puisse compléter les besoins supplémentaires avant de lancer limplémentation.
Contraintes :
- Ne modifie rien tant que lanalyse nest pas terminée.
- Sois factuel : si une info nest pas trouvée dans le projet, indique “Non trouvé”.
- Structure claire, utilisable comme base de refonte.
Livrables attendus :
1) `analyse_version_1.md` (créé à la racine du repo)
2) `prompt_method_redev.md` (créé à la racine du repo)
---
## 1) Étape A — Scan & compréhension du projet
### 1.1 Cartographie
- Liste larborescence (équivalent `tree -a -I "node_modules|.git|__pycache__|dist|build"`).
- Identifie :
- langage(s), frameworks, dépendances
- points dentrée (main, service, CLI, daemon)
- modules clés (mqtt, commandes système, sécurité, config, logs, UI éventuelle)
### 1.2 Exécution et cycle de vie
Déduis et documente :
- comment lapp se lance (commande, service systemd, tâche planifiée Windows, docker ?)
- comment elle sarrête proprement
- gestion de l’état (retained, availability, LWT, reconnexion, QoS)
- compatibilité OS (Windows/Linux), permissions nécessaires
### 1.3 MQTT : topics & payloads
Reconstitue précisément (si présent) :
- broker, auth (user/pass, TLS), client_id
- topics (commandes / état / availability / logs / télémétrie)
- structure des messages (JSON ? champs, valeurs possibles)
- mapping “topic → action exécutée”
- sécurité : validation dinput, allowlist, anti-injection
### 1.4 Configuration
- Où sont les configs ? (`.env`, yaml, json, ini, registry, args CLI)
- paramètres supportés, valeurs par défaut, variables denvironnement
- secrets : comment sont-ils stockés ?
### 1.5 Dépendances & packaging
- `requirements.txt`, `pyproject.toml`, `package.json`, etc.
- version Python/Node/Go
- build : exe (pyinstaller), wheel, docker image, service
- fichiers docs existants (README, CHANGELOG, etc.)
### 1.6 Journalisation & observabilité
- logs (format, rotation)
- niveaux de logs
- métriques éventuelles
- gestion erreurs / retries / backoff
---
## 2) Étape B — Générer `analyse_version_1.md`
Crée un fichier `analyse_version_1.md` structuré comme suit (remplis avec ce que tu trouves) :
## 2.1 Résumé exécutif
- Objectif de lapp
- OS supportés
- Dépendances majeures
- Mode dexécution (service/cli/etc.)
## 2.2 Architecture
- Diagramme textuel (ASCII) des composants
- Points dentrée, modules, flux de données
## 2.3 MQTT
### 2.3.1 Connexion
- Broker, auth, TLS, client_id, keepalive, LWT
### 2.3.2 Topics
- Tableau : Topic | Direction (in/out) | QoS | Retain | Payload | Action/usage
### 2.3.3 Commandes supportées
- Liste exhaustive + exemples de payloads si trouvés
## 2.4 Commandes système exécutées
- actions (shutdown/reboot/sleep/commands) + méthode utilisée
- risques sécurité + mitigations actuelles
## 2.5 Configuration
- sources (env/files/args)
- paramètres et defaults (tableau)
## 2.6 Stockage / état
- fichiers, DB, mémoire, cache
- persistence (oui/non)
## 2.7 Déploiement actuel
- comment installer
- comment lancer
- comment update
- comment rollback
## 2.8 Points faibles / dettes techniques
- liste priorisée (P0/P1/P2)
- éléments manquants / non trouvés
## 2.9 Suggestions de refonte (sans coder)
- “quick wins”
- architecture cible probable (optionnelle)
- axes sécurité
---
## 3) Étape C — Générer `prompt_method_redev.md` (méthode de redev)
Crée un fichier `prompt_method_redev.md` qui explique **comment on va re-développer** en itérations, sous forme de “prompts à donner à Codex”, avec checkpoints.
Format attendu :
# Méthode de re-développement / re-déploiement (V2)
## 1) Pré-requis
- outils, versions, conventions repo, structure attendue
## 2) Liste des informations que lutilisateur doit compléter
Crée une section “À compléter par lutilisateur” avec des champs vides, par exemple :
- OS cibles : [ ]
- Broker MQTT : [ ]
- Auth/TLS : [ ]
- Liste commandes à supporter : [ ]
- Politique de sécurité (allowlist, signature, ACL broker) : [ ]
- Packaging désiré (exe windows, systemd linux, docker) : [ ]
- Intégration Home Assistant autodiscovery : [oui/non] + détails : [ ]
- Availability / birth / will : [ ]
- Fréquence télémétrie : [ ]
- etc.
## 3) Plan itératif (prompts)
Donne une séquence de prompts “copier-coller” :
- Prompt 1 : “Génère larchitecture cible + conventions”
- Prompt 2 : “Implémente le noyau MQTT + config + logs”
- Prompt 3 : “Implémente le moteur de commandes avec allowlist”
- Prompt 4 : “Ajoute availability/LWT + heartbeat”
- Prompt 5 : “Ajoute autodiscovery Home Assistant (si demandé)”
- Prompt 6 : “Packaging Windows (service/tâche)”
- Prompt 7 : “Packaging Linux (systemd)”
- Prompt 8 : “Tests + harness MQTT + doc + exemples”
Chaque prompt doit :
- préciser les fichiers à créer/modifier
- imposer une checklist de validation (tests, lint, doc)
- définir une sortie attendue (ex: `docs/deploy_windows.md`, `docker-compose.yml`, etc.)
## 4) Procédure de re-déploiement
- stratégie : in-place vs nouveau dossier
- migration config
- compatibilité topics (versioning)
- rollback
---
## 4) Règles de sortie
- Crée uniquement les 2 fichiers demandés.
- Si tu dois exécuter des commandes pour comprendre (ex: grep), fais-le localement dans ton environnement.
- Termine en affichant :
- un résumé de 10 lignes
- le chemin des fichiers créés
- 5 questions “À compléter par lutilisateur” (copiées depuis la section correspondante)
---
## 5) Action immédiate
Commence maintenant lanalyse du projet présent dans le dossier courant.
@@ -0,0 +1,120 @@
## 2.1 Résumé exécutif
- Objectif de lapp : exposer via MQTT (Home Assistant autodiscovery) des entites de telemetrie PC + commandes (shutdown, reboot, ecran, CPU freq).
- OS supportes : Linux (systemd, sudo, /sys, busctl GNOME). Windows : Non trouve.
- Dependances majeures : Python, paho-mqtt, psutil, pynvml.
- Mode dexecution : script Python en CLI, ou service systemd via `mqtt_pilot.service`.
## 2.2 Architecture
Diagramme textuel (ASCII) :
[systemd service] -> [main_prog.py/main.py] -> [paho-mqtt client] -> [MQTT broker]
|-> [psutil/pynvml] (telemetrie)
|-> [subprocess/busctl/sudo] (actions)
Points dentree, modules, flux de donnees :
- Points dentree : `main.py`, `main_prog.py`, `main-lenovo-bureau.py` (variantes), service systemd via `mqtt_pilot.service` -> `main_prog.py`.
- MQTT : publication dentites Home Assistant (discovery + availability) + publication detats capteurs.
- Actions systeme : shutdown, reboot, extinction/allumage ecran, changement de frequence CPU.
## 2.3 MQTT
### 2.3.1 Connexion
- Broker : 10.0.0.3:1883
- Auth : username/password vides (Non trouve pour TLS)
- TLS : Non trouve
- client_id : non defini (paho-mqtt genere un ID)
- keepalive : 60
- LWT : Non trouve (availability publie manuellement)
### 2.3.2 Topics
Le projet contient plusieurs variantes avec topics differents (noms dhote ou `device_name`).
Tableau (main_prog.py - device_name = "yoga14") :
Topic | Direction | QoS | Retain | Payload | Action/usage
- `homeassistant/switch/yoga14/shutdown_yoga14/config` | out | defaut | true | JSON discovery | autodiscovery switch shutdown
- `homeassistant/switch/yoga14/reboot_yoga14/config` | out | defaut | true | JSON discovery | autodiscovery switch reboot
- `homeassistant/switch/yoga14/screen_yoga14/config` | out | defaut | true | JSON discovery | autodiscovery switch ecran
- `homeassistant/sensor/yoga14/battery_yoga14/config` | out | defaut | true | JSON discovery | autodiscovery batterie
- `homeassistant/binary_sensor/yoga14/charging_status_yoga14/config` | out | defaut | true | JSON discovery | autodiscovery charge
- `homeassistant/sensor/yoga14/cpu_temperature_yoga14/config` | out | defaut | true | JSON discovery | autodiscovery temp CPU
- `homeassistant/sensor/yoga14/cpu_usage_yoga14/config` | out | defaut | true | JSON discovery | autodiscovery usage CPU
- `homeassistant/sensor/yoga14/memory_usage_yoga14/config` | out | defaut | true | JSON discovery | autodiscovery usage RAM
- `homeassistant/sensor/yoga14/cpu_frequency_yoga14/config` | out | defaut | true | JSON discovery | autodiscovery freq CPU
- `homeassistant/sensor/yoga14/ip_address_yoga14/config` | out | defaut | true | JSON discovery | autodiscovery IP
- `homeassistant/number/yoga14/cpu_frequency_slider_yoga14/config` | out | defaut | true | JSON discovery | autodiscovery slider freq
- `pilot/yoga14/shutdown/set` | in | defaut | - | "ON"/"OFF" | OFF -> shutdown
- `pilot/yoga14/reboot/set` | in | defaut | - | "ON"/"OFF" | OFF -> reboot
- `pilot/yoga14/screen/set` | in | defaut | - | "ON"/"OFF" | OFF/ON -> busctl display
- `pilot/yoga14/cpu_frequency_slider/set` | in | defaut | - | "<float>" GHz | set freq CPU
- `pilot/yoga14/*/state` | out | defaut | true | valeurs numeriques/strings | etats capteurs/switch
- `pilot/yoga14/*/available` | out | defaut | true | online/offline | availability
Tableau (main.py / main-lenovo-bureau.py - hostname dynamique) :
- `homeassistant/switch/<hostname>/shutdown_<hostname>/config` | out | defaut | true | JSON discovery | autodiscovery shutdown
- `homeassistant/sensor/<hostname>/cpu_temp_<hostname>/config` | out | defaut | true | JSON discovery | autodiscovery CPU temp
- `homeassistant/sensor/<hostname>/memory_used_<hostname>/config` | out | defaut | true | JSON discovery | autodiscovery RAM used
- `homeassistant/sensor/<hostname>/cpu_usage_<hostname>/config` | out | defaut | true | JSON discovery | autodiscovery CPU usage
- `homeassistant/sensor/<hostname>/gpu_temp_<hostname>/config` | out | defaut | true | JSON discovery | autodiscovery GPU temp (main.py)
- `homeassistant/sensor/<hostname>/gpu_memory_usage_<hostname>/config` | out | defaut | true | JSON discovery | autodiscovery GPU mem (main.py)
- `pilot/<hostname>/shutdown/available` | in | defaut | - | "ON"/"OFF" | OFF -> shutdown
- `pilot/<hostname>/shutdown` | out | defaut | true | "ON"/"OFF" | etat switch
- `pilot/<hostname>/<sensor>` | out | defaut | true | valeurs numeriques | capteurs
- `pilot/<hostname>/<sensor>/available` | out | defaut | true | online/offline | availability
### 2.3.3 Commandes supportees
- Shutdown : payload "OFF" sur `pilot/<device>/shutdown/set` (main_prog.py) ou `pilot/<hostname>/shutdown/available` (main.py / main-lenovo-bureau.py).
- Reboot : payload "OFF" sur `pilot/<device>/reboot/set` (main_prog.py).
- Ecran on/off : payload "ON"/"OFF" sur `pilot/<device>/screen/set` (main_prog.py).
- Slider frequence CPU : payload float en GHz sur `pilot/<device>/cpu_frequency_slider/set`.
- Telemetrie : batterie, charge, CPU temp/usage/freq, RAM, IP, GPU temp, GPU mem (selon variante).
## 2.4 Commandes systeme executees
- Shutdown : `sudo shutdown -h now`
- Reboot : `sudo reboot`
- Ecran : `busctl --user set-property org.gnome.Mutter.DisplayConfig ... PowerSaveMode i 1/0`
- CPU freq : ecriture dans `/sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed`
Risques securite + mitigations actuelles :
- Risques : execution de commandes privilegiees via MQTT sans validation, topics previsibles, pas dauth/TLS.
- Mitigations : aucune au niveau code; README recommande `sudoers` NOPASSWD.
## 2.5 Configuration
Sources : hardcode dans scripts Python.
Parametres et defaults (tableau) :
- mqtt_broker_ip_address : 10.0.0.3
- mqtt_port : 1883
- mqtt_username/mqtt_password : ""
- discovery_prefix : homeassistant
- device_name / hostname : "yoga14" ou hostname systeme
- update_interval : multiples (5/10/20/60) selon entite
Secrets : Non trouve (pas de gestion de secrets, valeurs en clair).
## 2.6 Stockage / etat
- Pas de base de donnees ni fichiers detat.
- Etat publie via MQTT, souvent en retained (availability + states).
- Persistence : non (hors retained MQTT).
## 2.7 Deploiement actuel
- Installer : venv python + `pip install -r requirements.txt`.
- Lancer : `python3 main.py` ou `python3 main_prog.py`.
- Service : `mqtt_pilot.service` + `install.sh` (systemd).
- Update : Non trouve (pas de script dupdate).
- Rollback : Non trouve.
## 2.8 Points faibles / dettes techniques
- P0 : Pas dauth/TLS MQTT; commandes privilegiees sans validation; allowlist absente.
- P1 : Config hardcodee (broker, topics, device_name); duplication de code entre scripts.
- P1 : Availability publiee en boucle sans LWT; pas de QoS/retours derreurs robustes.
- P2 : Pas de tests, pas de logs structurels, pas de packaging.
- P2 : Compatibilite Windows non traitee; dependances GPU/psutil non verifiees.
Elements manquants / non trouves :
- Fichier de configuration externe (.env/yaml/json).
- TLS, ACL broker, signature de messages.
- Definition formelle de schema de payload.
## 2.9 Suggestions de refonte (sans coder)
- Quick wins : centraliser config (env/yaml), ajouter LWT + heartbeat, factoriser modules MQTT/sensors/commands.
- Architecture cible probable : core MQTT + module commands + module sensors + config + logging; separation OS (Linux/Windows).
- Axes securite : auth/TLS broker, allowlist commandes, validation payloads, limitation topics, journalisation des actions.
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# Vérifier si le script est exécuté avec les privilèges root
if [ "$EUID" -ne 0 ]; then
echo "Veuillez exécuter ce script avec sudo ou en tant que root."
exit 1
fi
# Copier le fichier de service dans le répertoire systemd
cp mqtt_pilot.service /etc/systemd/system/
# Recharger systemd pour prendre en compte le nouveau service
systemctl daemon-reload
# Activer le service pour qu'il démarre au démarrage
systemctl enable mqtt_pilot.service
# Démarrer le service immédiatement
systemctl stop mqtt_pilot.service
systemctl start mqtt_pilot.service
echo "Le service mqtt_pilot a été installé et démarré avec succès."
@@ -0,0 +1,289 @@
# ajouter cette ligne en bas du fichier : sudo visudo
# gilles ALL=(ALL) NOPASSWD: /sbin/shutdown
# ajouter le hostname du computer
import os
import time
import json
import paho.mqtt.client as mqtt
import subprocess
import threading
import psutil
import socket
hostname = socket.gethostname()
# Fonctions pour obtenir les températures CPU
def get_cpu_temperature():
temps = psutil.sensors_temperatures()
for name, entries in temps.items():
#if name == 'k10temp':
if name == 'coretemp':
for entry in entries:
#if entry.label == 'Tctl':
if entry.label == 'Package id 0':
return entry.current
def get_cpu_usage():
cpu_usage = psutil.cpu_percent()
return round(cpu_usage, 1) # Arrondir à 1 chiffre après la virgule
# Paramètres MQTT
mqtt_broker_ip_address = "10.0.0.3"
mqtt_port = 1883
mqtt_username = ""
mqtt_password = ""
#update_frequency = 60 # Mise à jour toutes les 60 secondes
discovery_prefix = "homeassistant"
update_frequency = 5 # en secondes
# Fonction pour obtenir la quantité de mémoire utilisée
def get_memory_used():
memory_info = psutil.virtual_memory()
memory_used_mb = memory_info.used / 1024 / 1024 # Convertir en MB
return round(memory_used_mb) # Arrondir à 0 chiffre après la virgule
device_info = {
"identifiers": ["Mqtt_pilot"],
"name": f"{hostname}",
"manufacturer": "Black",
"model": "desktop",
"sw_version": "1.0.0",
"suggested_area": "salon",
}
# Configuration des entités
shutdown_entity = {
"name": f"shutdown_{hostname}",
"type": "switch",
"unique_id": f"shutdown_{hostname}_44:37:e6:6b:53:86",
"command_topic": f"pilot/{hostname}/shutdown/available",
"state_topic": f"pilot/{hostname}/shutdown",
"availability_topic": f"pilot/{hostname}/shutdown/available",
"device_class": "switch",
"payload_on": "ON",
"payload_off": "OFF",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:power",
"device": device_info,
}
cpu_temp_entity = {
"name": f"cpu_temp_{hostname}",
"type": "sensor",
"unique_id": f"cpu_temp_{hostname}_44:37:e6:6b:53:86",
"state_topic": f"pilot/{hostname}/cpu_temp",
"availability_topic": f"pilot/{hostname}/cpu_temp/available",
"device_class": "temperature",
"unit_of_measurement": "°C",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:thermometer",
"device": device_info,
}
# Définition de l'entité memory_used
memory_used_entity = {
"name": f"memory_used_{hostname}",
"type": "sensor",
"unique_id": f"memory_used_{hostname}_44:37:e6:6b:53:86",
"state_topic": f"pilot/{hostname}/memory_used",
"availability_topic": f"pilot/{hostname}/memory_used/available",
# "device_class": "memory",
"unit_of_measurement": "MB",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:memory",
"device": device_info,
}
cpu_usage_entity = {
"name": f"cpu_usage_{hostname}",
"type": "sensor",
"unique_id": f"cpu_usage_{hostname}_44:37:e6:6b:53:86",
"state_topic": f"pilot/{hostname}/cpu_usage",
"availability_topic": f"pilot/{hostname}/cpu_usage/available",
"unit_of_measurement": "%",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:memory",
"device": device_info,
}
def publish_discovery_messages(client):
# Publie les messages de découverte pour les entités
# ...
print("publish_discovery_messages")
client.publish(
f"{discovery_prefix}/switch/{hostname}/{shutdown_entity['name']}/config",
json.dumps(shutdown_entity),
retain=True,
)
# Publication de la configuration du capteur cpu_temp
client.publish(
f"{discovery_prefix}/sensor/{hostname}/{cpu_temp_entity['name']}/config",
json.dumps(cpu_temp_entity),
retain=True,
)
# Publication de la configuration du capteur memory_used
client.publish(
f"{discovery_prefix}/sensor/{hostname}/{memory_used_entity['name']}/config",
json.dumps(memory_used_entity),
retain=True,
)
# Publication de la disponibilité pour l'entité cpu_usage
client.publish(
f"{discovery_prefix}/sensor/{hostname}/{cpu_usage_entity['name']}/config",
json.dumps(cpu_usage_entity),
retain=True,
)
# client.publish(f"{discovery_prefix}/sensor/{battery_entity['name']}/config", json.dumps(battery_entity), retain=True)
def publish_availability(client):
client.publish(
shutdown_entity["availability_topic"],
shutdown_entity["payload_available"],
retain=True,
)
# Publication de la disponibilité pour l'entité cpu_temp
client.publish(
cpu_temp_entity["availability_topic"],
cpu_temp_entity["payload_available"],
retain=True,
)
# Publication de la disponibilité pour l'entité memory_used
client.publish(
memory_used_entity["availability_topic"],
memory_used_entity["payload_available"],
retain=True,
)
client.publish(
cpu_usage_entity["availability_topic"],
cpu_usage_entity["payload_available"],
retain=True,
)
print("Published availability for all entities")
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected with result code {rc}")
client.subscribe(shutdown_entity["command_topic"])
publish_discovery_messages(client)
publish_availability(client)
# publish_sensor_data(
# client
# ) # Démarre la première publication des données du capteur
# Publier l'état "ON" pour le switch au démarrage
client.publish(
shutdown_entity["state_topic"], shutdown_entity["payload_on"], retain=True
)
print(f"Set {shutdown_entity['name']} to ON")
def on_message(client, userdata, message):
# Gestion des messages MQTT
print("on_message")
# Vérifier si le message est pour le switch "shutdown"
if message.topic == shutdown_entity["command_topic"]:
if message.payload.decode() == shutdown_entity["payload_off"]:
print("Received 'OFF' command - shutting down the system")
client.publish(
shutdown_entity["state_topic"],
shutdown_entity["payload_off"],
retain=True,
)
# Exécuter la commande de shutdown
time.sleep(1)
client.publish(
shutdown_entity["availability_topic"],
shutdown_entity["payload_not_available"],
retain=True,
)
time.sleep(1)
subprocess.run(["sudo", "shutdown", "-h", "now"])
elif message.payload.decode() == shutdown_entity["payload_on"]:
print("Received 'ON' command - no action for 'ON'")
def publish_sensor_values(client):
cpu_temp = get_cpu_temperature()
if cpu_temp is not None:
client.publish(cpu_temp_entity["state_topic"], round(cpu_temp, 1), retain=True)
memory_used = get_memory_used()
if memory_used is not None:
client.publish(memory_used_entity["state_topic"], memory_used, retain=True)
cpu_usage = get_cpu_usage()
if cpu_usage is not None:
client.publish(cpu_usage_entity["state_topic"], cpu_usage, retain=True)
# Configuration et démarrage du client MQTT
client = mqtt.Client()
client.username_pw_set(mqtt_username, mqtt_password)
client.on_connect = on_connect
client.on_message = on_message
client.connect(mqtt_broker_ip_address, mqtt_port, 60)
client.loop_start()
# Maintenir le script en exécution
try:
while True:
publish_availability(client) # Maintenir l'état disponible
publish_sensor_values(client) # Publier les valeurs des capteurs
time.sleep(update_frequency) # Attendre avant la prochaine mise à jour
except KeyboardInterrupt:
print("Script interrupted, closing MQTT connection")
# Publier l'état "unavailable" pour les entités
client.publish(
shutdown_entity["availability_topic"],
shutdown_entity["payload_not_available"],
retain=True,
)
client.publish(
cpu_temp_entity["availability_topic"],
cpu_temp_entity["payload_not_available"],
retain=True,
)
client.publish(
memory_used_entity["availability_topic"],
memory_used_entity["payload_not_available"],
retain=True,
)
client.publish(
cpu_usage_entity["availability_topic"],
cpu_usage_entity["payload_not_available"],
retain=True,
)
client.disconnect()
+404
View File
@@ -0,0 +1,404 @@
# ajouter cette ligne en bas du fichier : sudo visudo
# gilles ALL=(ALL) NOPASSWD: /sbin/shutdown
# ajouter le hostname du computer
import os
import time
import json
import paho.mqtt.client as mqtt
import subprocess
import threading
import psutil
import pynvml
import socket
hostname = socket.gethostname()
# Initialisation de pynvml
pynvml.nvmlInit()
# Fonctions pour obtenir les températures CPU et GPU
def get_cpu_temperature():
temps = psutil.sensors_temperatures()
for name, entries in temps.items():
if name == 'k10temp':
for entry in entries:
if entry.label == 'Tctl':
return entry.current
def get_gpu_temperature():
try:
handle = pynvml.nvmlDeviceGetHandleByIndex(0)
return pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU)
except pynvml.NVMLError as error:
print(f"Failed to get GPU temperature: {error}")
return None
def get_gpu_memory_usage():
pynvml.nvmlInit()
handle = pynvml.nvmlDeviceGetHandleByIndex(0) # 0 pour la première carte graphique
info = pynvml.nvmlDeviceGetMemoryInfo(handle)
pynvml.nvmlShutdown()
memory_used_gb = info.used / 1024 / 1024 # Convertir en GB
return round(memory_used_gb, 2) # Arrondir à 2 chiffres après la virgule
# Paramètres MQTT
mqtt_broker_ip_address = "10.0.0.3"
mqtt_port = 1883
mqtt_username = ""
mqtt_password = ""
#update_frequency = 60 # Mise à jour toutes les 60 secondes
discovery_prefix = "homeassistant"
update_frequency = 5 # en secondes
# Fonction pour obtenir la quantité de mémoire utilisée
def get_memory_used():
memory_info = psutil.virtual_memory()
memory_used_mb = memory_info.used / 1024 / 1024 # Convertir en MB
return round(memory_used_mb) # Arrondir à 0 chiffre après la virgule
def get_cpu_usage():
cpu_usage = psutil.cpu_percent()
return round(cpu_usage, 1) # Arrondir à 1 chiffre après la virgule
device_info = {
"identifiers": ["Mqtt_pilot"],
"name": f"{hostname}",
"manufacturer": "Black",
"model": "desktop",
"sw_version": "1.0.0",
"suggested_area": "salon",
}
# Configuration des entités
shutdown_entity = {
"name": f"shutdown_{hostname}",
"type": "switch",
"unique_id": f"shutdown_{hostname}_18:c0:4d:b5:65:74",
"command_topic": f"pilot/{hostname}/shutdown/available",
"state_topic": f"pilot/{hostname}/shutdown",
"availability_topic": f"pilot/{hostname}/shutdown/available",
"device_class": "switch",
"payload_on": "ON",
"payload_off": "OFF",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:power",
"device": device_info,
}
# battery_entity = {
# "name": f"battery_{hostname}",
# "type": "sensor",
# "unique_id": f"battery_{hostname}_18:c0:4d:b5:65:74",
# "state_topic": f"pilot/{hostname}/battery",
# "unit_of_measurement": "%",
# "device_class": "battery",
# "availability_topic": f"pilot/{hostname}/battery/available",
# "payload_available": "online",
# "payload_not_available": "offline",
# "icon": "mdi:battery",
# "device": device_info,
# }
cpu_temp_entity = {
"name": f"cpu_temp_{hostname}",
"type": "sensor",
"unique_id": f"cpu_temp_{hostname}_18:c0:4d:b5:65:74",
"state_topic": f"pilot/{hostname}/cpu_temp",
"availability_topic": f"pilot/{hostname}/cpu_temp/available",
"device_class": "temperature",
"unit_of_measurement": "°C",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:thermometer",
"device": device_info,
}
gpu_temp_entity = {
"name": f"gpu_temp_{hostname}",
"type": "sensor",
"unique_id": f"gpu_temp_{hostname}_18:c0:4d:b5:65:74",
"state_topic": f"pilot/{hostname}/gpu_temp",
"availability_topic": f"pilot/{hostname}/gpu_temp/available",
"device_class": "temperature",
"unit_of_measurement": "°C",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:thermometer",
"device": device_info,
}
# Définition de l'entité memory_used
memory_used_entity = {
"name": f"memory_used_{hostname}",
"type": "sensor",
"unique_id": f"memory_used_{hostname}_18:c0:4d:b5:65:74",
"state_topic": f"pilot/{hostname}/memory_used",
"availability_topic": f"pilot/{hostname}/memory_used/available",
# "device_class": "memory",
"unit_of_measurement": "MB",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:memory",
"device": device_info,
}
gpu_memory_usage_entity = {
"name": f"gpu_memory_usage_{hostname}",
"type": "sensor",
"unique_id": f"gpu_memory_used_{hostname}_18:c0:4d:b5:65:74",
"state_topic": f"pilot/{hostname}/gpu_memory_usage/state",
"availability_topic": f"pilot/{hostname}/gpu_memory_usage/available",
"unit_of_measurement": "MB",
# "device_class": "memory",
"unit_of_measurement": "MB",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:memory",
"device": device_info,
}
cpu_usage_entity = {
"name": f"cpu_usage_{hostname}",
"type": "sensor",
"unique_id": f"cpu_usage_{hostname}_18:c0:4d:b5:65:74",
"state_topic": f"pilot/{hostname}/cpu_usage",
"availability_topic": f"pilot/{hostname}/cpu_usage/available",
"unit_of_measurement": "%",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:memory",
"device": device_info,
}
def publish_discovery_messages(client):
# Publie les messages de découverte pour les entités
# ...
print("publish_discovery_messages")
client.publish(
f"{discovery_prefix}/switch/{hostname}/{shutdown_entity['name']}/config",
json.dumps(shutdown_entity),
retain=True,
)
# Publication de la configuration du capteur cpu_temp
client.publish(
f"{discovery_prefix}/sensor/{hostname}/{cpu_temp_entity['name']}/config",
json.dumps(cpu_temp_entity),
retain=True,
)
# Publication de la configuration du capteur gpu_temp
client.publish(
f"{discovery_prefix}/sensor/{hostname}/{gpu_temp_entity['name']}/config",
json.dumps(gpu_temp_entity),
retain=True,
)
# Publication de la configuration du capteur memory_used
client.publish(
f"{discovery_prefix}/sensor/{hostname}/{memory_used_entity['name']}/config",
json.dumps(memory_used_entity),
retain=True,
)
# Publication de la disponibilité pour l'entité cpu_usage
client.publish(
f"{discovery_prefix}/sensor/{hostname}/{cpu_usage_entity['name']}/config",
json.dumps(cpu_usage_entity),
retain=True,
)
# Publication de la configuration du capteur gpu_memory_usage
client.publish(
f"{discovery_prefix}/sensor/{hostname}/{gpu_memory_usage_entity['name']}/config",
json.dumps(gpu_memory_usage_entity),
retain=True,
)
# client.publish(f"{discovery_prefix}/sensor/{battery_entity['name']}/config", json.dumps(battery_entity), retain=True)
def publish_availability(client):
client.publish(
shutdown_entity["availability_topic"],
shutdown_entity["payload_available"],
retain=True,
)
# Publication de la disponibilité pour l'entité cpu_temp
client.publish(
cpu_temp_entity["availability_topic"],
cpu_temp_entity["payload_available"],
retain=True,
)
# Publication de la disponibilité pour l'entité gpu_temp
client.publish(
gpu_temp_entity["availability_topic"],
gpu_temp_entity["payload_available"],
retain=True,
)
# Publication de la disponibilité pour l'entité memory_used
client.publish(
memory_used_entity["availability_topic"],
memory_used_entity["payload_available"],
retain=True,
)
client.publish(
cpu_usage_entity["availability_topic"],
cpu_usage_entity["payload_available"],
retain=True,
)
# Publication de la disponibilité pour l'entité gpu_memory_usage
client.publish(
gpu_memory_usage_entity["availability_topic"],
"online", # ou "offline" si le capteur n'est pas disponible
retain=True,
)
# client.publish(
# battery_entity["availability_topic"],
# battery_entity["payload_available"],
# retain=True,
# )
print("Published availability for all entities")
# def publish_sensor_data(client):
# battery_level = get_battery_level()
# if battery_level is not None:
# client.publish(battery_entity["state_topic"], battery_level, retain=True)
# threading.Timer(update_frequency, publish_sensor_data, [client]).start()
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected with result code {rc}")
client.subscribe(shutdown_entity["command_topic"])
publish_discovery_messages(client)
publish_availability(client)
# publish_sensor_data(
# client
# ) # Démarre la première publication des données du capteur
# Publier l'état "ON" pour le switch au démarrage
client.publish(
shutdown_entity["state_topic"], shutdown_entity["payload_on"], retain=True
)
print(f"Set {shutdown_entity['name']} to ON")
def on_message(client, userdata, message):
# Gestion des messages MQTT
print("on_message")
# Vérifier si le message est pour le switch "shutdown"
if message.topic == shutdown_entity["command_topic"]:
if message.payload.decode() == shutdown_entity["payload_off"]:
print("Received 'OFF' command - shutting down the system")
client.publish(
shutdown_entity["state_topic"],
shutdown_entity["payload_off"],
retain=True,
)
# Exécuter la commande de shutdown
time.sleep(1)
client.publish(
shutdown_entity["availability_topic"],
shutdown_entity["payload_not_available"],
retain=True,
)
# client.publish(
# battery_entity["availability_topic"],
# battery_entity["payload_not_available"],
# retain=True,
# )
time.sleep(1)
subprocess.run(["sudo", "shutdown", "-h", "now"])
elif message.payload.decode() == shutdown_entity["payload_on"]:
print("Received 'ON' command - no action for 'ON'")
def publish_sensor_values(client):
cpu_temp = get_cpu_temperature()
if cpu_temp is not None:
client.publish(cpu_temp_entity["state_topic"], round(cpu_temp, 1), retain=True)
gpu_temp = get_gpu_temperature()
if gpu_temp is not None:
client.publish(gpu_temp_entity["state_topic"], round(gpu_temp, 1), retain=True)
memory_used = get_memory_used()
if memory_used is not None:
client.publish(memory_used_entity["state_topic"], memory_used, retain=True)
cpu_usage = get_cpu_usage()
if cpu_usage is not None:
client.publish(cpu_usage_entity["state_topic"], cpu_usage, retain=True)
gpu_memory_usage = get_gpu_memory_usage()
if gpu_memory_usage is not None:
client.publish(gpu_memory_usage_entity["state_topic"], gpu_memory_usage, retain=True)
# def get_battery_level():
# try:
# with open("/sys/class/power_supply/BAT0/capacity", "r") as file:
# return file.read().strip()
# except Exception as e:
# print(f"Error reading battery level: {e}")
# return None
# Configuration et démarrage du client MQTT
client = mqtt.Client()
client.username_pw_set(mqtt_username, mqtt_password)
client.on_connect = on_connect
client.on_message = on_message
client.connect(mqtt_broker_ip_address, mqtt_port, 60)
client.loop_start()
# Maintenir le script en exécution
try:
while True:
publish_availability(client) # Maintenir l'état disponible
publish_sensor_values(client) # Publier les valeurs des capteurs
time.sleep(update_frequency) # Attendre avant la prochaine mise à jour
except KeyboardInterrupt:
print("Script interrupted, closing MQTT connection")
# Publier l'état "unavailable" pour les entités
client.publish(
shutdown_entity["availability_topic"],
shutdown_entity["payload_not_available"],
retain=True,
)
client.publish(
cpu_temp_entity["availability_topic"],
cpu_temp_entity["payload_not_available"],
retain=True,
)
client.publish(
gpu_temp_entity["availability_topic"],
gpu_temp_entity["payload_not_available"],
retain=True,
)
client.publish(
memory_used_entity["availability_topic"],
memory_used_entity["payload_not_available"],
retain=True,
)
client.publish(
cpu_usage_entity["availability_topic"],
cpu_usage_entity["payload_not_available"],
retain=True,
)
client.publish(
gpu_memory_usage_entity["availability_topic"],
gpu_memory_usage_entity["payload_not_available"],
retain=True,
)
# client.publish(
# battery_entity["availability_topic"],
# battery_entity["payload_not_available"],
# retain=True,
# )
# Fermer la connexion MQTT proprement
client.disconnect()
+535
View File
@@ -0,0 +1,535 @@
# ajouter cette ligne en bas du fichier : sudo visudo
# gilles ALL=(ALL) NOPASSWD: /sbin/shutdown
# gilles ALL=(ALL) NOPASSWD: /sbin/reboot
# gilles ALL=(ALL) NOPASSWD: /usr/bin/tee /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
# structure du message discovery <discovery_prefix>/<component>/[<node_id>/]<object_id>/config
import os
import time
import json
import paho.mqtt.client as mqtt
import subprocess
import threading
import psutil
import threading
stop_threads = threading.Event()
# Paramètres MQTT
mqtt_broker_ip_address = "10.0.0.3"
mqtt_port = 1883
mqtt_username = ""
mqtt_password = ""
default_update_frequency = 60 # Mise à jour toutes les 60 secondes
discovery_prefix = "homeassistant"
device_name = "yoga14"
mac_address = "60:57:18:99:ed:05"
node_id = device_name
mise_a_jours_frequente = 5
mise_a_jour_moyenne = 30
mise_a_jour_lente = 60
device_info = {
"identifiers": [device_name],
"name": "Yoga 14",
"manufacturer": "Lenovo",
"model": "laptop",
"sw_version": "1.0.0",
"suggested_area": "salon",
}
# Configuration des entités
shutdown_entity = {
"name": f"shutdown_{device_name}",
"type": "switch",
"unique_id": f"shutdown_{device_name}_{mac_address}",
"command_topic": f"pilot/{device_name}/shutdown/set",
"state_topic": f"pilot/{device_name}/shutdown/state",
"availability_topic": f"pilot/{device_name}/shutdown/available",
"device_class": "switch",
"payload_on": "ON",
"payload_off": "OFF",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:power",
"device": device_info,
}
reboot_entity = {
"name": f"reboot_{device_name}",
"type": "switch",
"unique_id": f"reboot_{device_name}_{mac_address}",
"command_topic": f"pilot/{device_name}/reboot/set",
"state_topic": f"pilot/{device_name}/reboot/state",
"availability_topic": f"pilot/{device_name}/reboot/available",
"device_class": "switch",
"payload_on": "ON",
"payload_off": "OFF",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:restart",
"device": device_info,
}
battery_entity = {
"name": f"battery_{device_name}",
"type": "sensor",
"unique_id": f"battery_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/battery/state",
"unit_of_measurement": "%",
"device_class": "battery",
"availability_topic": f"pilot/{device_name}/battery/available",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:battery",
"device": device_info,
"update_interval": 60
}
charging_status_entity = {
"name": f"charging_status_{device_name}",
"type": "binary_sensor",
"unique_id": f"charging_status_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/charging_status/state",
"availability_topic": f"pilot/{device_name}/charging_status/available",
"device_class": "battery_charging",
"payload_on": "ON",
"payload_off": "OFF",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:battery-charging",
"device": device_info,
"update_interval": 5
}
screen_entity = {
"name": f"screen_{device_name}",
"type": "switch",
"unique_id": f"screen_{device_name}_{mac_address}",
"command_topic": f"pilot/{device_name}/screen/set",
"state_topic": f"pilot/{device_name}/screen/state",
"availability_topic": f"pilot/{device_name}/screen/available",
"device_class": "switch",
"payload_on": "ON",
"payload_off": "OFF",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:monitor",
"device": device_info,
}
# Ajout des nouvelles entités pour le CPU et la mémoire
cpu_temperature_entity = {
"name": f"cpu_temperature_{device_name}",
"type": "sensor",
"unique_id": f"cpu_temperature_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/cpu_temperature/state",
"unit_of_measurement": "°C",
"device_class": "temperature",
"availability_topic": f"pilot/{device_name}/cpu_temperature/available",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:thermometer",
"device": device_info,
"update_interval": 20
}
cpu_usage_entity = {
"name": f"cpu_usage_{device_name}",
"type": "sensor",
"unique_id": f"cpu_usage_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/cpu_usage/state",
"unit_of_measurement": "%",
"device_class": "power",
"availability_topic": f"pilot/{device_name}/cpu_usage/available",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:chip",
"device": device_info,
"update_interval": 10
}
memory_usage_entity = {
"name": f"memory_usage_{device_name}",
"type": "sensor",
"unique_id": f"memory_usage_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/memory_usage/state",
"unit_of_measurement": "%",
# "device_class": "memory",
"availability_topic": f"pilot/{device_name}/memory_usage/available",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:memory",
"device": device_info,
"update_interval": 10
}
cpu_frequency_entity = {
"name": f"cpu_frequency_{device_name}",
"type": "sensor",
"unique_id": f"cpu_frequency_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/cpu_frequency/state",
"unit_of_measurement": "GHz",
"device_class": "frequency",
"availability_topic": f"pilot/{device_name}/cpu_frequency/available",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:speedometer",
"device": device_info,
"update_interval": 20
}
ip_address_entity = {
"name": f"ip_address_{device_name}",
"type": "sensor",
"unique_id": f"ip_address_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/ip_address/state",
"availability_topic": f"pilot/{device_name}/ip_address/available",
#"device_class": "connectivity",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:ip",
"device": device_info,
"update_interval": 60
}
cpu_frequency_slider_entity = {
"name": f"cpu_frequency_slider_{device_name}",
"type": "number",
"unique_id": f"cpu_frequency_slider_{device_name}_{mac_address}",
"command_topic": f"pilot/{device_name}/cpu_frequency_slider/set",
"state_topic": f"pilot/{device_name}/cpu_frequency_slider/state",
"availability_topic": f"pilot/{device_name}/cpu_frequency_slider/available",
"payload_available": "online",
"payload_not_available": "offline",
"min": 0.5,
"max": 3.0,
"step": 0.1,
"unit_of_measurement": "GHz",
"icon": "mdi:speedometer",
"device": device_info,
}
def publish_discovery_messages(client):
# Publie les messages de découverte pour les entités
# ...
# print("publish_discovery_messages")
client.publish(f"{discovery_prefix}/switch/{node_id}/{shutdown_entity['name']}/config", json.dumps(shutdown_entity), retain=True)
client.publish(f"{discovery_prefix}/switch/{node_id}/{reboot_entity['name']}/config", json.dumps(reboot_entity), retain=True)
client.publish(f"{discovery_prefix}/sensor/{node_id}/{battery_entity['name']}/config", json.dumps(battery_entity), retain=True)
client.publish(f"{discovery_prefix}/switch/{node_id}/{screen_entity['name']}/config", json.dumps(screen_entity), retain=True)
client.publish(f"{discovery_prefix}/sensor/{node_id}/{cpu_temperature_entity['name']}/config", json.dumps(cpu_temperature_entity), retain=True)
client.publish(f"{discovery_prefix}/sensor/{node_id}/{cpu_usage_entity['name']}/config", json.dumps(cpu_usage_entity), retain=True)
client.publish(f"{discovery_prefix}/sensor/{node_id}/{memory_usage_entity['name']}/config", json.dumps(memory_usage_entity), retain=True)
client.publish(f"{discovery_prefix}/sensor/{node_id}/{cpu_frequency_entity['name']}/config", json.dumps(cpu_frequency_entity), retain=True)
client.publish(f"{discovery_prefix}/binary_sensor/{node_id}/{charging_status_entity['name']}/config", json.dumps(charging_status_entity), retain=True)
client.publish(f"{discovery_prefix}/sensor/{node_id}/{ip_address_entity['name']}/config", json.dumps(ip_address_entity), retain=True)
client.publish(f"{discovery_prefix}/number/{node_id}/{cpu_frequency_slider_entity['name']}/config", json.dumps(cpu_frequency_slider_entity), retain=True)
# print("Discovery messages published")
def publish_availability(client):
client.publish(shutdown_entity["availability_topic"], shutdown_entity["payload_available"], retain=True)
client.publish(reboot_entity["availability_topic"], reboot_entity["payload_available"], retain=True)
client.publish(battery_entity["availability_topic"], battery_entity["payload_available"], retain=True)
client.publish(screen_entity["availability_topic"], screen_entity["payload_available"], retain=True)
client.publish(cpu_temperature_entity["availability_topic"], cpu_temperature_entity["payload_available"], retain=True)
client.publish(cpu_usage_entity["availability_topic"], cpu_usage_entity["payload_available"], retain=True)
client.publish(memory_usage_entity["availability_topic"], memory_usage_entity["payload_available"], retain=True)
client.publish(cpu_frequency_entity["availability_topic"], cpu_frequency_entity["payload_available"], retain=True)
client.publish(charging_status_entity["availability_topic"], charging_status_entity["payload_available"], retain=True)
client.publish(ip_address_entity["availability_topic"], ip_address_entity["payload_available"], retain=True)
client.publish(cpu_frequency_slider_entity["availability_topic"], cpu_frequency_slider_entity["payload_available"], retain=True)
# print("Published availability for all entities")
def get_local_ip_address():
try:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(0)
s.connect(('10.254.254.254', 1)) # Adresse IP arbitraire pour établir une connexion
local_ip = s.getsockname()[0]
s.close()
print(f"Publishing IP address: {local_ip}") # Log l'adresse IP
return local_ip
except Exception as e:
print(f"Error retrieving local IP address: {e}")
return None
def get_cpu_temperature():
# Utilisation de psutil ou autre pour obtenir la température du CPU
try:
temp = psutil.sensors_temperatures()['coretemp'][0].current # Par exemple pour les CPUs Intel
print(f"Publishing CPU temperature: {temp}°C")
return temp
except Exception as e:
print(f"Error reading CPU temperature: {e}")
return None
def get_cpu_usage():
try:
cpu_percent = psutil.cpu_percent(interval=1)
print(f"CPU usage: {cpu_percent}%") # Ajoute ce log pour déboguer
return cpu_percent
except Exception as e:
print(f"Error reading CPU usage: {e}")
return None
def get_memory_usage():
try:
memory_info = psutil.virtual_memory()
print(f"Memory usage: {memory_info.percent}%") # Ajoute ce log pour déboguer
return memory_info.percent
except Exception as e:
print(f"Error reading memory usage: {e}")
return None
def get_cpu_frequency():
try:
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq", "r") as file:
# La fréquence est en kHz, donc on la convertit en GHz
freq_khz = int(file.read().strip())
freq_ghz = freq_khz / 1_000_000 # Conversion de kHz à GHz
print(f"CPU frequency: {freq_ghz:.2f} GHz") # Ajoute ce log pour déboguer
return f"{freq_ghz:.2f}" # Formater avec 2 chiffres après la virgule
except Exception as e:
print(f"Error reading CPU frequency: {e}")
return None
def get_battery_level():
try:
with open("/sys/class/power_supply/BAT0/capacity", "r") as file:
battery_level = file.read().strip()
print(f"Publishing battery level: {battery_level}%")
return battery_level
except Exception as e:
print(f"Error reading battery level: {e}")
return None
def get_charging_status():
try:
with open("/sys/class/power_supply/ADP1/online", "r") as file:
status = file.read().strip()
print(f"Publishing charging status: {status}")
return "ON" if status == "1" else "OFF"
except Exception as e:
print(f"Error reading charging status: {e}")
return None
def set_cpu_frequency(frequency):
try:
frequency_khz = int(float(frequency) * 1_000_000) # Convertir GHz en kHz
with open("/sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed", "w") as file:
file.write(str(frequency_khz))
print(f"Set CPU frequency to {frequency} GHz")
except Exception as e:
print(f"Error setting CPU frequency: {e}")
def publish_sensor_data(client):
battery_level = get_battery_level()
if battery_level is not None:
client.publish(battery_entity["state_topic"], battery_level, retain=True)
cpu_temp = get_cpu_temperature()
if cpu_temp is not None:
client.publish(cpu_temperature_entity["state_topic"], cpu_temp, retain=True)
cpu_usage = get_cpu_usage()
if cpu_usage is not None:
client.publish(cpu_usage_entity["state_topic"], cpu_usage, retain=True)
memory_usage = get_memory_usage()
if memory_usage is not None:
print(f"Publishing memory usage: {memory_usage}%") # Ajoute ce log
client.publish(memory_usage_entity["state_topic"], memory_usage, retain=True)
cpu_freq = get_cpu_frequency()
if cpu_freq is not None:
client.publish(cpu_frequency_entity["state_topic"], cpu_freq, retain=True)
print(f"Published CPU frequency: {cpu_freq} GHz")
charging_status = get_charging_status()
if charging_status is not None:
client.publish(charging_status_entity["state_topic"], charging_status, retain=True)
threading.Timer(update_frequency, publish_sensor_data, [client]).start()
def publish_battery_level(client):
battery_level = get_battery_level()
if battery_level is not None:
client.publish(battery_entity["state_topic"], battery_level, retain=True)
threading.Timer(get_update_interval(battery_entity), publish_battery_level, [client]).start()
def publish_cpu_temperature(client):
cpu_temp = get_cpu_temperature()
if cpu_temp is not None:
client.publish(cpu_temperature_entity["state_topic"], cpu_temp, retain=True)
threading.Timer(get_update_interval(cpu_temperature_entity), publish_cpu_temperature, [client]).start()
def publish_cpu_usage(client):
cpu_usage = get_cpu_usage()
if cpu_usage is not None:
client.publish(cpu_usage_entity["state_topic"], cpu_usage, retain=True)
threading.Timer(get_update_interval(cpu_usage_entity), publish_cpu_usage, [client]).start()
def publish_memory_usage(client):
memory_usage = get_memory_usage()
if memory_usage is not None:
client.publish(memory_usage_entity["state_topic"], memory_usage, retain=True)
threading.Timer(get_update_interval(memory_usage_entity), publish_memory_usage, [client]).start()
def publish_cpu_frequency(client):
cpu_freq = get_cpu_frequency()
if cpu_freq is not None:
client.publish(cpu_frequency_entity["state_topic"], cpu_freq, retain=True)
threading.Timer(get_update_interval(cpu_frequency_entity), publish_cpu_frequency, [client]).start()
def publish_charging_status(client):
charging_status = get_charging_status()
if charging_status is not None:
client.publish(charging_status_entity["state_topic"], charging_status, retain=True)
threading.Timer(get_update_interval(charging_status_entity), publish_charging_status, [client]).start()
def publish_ip_address(client):
local_ip = get_local_ip_address()
if local_ip is not None:
client.publish(ip_address_entity["state_topic"], local_ip, retain=True)
threading.Timer(get_update_interval(ip_address_entity), publish_ip_address, [client]).start()
def publish_current_cpu_frequency(client):
cpu_freq = get_cpu_frequency()
if cpu_freq is not None:
client.publish(cpu_frequency_slider_entity["state_topic"], cpu_freq, retain=True)
def get_update_interval(entity):
return entity.get("update_interval", default_update_frequency)
def on_connect(client, userdata, flags, rc):
if rc == 0:
print("Connected with result code {rc}")
client.subscribe(shutdown_entity["command_topic"])
client.subscribe(reboot_entity["command_topic"])
client.subscribe(screen_entity["command_topic"])
client.subscribe(cpu_frequency_slider_entity["command_topic"]) # S'abonner au slider
publish_discovery_messages(client)
publish_availability(client)
# Démarrer la publication des données avec les fréquences respectives
publish_battery_level(client)
publish_cpu_temperature(client)
publish_cpu_usage(client)
publish_memory_usage(client)
publish_cpu_frequency(client)
publish_charging_status(client)
publish_ip_address(client) # Ajout de la publication de l'adresse IP
publish_current_cpu_frequency(client) # Publier l'état initial du slider
# Publier l'état "ON" pour le switch au démarrage
client.publish(shutdown_entity["state_topic"], shutdown_entity["payload_on"], retain=True)
client.publish(reboot_entity["state_topic"], reboot_entity["payload_on"], retain=True)
client.publish(screen_entity["state_topic"], screen_entity["payload_on"], retain=True)
print(f"Set {shutdown_entity['name']} to ON")
def deactivate_entities(client):
"""Désactive toutes les entités en les marquant comme 'unavailable'."""
client.publish(shutdown_entity["availability_topic"], shutdown_entity["payload_not_available"], retain=True)
client.publish(reboot_entity["availability_topic"], reboot_entity["payload_not_available"], retain=True)
client.publish(battery_entity["availability_topic"], battery_entity["payload_not_available"], retain=True)
client.publish(screen_entity["availability_topic"], screen_entity["payload_not_available"], retain=True)
client.publish(cpu_temperature_entity["availability_topic"], cpu_temperature_entity["payload_not_available"], retain=True)
client.publish(cpu_usage_entity["availability_topic"], cpu_usage_entity["payload_not_available"], retain=True)
client.publish(memory_usage_entity["availability_topic"], memory_usage_entity["payload_not_available"], retain=True)
client.publish(cpu_frequency_entity["availability_topic"], cpu_frequency_entity["payload_not_available"], retain=True)
client.publish(charging_status_entity["availability_topic"], charging_status_entity["payload_not_available"], retain=True)
client.publish(ip_address_entity["availability_topic"], ip_address_entity["payload_not_available"], retain=True)
client.publish(cpu_frequency_slider_entity["availability_topic"], cpu_frequency_slider_entity["payload_not_available"], retain=True)
client.loop_stop() # Arrête la boucle MQTT proprement pour s'assurer que tous les messages sont publiés
print("All entities deactivated.")
def on_message(client, userdata, message):
# Gestion des messages MQTT
print("on_message")
# Vérifier si le message est pour le switch "shutdown"
if message.topic == shutdown_entity["command_topic"]:
if message.payload.decode() == shutdown_entity["payload_off"]:
print("Received 'OFF' command - shutting down the system")
client.publish(shutdown_entity["state_topic"], shutdown_entity["payload_off"], retain=True)
# Exécuter la commande de shutdown
time.sleep(1)
deactivate_entities(client) # Désactiver toutes les entités avant de fermer
time.sleep(2)
subprocess.run(["sudo", "shutdown", "-h", "now"])
exit(0) # Sortir immédiatement du programme après le shutdown
elif message.payload.decode() == shutdown_entity["payload_on"]:
print("Received 'ON' command - no action for 'ON'")
elif message.topic == reboot_entity["command_topic"]:
if message.payload.decode() == reboot_entity["payload_off"]:
print("Received 'OFF' command - rebooting the system")
client.publish(reboot_entity["state_topic"], reboot_entity["payload_off"], retain=True)
time.sleep(1)
deactivate_entities(client) # Désactiver toutes les entités avant de redémarrer
subprocess.run(["sudo", "reboot"])
os._exit(0) # Sortir immédiatement du programme après le reboot
elif message.payload.decode() == reboot_entity["payload_on"]:
print("Received 'ON' command - no action for 'ON'")
elif message.topic == screen_entity["command_topic"]:
if message.payload.decode() == screen_entity["payload_off"]:
print("Received 'OFF' command - turning off the screen")
client.publish(screen_entity["state_topic"], screen_entity["payload_off"], retain=True)
result = subprocess.run(["busctl", "--user", "set-property", "org.gnome.Mutter.DisplayConfig", "/org/gnome/Mutter/DisplayConfig", "org.gnome.Mutter.DisplayConfig", "PowerSaveMode", "i", "1"], capture_output=True, text=True)
print(f"Command output: {result.stdout}")
if result.stderr:
print(f"Command error: {result.stderr}")
elif message.payload.decode() == screen_entity["payload_on"]:
print("Received 'ON' command - turning on the screen")
client.publish(screen_entity["state_topic"], screen_entity["payload_on"], retain=True)
result = subprocess.run(["busctl", "--user", "set-property", "org.gnome.Mutter.DisplayConfig", "/org/gnome/Mutter/DisplayConfig", "org.gnome.Mutter.DisplayConfig", "PowerSaveMode", "i", "0"], capture_output=True, text=True)
print(f"Command output: {result.stdout}")
if result.stderr:
print(f"Command error: {result.stderr}")
elif message.topic == cpu_frequency_slider_entity["command_topic"]:
frequency = message.payload.decode()
print(f"Received CPU frequency slider command: {frequency} GHz")
set_cpu_frequency(frequency)
client.publish(cpu_frequency_entity["state_topic"], frequency, retain=True)
client.publish(cpu_frequency_slider_entity["state_topic"], frequency, retain=True)
# Configuration et démarrage du client MQTT
client = mqtt.Client()
client.username_pw_set(mqtt_username, mqtt_password)
client.on_connect = on_connect
client.on_message = on_message
client.connect(mqtt_broker_ip_address, mqtt_port, 60)
client.loop_start()
# Maintenir le script en exécution
try:
while True:
publish_availability(client) # Maintenir l'état disponible
time.sleep(1) # Attendre avant la prochaine mise à jour
except KeyboardInterrupt:
print("Script interrupted, closing MQTT connection")
# Publier l'état "unavailable" pour les entités
deactivate_entities(client)
time.sleep(1)
# Fermer la connexion MQTT proprement
client.disconnect()
+108
View File
@@ -0,0 +1,108 @@
Backup manifest
Timestamp: 20251229_0205
Source: /home/gilles/projects/pilot
Version note: Non trouve
Arborescence:
./analyse.md
./analyse_version_1.md
./.git/config
./.git/description
./.git/HEAD
./.git/hooks/applypatch-msg.sample
./.git/hooks/commit-msg.sample
./.git/hooks/fsmonitor-watchman.sample
./.git/hooks/post-update.sample
./.git/hooks/pre-applypatch.sample
./.git/hooks/pre-commit.sample
./.git/hooks/pre-merge-commit.sample
./.git/hooks/prepare-commit-msg.sample
./.git/hooks/pre-push.sample
./.git/hooks/pre-rebase.sample
./.git/hooks/pre-receive.sample
./.git/hooks/push-to-checkout.sample
./.git/hooks/sendemail-validate.sample
./.git/hooks/update.sample
./.gitignore
./.git/index
./.git/info/exclude
./.git/logs/HEAD
./.git/logs/refs/heads/main
./.git/logs/refs/remotes/origin/HEAD
./.git/objects/1f/879b6f482e6eaf65f3bf14cd7b2e857db0c357
./.git/objects/3f/1d4cf71e3112b318e0138b670e404b6810b2a2
./.git/objects/5d/6ec335b58125b20ba018ef71a629038d13193e
./.git/objects/a0/e84cc209dc6d3c6d85249832d09ff44ed09134
./.git/objects/c5/e4f271728b45440da82d6a2ec8564eb3ce9452
./.git/objects/c8/5502c87a58505f35cbb5b09107da471ccfd0f8
./.git/objects/ed/811e801fed29e2189a0cf95e7253d860c5ffbe
./.git/objects/f2/4d287e7d938f756f6939b187d117940c590330
./.git/objects/pack/pack-bcca462ee5dd238ad61fc02f0b68fdf0f8a40e47.idx
./.git/objects/pack/pack-bcca462ee5dd238ad61fc02f0b68fdf0f8a40e47.pack
./.git/objects/pack/pack-bcca462ee5dd238ad61fc02f0b68fdf0f8a40e47.rev
./.git/packed-refs
./.git/refs/heads/main
./.git/refs/remotes/origin/HEAD
./install.sh
./main-lenovo-bureau.py
./main_prog.py
./main.py
./manifest.txt
./mqtt_pilot.service
./mqtt_unvai.py
./prompt_codex_v_2.md
./prompt_method_redev.md
./README.md
./requirements.txt
SHA256:
18d09692cf81f4ffed570b0f025ce87a809106fa8039114bf225135188db2ec2 ./analyse.md
8970cd0087f6b30ddad80514c9db8ebc2a40a199aeaaafd347c531875849074c ./analyse_version_1.md
0e83cf83a6a8166adf565435bf037b67318639e8dc9fd379b5267bd210332f6b ./.git/config
85ab6c163d43a17ea9cf7788308bca1466f1b0a8d1cc92e26e9bf63da4062aee ./.git/description
28d25bf82af4c0e2b72f50959b2beb859e3e60b9630a5e8c603dad4ddb2b6e80 ./.git/HEAD
0223497a0b8b033aa58a3a521b8629869386cf7ab0e2f101963d328aa62193f7 ./.git/hooks/applypatch-msg.sample
1f74d5e9292979b573ebd59741d46cb93ff391acdd083d340b94370753d92437 ./.git/hooks/commit-msg.sample
e0549964e93897b519bd8e333c037e51fff0f88ba13e086a331592bf801fa1d0 ./.git/hooks/fsmonitor-watchman.sample
81765af2daef323061dcbc5e61fc16481cb74b3bac9ad8a174b186523586f6c5 ./.git/hooks/post-update.sample
e15c5b469ea3e0a695bea6f2c82bcf8e62821074939ddd85b77e0007ff165475 ./.git/hooks/pre-applypatch.sample
57185b7b9f05239d7ab52db045f5b89eb31348d7b2177eab214f5eb872e1971b ./.git/hooks/pre-commit.sample
d3825a70337940ebbd0a5c072984e13245920cdf8898bd225c8d27a6dfc9cb53 ./.git/hooks/pre-merge-commit.sample
e9ddcaa4189fddd25ed97fc8c789eca7b6ca16390b2392ae3276f0c8e1aa4619 ./.git/hooks/prepare-commit-msg.sample
ecce9c7e04d3f5dd9d8ada81753dd1d549a9634b26770042b58dda00217d086a ./.git/hooks/pre-push.sample
4febce867790052338076f4e66cc47efb14879d18097d1d61c8261859eaaa7b3 ./.git/hooks/pre-rebase.sample
a4c3d2b9c7bb3fd8d1441c31bd4ee71a595d66b44fcf49ddb310252320169989 ./.git/hooks/pre-receive.sample
a53d0741798b287c6dd7afa64aee473f305e65d3f49463bb9d7408ec3b12bf5f ./.git/hooks/push-to-checkout.sample
44ebfc923dc5466bc009602f0ecf067b9c65459abfe8868ddc49b78e6ced7a92 ./.git/hooks/sendemail-validate.sample
8d5f2fa83e103cf08b57eaa67521df9194f45cbdbcb37da52ad586097a14d106 ./.git/hooks/update.sample
9cfc3f4a29dca37bd189d1066f3cf3ff715b2b1f38200e0e88cf766e153e3f8c ./.gitignore
c7758fa326bbf97311e67ff1ad18d3dfe7e1c93125f4585a17a68e69a3c2c8f7 ./.git/index
6671fe83b7a07c8932ee89164d1f2793b2318058eb8b98dc5c06ee0a5a3b0ec1 ./.git/info/exclude
ac7327ceb621d8d30347a7d98d44d3c7e3599564cd0f0bd0278a0e214b082ccf ./.git/logs/HEAD
ac7327ceb621d8d30347a7d98d44d3c7e3599564cd0f0bd0278a0e214b082ccf ./.git/logs/refs/heads/main
ac7327ceb621d8d30347a7d98d44d3c7e3599564cd0f0bd0278a0e214b082ccf ./.git/logs/refs/remotes/origin/HEAD
7b0af6e4eb041a50bc5e1998b9c47a874774eecd83b01aa66587893be847893f ./.git/objects/1f/879b6f482e6eaf65f3bf14cd7b2e857db0c357
6e926b948d9d4545be66aed1a672934e42c6e3a27e4474a77604220866c01d7f ./.git/objects/3f/1d4cf71e3112b318e0138b670e404b6810b2a2
1462461eca3f962987177b7d9485bac983d9be192b37431fb4ecfd8109d7fb69 ./.git/objects/5d/6ec335b58125b20ba018ef71a629038d13193e
3ec86ba9e7ef79dfe286cda67e2c31f3c91d5844350689375920059fe82a0ecc ./.git/objects/a0/e84cc209dc6d3c6d85249832d09ff44ed09134
883a4358c41667852e76afa874998a6a3e6d8b1fe78e01cb63bfe32584ef3a32 ./.git/objects/c5/e4f271728b45440da82d6a2ec8564eb3ce9452
3bbe90643732be3c41517a0182ca86d5780b16ea2331131f1934528ce9ccf158 ./.git/objects/c8/5502c87a58505f35cbb5b09107da471ccfd0f8
91692f8ca6b05f5f0bcf35dab188eb9509b9e2c9da28de834c4f9920b4cf4a6d ./.git/objects/ed/811e801fed29e2189a0cf95e7253d860c5ffbe
37569e5ce5100aab5ad92c175691092c99d77fe8f29be95e8ad808623a76d1ce ./.git/objects/f2/4d287e7d938f756f6939b187d117940c590330
13d6ddd5b76ebd3f7a1231ab643b6e6ff289a945f08a5418edb2ecaa0d290ddd ./.git/objects/pack/pack-bcca462ee5dd238ad61fc02f0b68fdf0f8a40e47.idx
de3008c3214f87f6c0ba8423fbc61cb9433c7a0b9e83b5e5dba954f6fe86081f ./.git/objects/pack/pack-bcca462ee5dd238ad61fc02f0b68fdf0f8a40e47.pack
bf3edf37a570c0e6f13e02d257d25b6d51de1969f60b7c433bc19625dd158fab ./.git/objects/pack/pack-bcca462ee5dd238ad61fc02f0b68fdf0f8a40e47.rev
4895bec2d2419bcca9fc9aeeb500d6b1eb6f303ac5c3ce54c3b1858a7ce6e3ec ./.git/packed-refs
27ef7b7ffec74ee504492a3a221bfe070550f5d93ae873d3d46c78a56c49fb60 ./.git/refs/heads/main
2bb6a24aa0fc6c484100f5d51a29bbad841cd2c755f5d93faa204e5dbb4eb2b4 ./.git/refs/remotes/origin/HEAD
dbf44a49752fbf1de6786c24480be06551b5035a74407f435bc56d09faba73ae ./install.sh
fc3ebed3eef0437d3c022f939b88ccdd852d170bb3a39779bd66c4ff7e745d51 ./main-lenovo-bureau.py
b17af44d1f4fcabde0cfd1fea5576abe96636a22fab3d2520035704bf00883db ./main_prog.py
50a314415c33ef577d160b5eb94c26b777458812036c68afb239465d2f696210 ./main.py
9379c86ff6c45c69eaca286e3be47697e8811d754ab4df0dd3acf561e021f472 ./manifest.txt
8b5a9be81ba60973ee0be55e4496c817ff39c2f0cd00c2d3d726ae6bc2c94140 ./mqtt_pilot.service
ba11d01fbf25a24e8f636ab2df5466a49bf7e59b422dce0f9b04861ac1ae75b3 ./mqtt_unvai.py
2ef347d3e390eed581760006d79b05ab7416dc23199f53f26c4a304b7759e34e ./prompt_codex_v_2.md
b2148d6708ba826a554a042de1a80c71984f5ec56bf890caa976ab88d359a045 ./prompt_method_redev.md
e328a510addc0a73abeb6aaa02c7dcd7ea4d844d8e0f52723add9dbd4c259d4a ./README.md
0f92f4d5d7abd57556690c76940f118fc1b024084a1a319f7d7eb87a0b011c19 ./requirements.txt
@@ -0,0 +1,19 @@
[Unit]
Description=MQTT Pilot Service
After=network.target
# Ajouter un délai après le démarrage du réseau
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=gilles
# Délai avant de démarrer le service
ExecStartPre=/bin/sleep 30
ExecStart=/home/gilles/pilot/monenv/bin/python3 /home/gilles/pilot/main_prog.py
Restart=on-failure
RestartSec=30
ExecStopPost=/home/gilles/pilot/monenv/bin/python3 /home/gilles/pilot/mqtt_unvai.py
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
+234
View File
@@ -0,0 +1,234 @@
import paho.mqtt.client as mqtt
import socket
hostname = socket.gethostname()
# Paramètres MQTT
mqtt_broker_ip_address = "10.0.0.3"
mqtt_port = 1883
mqtt_username = ""
mqtt_password = ""
discovery_prefix = "homeassistant"
device_name = "yoga14"
mac_address = "60:57:18:99:ed:05" # Ajout du mac_address manquant
device_info = {
"identifiers": [device_name],
"name": "Yoga 14",
"manufacturer": "Lenovo",
"model": "laptop",
"sw_version": "1.0.0",
"suggested_area": "salon",
}
# Configuration des entités
shutdown_entity = {
"name": f"shutdown_{device_name}",
"type": "switch",
"unique_id": f"shutdown_{device_name}_{mac_address}",
"command_topic": f"pilot/{device_name}/shutdown/set",
"state_topic": f"pilot/{device_name}/shutdown/state",
"availability_topic": f"pilot/{device_name}/shutdown/available",
"device_class": "switch",
"payload_on": "ON",
"payload_off": "OFF",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:power",
"device": device_info,
}
reboot_entity = {
"name": f"reboot_{device_name}",
"type": "switch",
"unique_id": f"reboot_{device_name}_{mac_address}",
"command_topic": f"pilot/{device_name}/reboot/set",
"state_topic": f"pilot/{device_name}/reboot/state",
"availability_topic": f"pilot/{device_name}/reboot/available",
"device_class": "switch",
"payload_on": "ON",
"payload_off": "OFF",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:restart",
"device": device_info,
}
battery_entity = {
"name": f"battery_{device_name}",
"type": "sensor",
"unique_id": f"battery_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/battery/state",
"unit_of_measurement": "%",
"device_class": "battery",
"availability_topic": f"pilot/{device_name}/battery/available",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:battery",
"device": device_info,
"update_interval": 60
}
charging_status_entity = {
"name": f"charging_status_{device_name}",
"type": "binary_sensor",
"unique_id": f"charging_status_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/charging_status/state",
"availability_topic": f"pilot/{device_name}/charging_status/available",
"device_class": "battery_charging",
"payload_on": "ON",
"payload_off": "OFF",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:battery-charging",
"device": device_info,
"update_interval": 5
}
screen_entity = {
"name": f"screen_{device_name}",
"type": "switch",
"unique_id": f"screen_{device_name}_{mac_address}",
"command_topic": f"pilot/{device_name}/screen/set",
"state_topic": f"pilot/{device_name}/screen/state",
"availability_topic": f"pilot/{device_name}/screen/available",
"device_class": "switch",
"payload_on": "ON",
"payload_off": "OFF",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:monitor",
"device": device_info,
}
# Ajout des nouvelles entités pour le CPU et la mémoire
cpu_temperature_entity = {
"name": f"cpu_temperature_{device_name}",
"type": "sensor",
"unique_id": f"cpu_temperature_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/cpu_temperature/state",
"unit_of_measurement": "°C",
"device_class": "temperature",
"availability_topic": f"pilot/{device_name}/cpu_temperature/available",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:thermometer",
"device": device_info,
"update_interval": 20
}
cpu_usage_entity = {
"name": f"cpu_usage_{device_name}",
"type": "sensor",
"unique_id": f"cpu_usage_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/cpu_usage/state",
"unit_of_measurement": "%",
"device_class": "power",
"availability_topic": f"pilot/{device_name}/cpu_usage/available",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:chip",
"device": device_info,
"update_interval": 10
}
memory_usage_entity = {
"name": f"memory_usage_{device_name}",
"type": "sensor",
"unique_id": f"memory_usage_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/memory_usage/state",
"unit_of_measurement": "%",
# "device_class": "memory",
"availability_topic": f"pilot/{device_name}/memory_usage/available",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:memory",
"device": device_info,
"update_interval": 10
}
cpu_frequency_entity = {
"name": f"cpu_frequency_{device_name}",
"type": "sensor",
"unique_id": f"cpu_frequency_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/cpu_frequency/state",
"unit_of_measurement": "GHz",
"device_class": "frequency",
"availability_topic": f"pilot/{device_name}/cpu_frequency/available",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:speedometer",
"device": device_info,
"update_interval": 20
}
ip_address_entity = {
"name": f"ip_address_{device_name}",
"type": "sensor",
"unique_id": f"ip_address_{device_name}_{mac_address}",
"state_topic": f"pilot/{device_name}/ip_address/state",
"availability_topic": f"pilot/{device_name}/ip_address/available",
#"device_class": "connectivity",
"payload_available": "online",
"payload_not_available": "offline",
"icon": "mdi:ip",
"device": device_info,
"update_interval": 60
}
cpu_frequency_slider_entity = {
"name": f"cpu_frequency_slider_{device_name}",
"type": "number",
"unique_id": f"cpu_frequency_slider_{device_name}_{mac_address}",
"command_topic": f"pilot/{device_name}/cpu_frequency_slider/set",
"state_topic": f"pilot/{device_name}/cpu_frequency_slider/state",
"availability_topic": f"pilot/{device_name}/cpu_frequency_slider/available",
"payload_available": "online",
"payload_not_available": "offline",
"min": 0.5,
"max": 3.0,
"step": 0.1,
"unit_of_measurement": "GHz",
"icon": "mdi:speedometer",
"device": device_info,
}
def deactivate_entities(client):
"""Désactive toutes les entités en les marquant comme 'unavailable'."""
entities = [
shutdown_entity,
reboot_entity,
battery_entity,
screen_entity,
cpu_temperature_entity,
cpu_usage_entity,
memory_usage_entity,
cpu_frequency_entity,
charging_status_entity,
ip_address_entity,
cpu_frequency_slider_entity
]
for entity in entities:
client.publish(entity["availability_topic"], entity["payload_not_available"], retain=True)
client.loop_stop() # Arrête la boucle MQTT proprement pour s'assurer que tous les messages sont publiés
print("All entities deactivated.")
# Fonction appelée lors de la connexion au broker MQTT
def on_connect(client, userdata, flags, rc):
if rc == 0:
print(f"Connected with result code {rc}")
deactivate_entities(client)
client.disconnect() # Déconnecter après la publication
# Configuration et démarrage du client MQTT
client = mqtt.Client()
client.username_pw_set(mqtt_username, mqtt_password)
client.on_connect = on_connect
client.connect(mqtt_broker_ip_address, mqtt_port, 60)
client.loop_forever()
+244
View File
@@ -0,0 +1,244 @@
# Prompt Codex — PILOT v2 (Rust) : refonte + déploiement Linux/Windows
## 0) Rôle et livrables attendus
Tu es **Codex**, agent de refonte et dindustrialisation. Tu dois :
1. **Analyser** le projet existant (PILOT v1) et produire un état des lieux.
2. Concevoir puis générer **PILOT v2** en **Rust**, avec **deux builds** (Linux et Windows) partageant un **contrat MQTT commun**, mais pouvant utiliser des implémentations différentes.
3. Produire une **documentation complète** : plan, tests, TODO, changelog, notice déploiement et notice dutilisation.
4. Mettre en place un **processus de backup** de la v1 avant modification.
**Sorties obligatoires (fichiers)**
- `docs/analyse_v1.md`
- `docs/architecture_v2.md`
- `docs/planning_v2.md`
- `docs/todo_v2.md`
- `CHANGELOG.md`
- `docs/deploiement.md`
- `docs/utilisation.md`
- `config/config.example.yaml`
- `README.md` (mis à jour)
- (code) `pilot-v2/` avec structure Rust + workspace si nécessaire
---
## 1) Objectifs fonctionnels de lapplication
Lapplication “pilot” expose via **MQTT** (Home Assistant autodiscovery) :
- **Télémetrie PC** : CPU usage, température, fréquence (lecture), RAM, IP, batterie/charge, GPU (si dispo).
- **États système** : `power_state` : `on|off|sleep|hibernate|locked|unknown` (au minimum on/sleep/off à implémenter).
- **Commandes** : shutdown, reboot, sleep (veille) ; écran on/off.
- **Home Assistant** : publication discovery + availability + states.
- **Robustesse** : reconnexion MQTT, LWT, cadence de publish, cache des valeurs.
### Contraintes et décisions
- **Config** : YAML (obligatoire).
- **Sécurité avancée (signature/HMAC)** : **plus tard** → à mettre dans TODO.
- **Écran** : proposer **2 technos** sélectionnables par paramètre (par OS).
- **Deux versions** : Linux et Windows (pas forcément mêmes technos internes), mais **même schéma MQTT**.
- **Technologie choisie** : **Rust** (déploiement binaire, robustesse).
- **Bonnes pratiques Rust** : modules clairs, `clippy`, `fmt`, logs `tracing`, erreurs `thiserror/anyhow`, configuration `serde_yaml`, code async si MQTT async.
---
## 2) Étape 1 — Backup obligatoire de la v1
Avant toute modification :
- Créer un dossier `backup_v1/<YYYYMMDD_HHMM>/` à la racine.
- Copier **lintégralité** du projet v1 dans ce dossier (sans exclure).
- Générer `backup_v1/<...>/manifest.txt` :
- hash (sha256) des fichiers
- liste arborescence
- note de version (si trouvée)
Aucun fichier v1 ne doit être détruit.
---
## 3) Étape 2 — Analyse v1 (à produire dans docs/analyse\_v1.md)
- Décrire : objectifs, techno, scripts, modules, service systemd, dépendances.
- Lister topics MQTT existants, discovery, states, commands.
- Recenser commandes système utilisées (shutdown/reboot/screen/cpu freq).
- Identifier points faibles (sécurité, duplication, config hardcodée, absence LWT).
- Proposer une synthèse “ce que v2 doit corriger”.
---
## 4) Spécification v2 (contrat MQTT + config YAML)
### 4.1 Schéma MQTT (stable)
Définir un schéma **unique** (à documenter dans `docs/architecture_v2.md`) :
- Base topic : `pilot/<device>/...`
- `availability` : online/offline (LWT recommandé)
- `status` : JSON (version, os, uptime, last\_error, backends)
- `capabilities` : JSON (features réellement actives)
- `state/<name>` : états capteurs/états système
- `cmd/<action>/set` : commandes
Décider QoS / retain :
- discovery: retain=true, qos=1
- states: retain configurable (défaut true)
- commands: retain=false
### 4.2 Configuration YAML (obligatoire)
Créer `config/config.example.yaml` et implémenter lecture depuis :
- Linux : `/etc/pilot/config.yaml` puis fallback `./config.yaml`
- Windows : `C:\ProgramData\Pilot\config.yaml` puis fallback `./config.yaml`
Le YAML doit inclure :
- device (name, identifiers)
- mqtt (host, port, creds, base\_topic, discovery\_prefix, client\_id)
- features (telemetry, commands)
- power backend (linux\_logind\_polkit | linux\_sudoers | windows\_service)
- screen backend (Linux: gnome\_busctl | x11\_xset ; Windows: winapi\_session | external\_tool)
- publish cadence (heartbeat, intervals)
- cooldown commandes
---
## 5) Architecture code Rust (séparation Linux/Windows)
Créer un dossier `pilot-v2/` (Rust) avec cette structure minimale :
```
pilot-v2/
Cargo.toml
src/
main.rs
lib.rs
config/
mqtt/
ha/
telemetry/
commands/
platform/
linux/
windows/
runtime/
security/
docs/ (optionnel si tu préfères à la racine)
```
### Principes dimplémentation
- Définir des `trait` internes (PowerControl, ScreenControl, TelemetryProvider).
- `platform/linux/*` implémente Linux.
- `platform/windows/*` implémente Windows.
- Factory sélectionne backends selon YAML et publie `capabilities`.
- Gestion erreurs : `thiserror` + `anyhow` (ou uniquement `anyhow`).
- Logs : `tracing` (niveau configurable).
- Format : `cargo fmt`, qualité : `cargo clippy` sans warnings.
### Dépendances Rust recommandées (à valider par Codex)
- MQTT : `rumqttc`
- YAML : `serde`, `serde_yaml`
- CLI : `clap` (optionnel mais recommandé)
- Logs : `tracing`, `tracing-subscriber`
- Erreurs : `anyhow`, `thiserror`
- Temps/scheduling : `tokio` (si async), sinon scheduler maison
---
## 6) Déploiement (Linux + Windows)
### Linux
- Produire un binaire `pilot`.
- Fournir un service systemd `pilot.service` (user dédié `pilot` recommandé).
- Support backend power via logind/polkit (recommandé) OU sudoers (fallback).
- Documenter installation, configuration, démarrage, logs (`journalctl -u pilot`).
### Windows
- Produire un binaire `pilot.exe`.
- Déploiement minimal : exécutable + config + tâche planifiée OU service Windows.
- Pour écran : prévoir limitation “service vs session”, et proposer backend `winapi_session` ou `external_tool`.
---
## 7) Planning + Tests
### 7.1 Planning (docs/planning\_v2.md)
Inclure un plan par phases :
1. Backup + analyse v1
2. Spéc contract MQTT + YAML
3. Squelette Rust + MQTT connect + LWT + status/capabilities
4. Telemetry de base (cpu/mem/net)
5. Commandes power (shutdown/reboot/sleep)
6. Écran (2 backends par OS)
7. Home Assistant discovery
8. Packaging + services (systemd / windows)
9. Tests + release
### 7.2 Tests (à implémenter au minimum)
- Unit tests : parsing YAML + validation
- Unit tests : parsing commandes MQTT payload + cooldown
- “Dry-run mode” : exécute sans faire shutdown/reboot, mais log laction
- Test manuel documenté : checklist HA (entités visibles, commandes ok)
---
## 8) TODO + CHANGELOG
- `docs/todo_v2.md` : liste items P0/P1/P2
- inclure : sécurité avancée (HMAC), TLS, ACL, signature
- inclure : amélioration power\_state (hibernate/locked), GPU multi-vendors
- `CHANGELOG.md` : format Keep a Changelog, version 2.0.0 initiale
---
## 9) Notices à produire
### docs/deploiement.md
- Pré-requis
- Installation Linux (binaire, config, service)
- Installation Windows (binaire, config, tâche/service)
- Upgrade / rollback (revenir v1 via dossier backup)
- Debug (logs, topics MQTT, health)
### docs/utilisation.md
- Entités HA attendues
- Commandes (shutdown/reboot/sleep/screen)
- Power state (interprétation)
- Paramètres YAML courants
- Dépannage rapide
---
## 10) Exigences de qualité
- Ne pas casser v1 : tout nouveau code dans `pilot-v2/`.
- Le contrat MQTT doit être **documenté** et stable.
- Lagent doit publier `status` + `capabilities`.
- Lagent doit fonctionner même si certaines features sont indisponibles (désactiver et expliquer via logs/status).
- Pas de promesses “Windows complet” si non implémenté : publier capabilities réelles.
---
## 11) Première action à exécuter maintenant
1. Générer le backup v1
2. Générer `docs/analyse_v1.md`
3. Proposer larborescence Rust + `config/config.example.yaml`
4. Proposer le contrat MQTT final (dans `docs/architecture_v2.md`) Ensuite seulement : commencer limplémentation.
Fin.
@@ -0,0 +1,76 @@
# Methode de re-developpement / re-deploiement (V2)
## 1) Pre-requis
- Python 3.10+ (ou version a valider), paho-mqtt, psutil, optionnel pynvml.
- Repo structuree : `src/`, `configs/`, `docs/`, `scripts/`, `tests/`.
- Conventions : configuration centralisee (env/yaml), logs JSON, topics versionnes.
## 2) Liste des informations que lutilisateur doit completer
Section : A completer par lutilisateur
- OS cibles : [ ]
- Broker MQTT (host:port) : [ ]
- Auth/TLS (user/pass, CA, client cert) : [ ]
- Client_id et keepalive : [ ]
- Prefix topics (ex: pilot/v2) : [ ]
- Liste commandes a supporter : [ ]
- Politique de securite (allowlist, validation, ACL broker) : [ ]
- Packaging desire (exe Windows, systemd Linux, docker) : [ ]
- Integration Home Assistant autodiscovery : [oui/non] + details : [ ]
- Availability / birth / will : [ ]
- Frequence telemetrie (par capteur) : [ ]
- Metriques a exposer (CPU, RAM, GPU, IP, batterie, etc.) : [ ]
- Politique de logs (format, rotation) : [ ]
## 3) Plan iteratif (prompts)
Prompt 1 :
"Genere larchitecture cible + conventions. Cree `docs/architecture.md` et `docs/topics.md`. Liste modules, flux, schemas de payload, et conventions de config."
- Fichiers : `docs/architecture.md`, `docs/topics.md`
- Checklist : schema topics, flux MQTT, decisions securite
- Sortie attendue : docs completes
Prompt 2 :
"Implemente le noyau MQTT + config + logs. Cree `src/app.py`, `src/config.py`, `src/logging.py`. Ajoute `configs/example.env`."
- Fichiers : `src/app.py`, `src/config.py`, `src/logging.py`, `configs/example.env`
- Checklist : connexion MQTT, LWT, logs JSON, chargement config
- Sortie attendue : demarrage local + doc config
Prompt 3 :
"Implemente le moteur de commandes avec allowlist. Cree `src/commands/` et `docs/commands.md`."
- Fichiers : `src/commands/__init__.py`, `src/commands/linux.py`, `docs/commands.md`
- Checklist : validation payload, allowlist, dry-run option
- Sortie attendue : commandes securisees + doc
Prompt 4 :
"Ajoute availability/LWT + heartbeat + etats. Etends `src/app.py` et `docs/topics.md`."
- Fichiers : `src/app.py`, `docs/topics.md`
- Checklist : LWT, availability topic, cadence heartbeat
- Sortie attendue : etats en retained
Prompt 5 :
"Ajoute autodiscovery Home Assistant (si demande). Cree `src/hass.py` et `docs/hass.md`."
- Fichiers : `src/hass.py`, `docs/hass.md`
- Checklist : discovery payloads, device_info, tests de base
- Sortie attendue : entites visibles dans HA
Prompt 6 :
"Packaging Windows (service/tache). Cree `docs/deploy_windows.md` et scripts."
- Fichiers : `docs/deploy_windows.md`, `scripts/install_windows.ps1`
- Checklist : lancement automatique, permissions, rollback
- Sortie attendue : procedure Windows claire
Prompt 7 :
"Packaging Linux (systemd). Cree `docs/deploy_linux.md`, `scripts/install_linux.sh`, `packaging/mqtt_pilot.service`."
- Fichiers : `docs/deploy_linux.md`, `scripts/install_linux.sh`, `packaging/mqtt_pilot.service`
- Checklist : user/service, restart policy, logs journald
- Sortie attendue : service systemd fonctionnel
Prompt 8 :
"Tests + harness MQTT + doc + exemples. Cree `tests/`, `scripts/mqtt_harness.py`, `docs/examples.md`."
- Fichiers : `tests/test_topics.py`, `scripts/mqtt_harness.py`, `docs/examples.md`
- Checklist : tests unitaires, exemples payload, validation topics
- Sortie attendue : test suite + exemples
## 4) Procedure de re-deploiement
- Strategie : nouveau dossier (V2) + migration progressive.
- Migration config : convertir valeurs hardcodees vers env/yaml.
- Compatibilite topics : versionner `pilot/v2` et garder mapping legacy.
- Rollback : conserver ancien service et bascule via systemd.
+3
View File
@@ -0,0 +1,3 @@
paho-mqtt==1.5.1
psutil==5.9.8
pynvml==11.5.0