From 4878866adac1fd89b964570e6cad402d82e48e80 Mon Sep 17 00:00:00 2001 From: Christian Baer Date: Fri, 23 May 2025 18:03:13 +0200 Subject: [PATCH] Converted to PyPy package --- .gitignore | 15 +- MANIFEST.in | 6 + cleanup_mqtt.py | 88 --------- pyproject.toml | 20 ++ requirements.txt | 11 +- run.sh | 73 ------- setup.py | 43 ++++ system2mqtt.py => src/system2mqtt/__init__.py | 0 src/system2mqtt/collectors/__init__.py | 20 ++ .../collectors}/cpu_temperature.py | 0 .../system2mqtt/collectors}/system_metrics.py | 0 .../system2mqtt/collectors}/zfs_pools.py | 0 main.py => src/system2mqtt/main.py | 185 +++++++++++------- 13 files changed, 218 insertions(+), 243 deletions(-) create mode 100644 MANIFEST.in delete mode 100755 cleanup_mqtt.py create mode 100644 pyproject.toml delete mode 100755 run.sh create mode 100755 setup.py rename system2mqtt.py => src/system2mqtt/__init__.py (100%) create mode 100644 src/system2mqtt/collectors/__init__.py rename {collectors => src/system2mqtt/collectors}/cpu_temperature.py (100%) rename {collectors => src/system2mqtt/collectors}/system_metrics.py (100%) rename {collectors => src/system2mqtt/collectors}/zfs_pools.py (100%) rename main.py => src/system2mqtt/main.py (53%) diff --git a/.gitignore b/.gitignore index 937446a..ff0c30a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,6 @@ wheels/ *.egg # Virtual Environment -.venv/ venv/ env/ ENV/ @@ -32,9 +31,15 @@ ENV/ *.swp *.swo -# OS -.DS_Store -Thumbs.db +# Testing +.coverage +htmlcov/ +.pytest_cache/ -# Project specific +# Distribution +dist/ +build/ +*.egg-info/ + +# Local configuration config.yaml \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..66103f7 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include LICENSE +include README.md +include requirements.txt +include config.yaml.example +recursive-include src/system2mqtt *.py +recursive-include src/system2mqtt/collectors *.py \ No newline at end of file diff --git a/cleanup_mqtt.py b/cleanup_mqtt.py deleted file mode 100755 index 115630b..0000000 --- a/cleanup_mqtt.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/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) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2e9e834 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 100 +target-version = ['py38'] +include = '\.pyi?$' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 100 + +[tool.mypy] +python_version = "3.8" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2a7f93f..655369f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,8 @@ -paho-mqtt==1.6.1 -psutil==5.9.5 -PyYAML==6.0.1 \ No newline at end of file +paho-mqtt>=2.0.0 +psutil>=5.9.0 +pyyaml>=6.0 +black>=23.0.0 +isort>=5.12.0 +mypy>=1.0.0 +pytest>=7.0.0 +pytest-cov>=4.0.0 \ No newline at end of file diff --git a/run.sh b/run.sh deleted file mode 100755 index 9973240..0000000 --- a/run.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/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 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..523a963 --- /dev/null +++ b/setup.py @@ -0,0 +1,43 @@ +from setuptools import setup, find_packages + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +setup( + name="system2mqtt", + version="0.1.0", + author="Christian Busch", + author_email="hello@chbus.ch", + description="A system for monitoring hosts by collecting metrics and sending them to Home Assistant via MQTT", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://git.debilux.org/chris/system2mqtt", + package_dir={"": "src"}, + packages=find_packages(where="src"), + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Operating System :: POSIX :: BSD :: FreeBSD", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: System :: Monitoring", + "Topic :: System :: Systems Administration", + ], + python_requires=">=3.8", + install_requires=[ + "paho-mqtt>=2.0.0", + "psutil>=5.9.0", + "pyyaml>=6.0", + ], + entry_points={ + "console_scripts": [ + "system2mqtt=system2mqtt.main:main", + ], + }, + include_package_data=True, +) \ No newline at end of file diff --git a/system2mqtt.py b/src/system2mqtt/__init__.py similarity index 100% rename from system2mqtt.py rename to src/system2mqtt/__init__.py diff --git a/src/system2mqtt/collectors/__init__.py b/src/system2mqtt/collectors/__init__.py new file mode 100644 index 0000000..be30156 --- /dev/null +++ b/src/system2mqtt/collectors/__init__.py @@ -0,0 +1,20 @@ +""" +Collectors package for system2mqtt. +Contains various collectors for different system metrics. +""" + +from .base_collector import BaseCollector +from .cpu_collector import CPUCollector +from .memory_collector import MemoryCollector +from .disk_collector import DiskCollector +from .network_collector import NetworkCollector +from .zfs_collector import ZFSCollector + +__all__ = [ + 'BaseCollector', + 'CPUCollector', + 'MemoryCollector', + 'DiskCollector', + 'NetworkCollector', + 'ZFSCollector', +] \ No newline at end of file diff --git a/collectors/cpu_temperature.py b/src/system2mqtt/collectors/cpu_temperature.py similarity index 100% rename from collectors/cpu_temperature.py rename to src/system2mqtt/collectors/cpu_temperature.py diff --git a/collectors/system_metrics.py b/src/system2mqtt/collectors/system_metrics.py similarity index 100% rename from collectors/system_metrics.py rename to src/system2mqtt/collectors/system_metrics.py diff --git a/collectors/zfs_pools.py b/src/system2mqtt/collectors/zfs_pools.py similarity index 100% rename from collectors/zfs_pools.py rename to src/system2mqtt/collectors/zfs_pools.py diff --git a/main.py b/src/system2mqtt/main.py similarity index 53% rename from main.py rename to src/system2mqtt/main.py index 50c7215..b8ea1a3 100644 --- a/main.py +++ b/src/system2mqtt/main.py @@ -10,83 +10,120 @@ import importlib import paho.mqtt.client as mqtt from typing import Dict, Any, List import yaml +from pathlib import Path + +def get_config_path(): + """Get the path to the config file. + + Returns: + Path: Path to the config file + """ + # First check if config file is specified via environment variable + if config_path := os.environ.get('SYSTEM2MQTT_CONFIG'): + return Path(config_path) + + # Then check user's config directory + if sys.platform == 'win32': + config_dir = Path(os.environ['APPDATA']) / 'system2mqtt' + else: + config_dir = Path.home() / '.config' / 'system2mqtt' + + config_file = config_dir / 'config.yaml' + + # Create config directory if it doesn't exist + config_dir.mkdir(parents=True, exist_ok=True) + + # If config file doesn't exist, copy example config + if not config_file.exists(): + example_config = Path(__file__).parent.parent.parent / 'config.yaml.example' + if example_config.exists(): + import shutil + shutil.copy(example_config, config_file) + print(f"Created config file at {config_file}") + print("Please edit the config file and restart the application.") + sys.exit(1) + else: + print("Error: Neither config.yaml nor config.yaml.example found!") + sys.exit(1) + + return config_file # 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.""" + config_path = get_config_path() + with open(config_path, '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...") + # Create MQTT client client = mqtt.Client(client_id=MQTT_CLIENT_ID) client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)