Initial commit: System2MQTT implementation
This commit is contained in:
commit
dc1d276df6
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual Environment
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
config.yaml
|
126
README.md
Normal file
126
README.md
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# system2mqtt
|
||||||
|
|
||||||
|
Ein System zur Überwachung von Hosts durch Sammeln von Metriken und Senden via MQTT an Home Assistant.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Modulare Struktur für Metrik-Sammler
|
||||||
|
- Einfache Erweiterbarkeit durch neue Sammler
|
||||||
|
- Automatische Erkennung in Home Assistant
|
||||||
|
- Verschlüsselte MQTT-Kommunikation
|
||||||
|
- Detaillierte Geräteinformationen in Home Assistant
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Repository klonen:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/system2mqtt.git
|
||||||
|
cd system2mqtt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Python-Abhängigkeiten installieren:
|
||||||
|
```bash
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Konfiguration anpassen:
|
||||||
|
```yaml
|
||||||
|
mqtt:
|
||||||
|
host: "mqtt.example.com" # MQTT Broker Adresse
|
||||||
|
port: 1883 # MQTT Port
|
||||||
|
username: "your_username"
|
||||||
|
password: "your_password"
|
||||||
|
client_id: "system2mqtt"
|
||||||
|
discovery_prefix: "homeassistant" # Home Assistant Discovery Prefix
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
|
||||||
|
Das System wird über das `run.sh` Skript gesteuert:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Starten des Systems
|
||||||
|
./run.sh start
|
||||||
|
|
||||||
|
# Stoppen des Systems
|
||||||
|
./run.sh stop
|
||||||
|
|
||||||
|
# MQTT Topics aufräumen
|
||||||
|
./run.sh cleanup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sammler
|
||||||
|
|
||||||
|
### System Metrics
|
||||||
|
|
||||||
|
Sammelt grundlegende Systemmetriken:
|
||||||
|
- Last Boot Zeit
|
||||||
|
- Load Average (1, 5, 15 Minuten)
|
||||||
|
- Speichernutzung (Gesamt, Verfügbar, Verwendet)
|
||||||
|
- Swap-Nutzung (Gesamt, Verfügbar, Verwendet)
|
||||||
|
- CPU-Auslastung
|
||||||
|
- Speicherauslastung
|
||||||
|
- Swap-Auslastung
|
||||||
|
|
||||||
|
### CPU Temperature
|
||||||
|
|
||||||
|
Sammelt CPU-Temperaturdaten:
|
||||||
|
- Unterstützt Linux und FreeBSD
|
||||||
|
- Automatische Erkennung des Betriebssystems
|
||||||
|
- Korrekte Einheit (°C) und Device Class (temperature)
|
||||||
|
|
||||||
|
## Datenformat
|
||||||
|
|
||||||
|
Das Datenaustauschformat ist versioniert und folgt den Home Assistant Spezifikationen. Jeder Collector gibt ein JSON-Objekt zurück mit folgender Struktur:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"sensor_id": "unique_sensor_id",
|
||||||
|
"name": "Sensor Name",
|
||||||
|
"value": "Sensor Value",
|
||||||
|
"state_class": "measurement|total|total_increasing",
|
||||||
|
"unit_of_measurement": "Unit",
|
||||||
|
"device_class": "temperature|humidity|pressure|...",
|
||||||
|
"icon": "mdi:icon-name",
|
||||||
|
"entity_category": "diagnostic|config|system",
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "Friendly Name",
|
||||||
|
"source": "Data Source",
|
||||||
|
"additional_info": "Additional Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Felder
|
||||||
|
|
||||||
|
- `sensor_id`: Eindeutige ID für den Sensor (wird für MQTT-Topics verwendet)
|
||||||
|
- `name`: Anzeigename des Sensors
|
||||||
|
- `value`: Aktueller Wert des Sensors
|
||||||
|
- `state_class`: Art der Messung (measurement, total, total_increasing)
|
||||||
|
- `unit_of_measurement`: Einheit der Messung
|
||||||
|
- `device_class`: Art des Sensors (temperature, humidity, pressure, etc.)
|
||||||
|
- `icon`: Material Design Icon Name (mdi:...)
|
||||||
|
- `entity_category`: Kategorie des Sensors (diagnostic, config, system)
|
||||||
|
- `attributes`: Zusätzliche Informationen als Key-Value-Paare
|
||||||
|
|
||||||
|
### Versionierung
|
||||||
|
|
||||||
|
Das Format ist versioniert, um zukünftige Erweiterungen zu ermöglichen. Die aktuelle Version ist 1.0.
|
||||||
|
|
||||||
|
## Home Assistant Integration
|
||||||
|
|
||||||
|
Das System nutzt die MQTT Discovery-Funktion von Home Assistant. Die Sensoren werden automatisch erkannt und erscheinen in Home Assistant mit:
|
||||||
|
- Korrektem Namen und Icon
|
||||||
|
- Aktuellen Werten
|
||||||
|
- Historischen Daten
|
||||||
|
- Detaillierten Geräteinformationen
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
MIT License
|
88
cleanup_mqtt.py
Executable file
88
cleanup_mqtt.py
Executable file
@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import time
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
"""Load configuration from YAML file."""
|
||||||
|
try:
|
||||||
|
with open('config.yaml', 'r') as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def on_connect(client, userdata, flags, rc):
|
||||||
|
"""Callback for when the client connects to the MQTT broker."""
|
||||||
|
if rc == 0:
|
||||||
|
print("Connected to MQTT broker")
|
||||||
|
else:
|
||||||
|
print(f"Failed to connect, return code {rc}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def on_message(client, userdata, message):
|
||||||
|
"""Callback for when a message is received."""
|
||||||
|
userdata.append(message.topic)
|
||||||
|
|
||||||
|
def get_topics(client, timeout=1):
|
||||||
|
"""Get all system2mqtt topics from the MQTT broker."""
|
||||||
|
topics = []
|
||||||
|
client.user_data_set(topics)
|
||||||
|
client.on_message = on_message
|
||||||
|
|
||||||
|
# Subscribe to all topics
|
||||||
|
client.subscribe('#')
|
||||||
|
client.loop_start()
|
||||||
|
|
||||||
|
# Wait for messages
|
||||||
|
time.sleep(timeout)
|
||||||
|
|
||||||
|
client.loop_stop()
|
||||||
|
client.unsubscribe('#')
|
||||||
|
|
||||||
|
# Filter for system2mqtt topics
|
||||||
|
return [topic for topic in topics if 'system2mqtt' in topic]
|
||||||
|
|
||||||
|
def cleanup_topics(config):
|
||||||
|
"""Clean up MQTT topics."""
|
||||||
|
# Create MQTT client
|
||||||
|
client = mqtt.Client()
|
||||||
|
client.username_pw_set(config['mqtt']['username'], config['mqtt']['password'])
|
||||||
|
client.on_connect = on_connect
|
||||||
|
|
||||||
|
# Connect to MQTT broker
|
||||||
|
try:
|
||||||
|
print(f"Connecting to MQTT broker at {config['mqtt']['host']}:{config['mqtt']['port']}...")
|
||||||
|
client.connect(config['mqtt']['host'], config['mqtt']['port'], 60)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to connect to MQTT broker: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Get all system2mqtt topics
|
||||||
|
print("Finding system2mqtt topics...")
|
||||||
|
topics = get_topics(client)
|
||||||
|
|
||||||
|
if not topics:
|
||||||
|
print("No system2mqtt topics found")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Delete each topic
|
||||||
|
for topic in topics:
|
||||||
|
print(f"Deleting topic: {topic}")
|
||||||
|
client.publish(topic, "", retain=True)
|
||||||
|
time.sleep(0.1) # Small delay between deletions
|
||||||
|
|
||||||
|
# Wait for messages to be published
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Stop the MQTT client loop and disconnect
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
print("Cleanup completed")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
config = load_config()
|
||||||
|
cleanup_topics(config)
|
121
collectors/cpu_temperature.py
Normal file
121
collectors/cpu_temperature.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import glob
|
||||||
|
from typing import Dict, Any, Optional, List, Tuple
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def get_temperature_linux_coretemp() -> List[Tuple[float, str]]:
|
||||||
|
"""Get CPU temperatures using coretemp module."""
|
||||||
|
temps = []
|
||||||
|
try:
|
||||||
|
for hwmon_dir in glob.glob('/sys/class/hwmon/hwmon*'):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(hwmon_dir, 'name'), 'r') as f:
|
||||||
|
if f.read().strip() == 'coretemp':
|
||||||
|
# Found coretemp, get all temperatures
|
||||||
|
for temp_file in glob.glob(os.path.join(hwmon_dir, 'temp*_input')):
|
||||||
|
try:
|
||||||
|
with open(temp_file, 'r') as tf:
|
||||||
|
temp = float(tf.read().strip()) / 1000.0
|
||||||
|
# Get label if available
|
||||||
|
label = "Package"
|
||||||
|
label_file = temp_file.replace('_input', '_label')
|
||||||
|
if os.path.exists(label_file):
|
||||||
|
with open(label_file, 'r') as lf:
|
||||||
|
label = lf.read().strip()
|
||||||
|
temps.append((temp, label))
|
||||||
|
except (FileNotFoundError, ValueError):
|
||||||
|
continue
|
||||||
|
except (FileNotFoundError, ValueError):
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return temps
|
||||||
|
|
||||||
|
def get_temperature_linux_thermal() -> List[Tuple[float, str]]:
|
||||||
|
"""Get CPU temperatures using thermal zones."""
|
||||||
|
temps = []
|
||||||
|
try:
|
||||||
|
for thermal_dir in glob.glob('/sys/class/thermal/thermal_zone*'):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(thermal_dir, 'type'), 'r') as f:
|
||||||
|
zone_type = f.read().strip()
|
||||||
|
if 'cpu' in zone_type.lower():
|
||||||
|
with open(os.path.join(thermal_dir, 'temp'), 'r') as tf:
|
||||||
|
temp = float(tf.read().strip()) / 1000.0
|
||||||
|
temps.append((temp, zone_type))
|
||||||
|
except (FileNotFoundError, ValueError):
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return temps
|
||||||
|
|
||||||
|
def get_temperature_freebsd() -> List[Tuple[float, str]]:
|
||||||
|
"""Get CPU temperatures on FreeBSD systems."""
|
||||||
|
temps = []
|
||||||
|
try:
|
||||||
|
# Get number of CPUs
|
||||||
|
cpu_count = int(subprocess.check_output(['sysctl', '-n', 'hw.ncpu']).decode().strip())
|
||||||
|
|
||||||
|
# Get temperature for each CPU
|
||||||
|
for cpu in range(cpu_count):
|
||||||
|
try:
|
||||||
|
temp = subprocess.check_output(['sysctl', '-n', f'dev.cpu.{cpu}.temperature']).decode().strip()
|
||||||
|
temp_value = float(temp)
|
||||||
|
temps.append((temp_value, f'CPU {cpu}'))
|
||||||
|
except (subprocess.SubprocessError, ValueError):
|
||||||
|
continue
|
||||||
|
except (subprocess.SubprocessError, ValueError):
|
||||||
|
pass
|
||||||
|
return temps
|
||||||
|
|
||||||
|
def collect_metrics() -> Dict[str, Any]:
|
||||||
|
"""Collect CPU temperature metrics."""
|
||||||
|
metrics = {
|
||||||
|
"entities": []
|
||||||
|
}
|
||||||
|
|
||||||
|
temps = []
|
||||||
|
|
||||||
|
# Get CPU temperatures based on OS
|
||||||
|
if sys.platform.startswith('linux'):
|
||||||
|
# Try coretemp first (most reliable)
|
||||||
|
temps.extend(get_temperature_linux_coretemp())
|
||||||
|
|
||||||
|
# If no coretemp found, try thermal zones
|
||||||
|
if not temps:
|
||||||
|
temps.extend(get_temperature_linux_thermal())
|
||||||
|
|
||||||
|
elif sys.platform.startswith('freebsd'):
|
||||||
|
temps.extend(get_temperature_freebsd())
|
||||||
|
|
||||||
|
# Add temperature sensors
|
||||||
|
if temps:
|
||||||
|
# Only keep package temperatures
|
||||||
|
package_temps = [(t, l) for t, l in temps if 'Package' in l]
|
||||||
|
|
||||||
|
# Add package temperature
|
||||||
|
for temp, label in package_temps:
|
||||||
|
metrics['entities'].append({
|
||||||
|
'sensor_id': 'cpu_temperature',
|
||||||
|
'name': 'CPU Temperature',
|
||||||
|
'value': str(round(temp, 1)),
|
||||||
|
'state_class': 'measurement',
|
||||||
|
'unit_of_measurement': '°C',
|
||||||
|
'device_class': 'temperature',
|
||||||
|
'icon': 'mdi:thermometer',
|
||||||
|
'attributes': {
|
||||||
|
'friendly_name': 'CPU Temperature',
|
||||||
|
'source': 'coretemp'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Example usage
|
||||||
|
metrics = collect_metrics()
|
||||||
|
print(metrics)
|
162
collectors/system_metrics.py
Normal file
162
collectors/system_metrics.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
def collect_metrics() -> Dict[str, Any]:
|
||||||
|
"""Collect system metrics and return them in the required format."""
|
||||||
|
# Get system metrics
|
||||||
|
boot_time = datetime.fromtimestamp(psutil.boot_time())
|
||||||
|
load_avg = psutil.getloadavg()
|
||||||
|
memory = psutil.virtual_memory()
|
||||||
|
swap = psutil.swap_memory()
|
||||||
|
cpu_percent = psutil.cpu_percent(interval=1)
|
||||||
|
|
||||||
|
# Convert bytes to GB
|
||||||
|
def to_gb(bytes_value: int) -> float:
|
||||||
|
return round(bytes_value / (1024**3), 2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"version": "1.0",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"name": "Last Boot",
|
||||||
|
"sensor_id": "last_boot",
|
||||||
|
"state_class": "total",
|
||||||
|
"device_class": "timestamp",
|
||||||
|
"unit_of_measurement": "",
|
||||||
|
"value": boot_time.astimezone().isoformat(),
|
||||||
|
"icon": "mdi:clock-time-four",
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "Last Boot Time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Load Average (15m)",
|
||||||
|
"sensor_id": "load_15m",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "",
|
||||||
|
"device_class": "power_factor",
|
||||||
|
"value": str(round(load_avg[2], 1)),
|
||||||
|
"icon": "mdi:gauge",
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "System Load (15m)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Load Average (5m)",
|
||||||
|
"sensor_id": "load_5m",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "",
|
||||||
|
"device_class": "power_factor",
|
||||||
|
"value": str(round(load_avg[1], 1)),
|
||||||
|
"icon": "mdi:gauge",
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "System Load (5m)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Load Average (1m)",
|
||||||
|
"sensor_id": "load_1m",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "",
|
||||||
|
"device_class": "power_factor",
|
||||||
|
"value": str(round(load_avg[0], 1)),
|
||||||
|
"icon": "mdi:gauge",
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "System Load (1m)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Memory Free",
|
||||||
|
"sensor_id": "memory_free",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "GB",
|
||||||
|
"device_class": "data_size",
|
||||||
|
"value": str(to_gb(memory.available)),
|
||||||
|
"icon": "mdi:memory",
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "Available Memory"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Memory Used",
|
||||||
|
"sensor_id": "memory_used",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "GB",
|
||||||
|
"device_class": "data_size",
|
||||||
|
"value": str(to_gb(memory.used)),
|
||||||
|
"icon": "mdi:memory",
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "Used Memory"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Memory Usage",
|
||||||
|
"sensor_id": "memory_usage",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "%",
|
||||||
|
"device_class": "power_factor",
|
||||||
|
"value": str(memory.percent),
|
||||||
|
"icon": "mdi:memory",
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "Memory Usage"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CPU Usage",
|
||||||
|
"sensor_id": "cpu_usage",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "%",
|
||||||
|
"device_class": "power_factor",
|
||||||
|
"value": str(cpu_percent),
|
||||||
|
"icon": "mdi:cpu-64-bit",
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "CPU Usage"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Swap Free",
|
||||||
|
"sensor_id": "swap_free",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "GB",
|
||||||
|
"device_class": "data_size",
|
||||||
|
"value": str(to_gb(swap.free)),
|
||||||
|
"icon": "mdi:harddisk",
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "Free Swap"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Swap Used",
|
||||||
|
"sensor_id": "swap_used",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "GB",
|
||||||
|
"device_class": "data_size",
|
||||||
|
"value": str(to_gb(swap.used)),
|
||||||
|
"icon": "mdi:harddisk",
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "Used Swap"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Swap Usage",
|
||||||
|
"sensor_id": "swap_usage",
|
||||||
|
"state_class": "measurement",
|
||||||
|
"unit_of_measurement": "%",
|
||||||
|
"device_class": "power_factor",
|
||||||
|
"value": str(swap.percent),
|
||||||
|
"icon": "mdi:harddisk",
|
||||||
|
"attributes": {
|
||||||
|
"friendly_name": "Swap Usage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Example usage
|
||||||
|
metrics = collect_metrics()
|
||||||
|
print(metrics)
|
8
config.yaml.example
Normal file
8
config.yaml.example
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
mqtt:
|
||||||
|
host: "mqtt.example.com"
|
||||||
|
port: 1883
|
||||||
|
username: "system2mqtt"
|
||||||
|
password: "your_secure_password"
|
||||||
|
client_id: "system2mqtt_{hostname}"
|
||||||
|
discovery_prefix: "homeassistant"
|
||||||
|
state_prefix: "system2mqtt"
|
184
main.py
Normal file
184
main.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
import platform
|
||||||
|
import importlib
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
# Load configuration
|
||||||
|
with open('config.yaml', 'r') as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
# MQTT Configuration
|
||||||
|
MQTT_HOST = config['mqtt']['host']
|
||||||
|
MQTT_PORT = config['mqtt']['port']
|
||||||
|
MQTT_USERNAME = config['mqtt']['username']
|
||||||
|
MQTT_PASSWORD = config['mqtt']['password']
|
||||||
|
MQTT_CLIENT_ID = config['mqtt']['client_id']
|
||||||
|
MQTT_DISCOVERY_PREFIX = config['mqtt']['discovery_prefix']
|
||||||
|
|
||||||
|
# Get hostname
|
||||||
|
HOSTNAME = socket.gethostname()
|
||||||
|
|
||||||
|
# Get OS information
|
||||||
|
OS_INFO = {
|
||||||
|
'system': platform.system(),
|
||||||
|
'release': platform.release(),
|
||||||
|
'version': platform.version(),
|
||||||
|
'machine': platform.machine(),
|
||||||
|
'processor': platform.processor()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_collectors() -> List[str]:
|
||||||
|
"""Get list of available collector scripts."""
|
||||||
|
collectors = []
|
||||||
|
for file in os.listdir('collectors'):
|
||||||
|
if file.endswith('.py') and not file.startswith('__'):
|
||||||
|
collectors.append(file[:-3])
|
||||||
|
return collectors
|
||||||
|
|
||||||
|
def load_collector(collector_name: str) -> Any:
|
||||||
|
"""Load a collector module."""
|
||||||
|
try:
|
||||||
|
return importlib.import_module(f'collectors.{collector_name}')
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"Error loading collector {collector_name}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_device_info() -> Dict[str, Any]:
|
||||||
|
"""Get device information for Home Assistant."""
|
||||||
|
return {
|
||||||
|
"identifiers": [f"system2mqtt_{HOSTNAME}"],
|
||||||
|
"name": f"System Metrics - {HOSTNAME}",
|
||||||
|
"model": OS_INFO['system'],
|
||||||
|
"manufacturer": "system2mqtt",
|
||||||
|
"sw_version": OS_INFO['version'],
|
||||||
|
"configuration_url": f"http://{HOSTNAME}",
|
||||||
|
"hw_version": OS_INFO['machine']
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_device_attributes() -> Dict[str, Any]:
|
||||||
|
"""Get additional device attributes."""
|
||||||
|
return {
|
||||||
|
"operating_system": OS_INFO['system'],
|
||||||
|
"os_release": OS_INFO['release'],
|
||||||
|
"os_version": OS_INFO['version'],
|
||||||
|
"architecture": OS_INFO['machine'],
|
||||||
|
"processor": OS_INFO['processor']
|
||||||
|
}
|
||||||
|
|
||||||
|
def on_connect(client: mqtt.Client, userdata: Any, flags: Dict[str, Any], rc: int) -> None:
|
||||||
|
"""Callback for when the client connects to the MQTT broker."""
|
||||||
|
print(f"Connected with result code {rc}")
|
||||||
|
# Subscribe to any topics if needed
|
||||||
|
# client.subscribe("$SYS/#")
|
||||||
|
|
||||||
|
def on_disconnect(client: mqtt.Client, userdata: Any, rc: int) -> None:
|
||||||
|
"""Callback for when the client disconnects from the MQTT broker."""
|
||||||
|
print(f"Disconnected with result code {rc}")
|
||||||
|
if rc != 0:
|
||||||
|
print("Unexpected disconnection. Attempting to reconnect...")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function."""
|
||||||
|
# Create MQTT client
|
||||||
|
client = mqtt.Client(client_id=MQTT_CLIENT_ID)
|
||||||
|
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
|
||||||
|
client.on_connect = on_connect
|
||||||
|
client.on_disconnect = on_disconnect
|
||||||
|
|
||||||
|
# Connect to MQTT broker
|
||||||
|
try:
|
||||||
|
client.connect(MQTT_HOST, MQTT_PORT, 60)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to connect to MQTT broker: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Start the MQTT client loop
|
||||||
|
client.loop_start()
|
||||||
|
|
||||||
|
# Get list of collectors
|
||||||
|
collectors = get_collectors()
|
||||||
|
print(f"Found collectors: {collectors}")
|
||||||
|
|
||||||
|
# Main loop
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Load and run each collector
|
||||||
|
for collector_name in collectors:
|
||||||
|
collector = load_collector(collector_name)
|
||||||
|
if collector:
|
||||||
|
try:
|
||||||
|
# Get metrics from collector
|
||||||
|
metrics = collector.collect_metrics()
|
||||||
|
|
||||||
|
# Add device info to each entity
|
||||||
|
device_info = get_device_info()
|
||||||
|
device_attributes = get_device_attributes()
|
||||||
|
|
||||||
|
for entity in metrics.get('entities', []):
|
||||||
|
# Create discovery topic
|
||||||
|
discovery_topic = f"{MQTT_DISCOVERY_PREFIX}/sensor/system2mqtt_{HOSTNAME}_{entity['sensor_id']}/config"
|
||||||
|
|
||||||
|
# Create discovery payload
|
||||||
|
discovery_payload = {
|
||||||
|
"name": entity['name'],
|
||||||
|
"unique_id": f"system2mqtt_{HOSTNAME}_{entity['sensor_id']}",
|
||||||
|
"state_topic": f"system2mqtt/{HOSTNAME}/{entity['sensor_id']}/state",
|
||||||
|
"state_class": entity['state_class'],
|
||||||
|
"unit_of_measurement": entity['unit_of_measurement'],
|
||||||
|
"device_class": entity['device_class'],
|
||||||
|
"icon": entity.get('icon'),
|
||||||
|
"device": device_info,
|
||||||
|
"availability": {
|
||||||
|
"topic": f"system2mqtt/{HOSTNAME}/status",
|
||||||
|
"payload_available": "online",
|
||||||
|
"payload_not_available": "offline"
|
||||||
|
},
|
||||||
|
"json_attributes_topic": f"system2mqtt/{HOSTNAME}/{entity['sensor_id']}/attributes"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Publish discovery message
|
||||||
|
client.publish(discovery_topic, json.dumps(discovery_payload), retain=True)
|
||||||
|
|
||||||
|
# Publish state
|
||||||
|
state_topic = f"system2mqtt/{HOSTNAME}/{entity['sensor_id']}/state"
|
||||||
|
client.publish(state_topic, entity['value'], retain=True)
|
||||||
|
|
||||||
|
# Publish attributes
|
||||||
|
attributes = entity.get('attributes', {})
|
||||||
|
attributes.update(device_attributes) # Add device attributes
|
||||||
|
attributes_topic = f"system2mqtt/{HOSTNAME}/{entity['sensor_id']}/attributes"
|
||||||
|
client.publish(attributes_topic, json.dumps(attributes), retain=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error collecting metrics from {collector_name}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Publish availability status
|
||||||
|
client.publish(f"system2mqtt/{HOSTNAME}/status", "online", retain=True)
|
||||||
|
|
||||||
|
# Wait before next collection
|
||||||
|
time.sleep(60) # Collect metrics every minute
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Stopping...")
|
||||||
|
# Publish offline status before stopping
|
||||||
|
client.publish(f"system2mqtt/{HOSTNAME}/status", "offline", retain=True)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in main loop: {e}")
|
||||||
|
time.sleep(60) # Wait before retrying
|
||||||
|
|
||||||
|
# Stop the MQTT client loop and disconnect
|
||||||
|
client.loop_stop()
|
||||||
|
client.disconnect()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
paho-mqtt==1.6.1
|
||||||
|
psutil==5.9.5
|
||||||
|
PyYAML==6.0.1
|
73
run.sh
Executable file
73
run.sh
Executable file
@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Virtual environment directory
|
||||||
|
VENV_DIR=".venv"
|
||||||
|
|
||||||
|
# Function to show usage
|
||||||
|
show_usage() {
|
||||||
|
echo "Usage: $0 [start|stop|cleanup]"
|
||||||
|
echo
|
||||||
|
echo "Commands:"
|
||||||
|
echo " start - Start the system2mqtt service"
|
||||||
|
echo " stop - Stop the system2mqtt service"
|
||||||
|
echo " cleanup - Clean up all system2mqtt MQTT topics"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to setup virtual environment
|
||||||
|
setup_venv() {
|
||||||
|
# Create virtual environment if it doesn't exist
|
||||||
|
if [ ! -d "$VENV_DIR" ]; then
|
||||||
|
echo "Creating virtual environment..."
|
||||||
|
python3 -m venv "$VENV_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate virtual environment
|
||||||
|
source "$VENV_DIR/bin/activate"
|
||||||
|
|
||||||
|
# Install/update dependencies
|
||||||
|
echo "Installing/updating dependencies..."
|
||||||
|
pip install -r requirements.txt
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to start the service
|
||||||
|
start_service() {
|
||||||
|
echo "Starting system2mqtt..."
|
||||||
|
setup_venv
|
||||||
|
python3 main.py
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to stop the service
|
||||||
|
stop_service() {
|
||||||
|
echo "Stopping system2mqtt..."
|
||||||
|
pkill -f "python3 main.py"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to cleanup MQTT topics
|
||||||
|
cleanup_topics() {
|
||||||
|
echo "Running MQTT cleanup..."
|
||||||
|
setup_venv
|
||||||
|
python3 cleanup_mqtt.py
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if a command was provided
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
show_usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Process command
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
start_service
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop_service
|
||||||
|
;;
|
||||||
|
cleanup)
|
||||||
|
cleanup_topics
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
show_usage
|
||||||
|
;;
|
||||||
|
esac
|
192
system2mqtt.py
Normal file
192
system2mqtt.py
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import yaml
|
||||||
|
import platform
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
from typing import Dict, Any
|
||||||
|
import importlib.util
|
||||||
|
import glob
|
||||||
|
|
||||||
|
class System2MQTT:
|
||||||
|
def __init__(self, config_path: str = "config.yaml"):
|
||||||
|
self.config = self._load_config(config_path)
|
||||||
|
self.hostname = socket.gethostname()
|
||||||
|
self.client = self._setup_mqtt_client()
|
||||||
|
self.connected = False
|
||||||
|
self.device_info = self._get_device_info()
|
||||||
|
self.collectors = self._load_collectors()
|
||||||
|
|
||||||
|
def _load_config(self, config_path: str) -> Dict[str, Any]:
|
||||||
|
"""Load configuration from YAML file."""
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def _setup_mqtt_client(self) -> mqtt.Client:
|
||||||
|
"""Setup MQTT client with configuration."""
|
||||||
|
client = mqtt.Client(
|
||||||
|
client_id=self.config['mqtt']['client_id'].format(hostname=self.hostname)
|
||||||
|
)
|
||||||
|
client.username_pw_set(
|
||||||
|
self.config['mqtt']['username'],
|
||||||
|
self.config['mqtt']['password']
|
||||||
|
)
|
||||||
|
client.on_connect = self._on_connect
|
||||||
|
client.on_disconnect = self._on_disconnect
|
||||||
|
return client
|
||||||
|
|
||||||
|
def _on_connect(self, client, userdata, flags, rc):
|
||||||
|
"""Callback for when the client connects to the broker."""
|
||||||
|
if rc == 0:
|
||||||
|
print("Connected to MQTT broker")
|
||||||
|
self.connected = True
|
||||||
|
else:
|
||||||
|
print(f"Failed to connect to MQTT broker with code: {rc}")
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def _on_disconnect(self, client, userdata, rc):
|
||||||
|
"""Callback for when the client disconnects from the broker."""
|
||||||
|
print(f"Disconnected from MQTT broker with code: {rc}")
|
||||||
|
self.connected = False
|
||||||
|
|
||||||
|
def _get_device_info(self) -> Dict[str, Any]:
|
||||||
|
"""Get device information for Home Assistant."""
|
||||||
|
return {
|
||||||
|
"identifiers": [f"system2mqtt_{self.hostname}"],
|
||||||
|
"name": f"System {self.hostname}",
|
||||||
|
"model": platform.machine(),
|
||||||
|
"manufacturer": platform.system()
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_unique_id(self, sensor_id: str) -> str:
|
||||||
|
"""Generate unique_id from sensor_id."""
|
||||||
|
return f"system2mqtt_{self.hostname}_{sensor_id}"
|
||||||
|
|
||||||
|
def _get_state_topic(self, sensor_id: str) -> str:
|
||||||
|
"""Generate state topic from sensor_id."""
|
||||||
|
return f"system2mqtt/{self.hostname}/{sensor_id}/state"
|
||||||
|
|
||||||
|
def _load_collectors(self) -> list:
|
||||||
|
"""Load all collector modules from the collectors directory."""
|
||||||
|
collectors = []
|
||||||
|
collector_dir = os.path.join(os.path.dirname(__file__), 'collectors')
|
||||||
|
|
||||||
|
# Find all Python files in the collectors directory
|
||||||
|
collector_files = glob.glob(os.path.join(collector_dir, '*.py'))
|
||||||
|
|
||||||
|
for collector_file in collector_files:
|
||||||
|
if collector_file.endswith('__init__.py'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
module_name = os.path.splitext(os.path.basename(collector_file))[0]
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, collector_file)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
|
||||||
|
if hasattr(module, 'collect_metrics'):
|
||||||
|
collectors.append(module)
|
||||||
|
print(f"Loaded collector: {module_name}")
|
||||||
|
|
||||||
|
return collectors
|
||||||
|
|
||||||
|
def process_collector_data(self, data: Dict[str, Any]):
|
||||||
|
"""Process data from collectors and publish to MQTT."""
|
||||||
|
if not self.connected:
|
||||||
|
print("Not connected to MQTT broker")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Publish discovery messages for each entity
|
||||||
|
for entity in data['entities']:
|
||||||
|
sensor_id = entity['sensor_id']
|
||||||
|
unique_id = self._get_unique_id(sensor_id)
|
||||||
|
state_topic = self._get_state_topic(sensor_id)
|
||||||
|
discovery_topic = f"{self.config['mqtt']['discovery_prefix']}/sensor/{unique_id}/config"
|
||||||
|
|
||||||
|
# Prepare discovery message
|
||||||
|
discovery_msg = {
|
||||||
|
"name": entity['name'],
|
||||||
|
"unique_id": unique_id,
|
||||||
|
"state_topic": state_topic,
|
||||||
|
"state_class": entity['state_class'],
|
||||||
|
"unit_of_measurement": entity['unit_of_measurement'],
|
||||||
|
"device_class": entity['device_class'],
|
||||||
|
"device": self.device_info
|
||||||
|
}
|
||||||
|
|
||||||
|
# Publish discovery message
|
||||||
|
self.client.publish(
|
||||||
|
discovery_topic,
|
||||||
|
json.dumps(discovery_msg),
|
||||||
|
retain=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Publish state
|
||||||
|
self.client.publish(
|
||||||
|
state_topic,
|
||||||
|
entity['value'],
|
||||||
|
retain=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Publish attributes if present
|
||||||
|
if 'attributes' in entity:
|
||||||
|
attributes_topic = f"{state_topic}/attributes"
|
||||||
|
self.client.publish(
|
||||||
|
attributes_topic,
|
||||||
|
json.dumps(entity['attributes']),
|
||||||
|
retain=True
|
||||||
|
)
|
||||||
|
|
||||||
|
def collect_and_publish(self):
|
||||||
|
"""Collect metrics from all collectors and publish them."""
|
||||||
|
for collector in self.collectors:
|
||||||
|
try:
|
||||||
|
data = collector.collect_metrics()
|
||||||
|
self.process_collector_data(data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error collecting metrics from {collector.__name__}: {e}")
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Connect to MQTT broker."""
|
||||||
|
try:
|
||||||
|
self.client.connect(
|
||||||
|
self.config['mqtt']['broker'],
|
||||||
|
self.config['mqtt']['port']
|
||||||
|
)
|
||||||
|
self.client.loop_start()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error connecting to MQTT broker: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
"""Disconnect from MQTT broker."""
|
||||||
|
self.client.loop_stop()
|
||||||
|
self.client.disconnect()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function."""
|
||||||
|
system2mqtt = System2MQTT()
|
||||||
|
system2mqtt.connect()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Initial collection
|
||||||
|
system2mqtt.collect_and_publish()
|
||||||
|
|
||||||
|
# Keep collecting metrics every 60 seconds
|
||||||
|
while True:
|
||||||
|
time.sleep(60)
|
||||||
|
system2mqtt.collect_and_publish()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nShutting down...")
|
||||||
|
finally:
|
||||||
|
system2mqtt.disconnect()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
x
Reference in New Issue
Block a user