Initial commit: System2MQTT implementation

This commit is contained in:
Christian Busch 2025-05-22 07:56:25 +02:00
commit dc1d276df6
10 changed files with 997 additions and 0 deletions

40
.gitignore vendored Normal file
View 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
View 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
View 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)

View 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)

View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
paho-mqtt==1.6.1
psutil==5.9.5
PyYAML==6.0.1

73
run.sh Executable file
View 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
View 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()