Power Li 1 gadu atpakaļ
revīzija
d109654e70
15 mainītis faili ar 1998 papildinājumiem un 0 dzēšanām
  1. 44 0
      .gitignore
  2. 40 0
      Dockerfile
  3. 21 0
      LICENSE
  4. 102 0
      Makefile
  5. 406 0
      README.md
  6. 106 0
      cmd/ha-gpu-ccd/README.md
  7. 216 0
      cmd/ha-gpu-ccd/main.go
  8. 18 0
      go.mod
  9. 29 0
      go.sum
  10. 272 0
      main.go
  11. 32 0
      nvrm-gpu-ha.conf.example
  12. 148 0
      pkg/config/config.go
  13. 206 0
      pkg/homeassistant/discovery.go
  14. 279 0
      pkg/nvidia/nvidia.go
  15. 79 0
      pkg/nvidia/nvidia_windows.go

+ 44 - 0
.gitignore

@@ -0,0 +1,44 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+
+# IDE files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# Build artifacts
+/nvml-gpu-ha
+/nvml-gpu-ha-*
+*.service
+
+# Logs
+*.log
+
+# Config files (may contain sensitive data)
+config.yaml
+config.yml
+*.env
+nvml-gpu-ha.conf
+*.conf

+ 40 - 0
Dockerfile

@@ -0,0 +1,40 @@
+# Build stage
+FROM golang:1.21-alpine AS builder
+
+# Install build dependencies
+RUN apk add --no-cache gcc musl-dev
+
+WORKDIR /app
+
+# Copy go mod files
+COPY go.mod go.sum ./
+
+# Download dependencies
+RUN go mod download
+
+# Copy source code
+COPY . .
+
+# Build the application
+RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o nvml-gpu-ha .
+
+# Runtime stage
+FROM nvidia/cuda:12.2-base-ubuntu22.04
+
+# Install runtime dependencies
+RUN apt-get update && \
+    apt-get install -y --no-install-recommends \
+    ca-certificates && \
+    rm -rf /var/lib/apt/lists/*
+
+# Copy the binary from builder stage
+COPY --from=builder /app/nvml-gpu-ha /usr/local/bin/nvml-gpu-ha
+
+# Create non-root user
+RUN useradd -r -s /bin/false nvml-gpu
+
+USER nvml-gpu
+
+# Default command
+ENTRYPOINT ["/usr/local/bin/nvml-gpu-ha"]
+CMD ["--mqtt-host=localhost", "--mqtt-port=1883", "--polling-period=30"]

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 pccr10001
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 102 - 0
Makefile

@@ -0,0 +1,102 @@
+# NVML GPU Home Assistant Monitor
+# Build configuration
+
+BINARY_NAME=nvml-gpu-ha
+GO_VERSION=1.21
+LDFLAGS=-s -w
+
+# Default target
+.PHONY: all
+all: build
+
+# Build for current platform
+.PHONY: build
+build:
+	go build -ldflags="$(LDFLAGS)" -o $(BINARY_NAME) .
+
+# Build for Linux (production target)
+.PHONY: build-linux
+build-linux:
+	GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o $(BINARY_NAME)-linux-amd64 .
+
+# Build for Linux ARM64 (for ARM servers)
+.PHONY: build-linux-arm64
+build-linux-arm64:
+	GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o $(BINARY_NAME)-linux-arm64 .
+
+# Build for Windows (development)
+.PHONY: build-windows
+build-windows:
+	GOOS=windows GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o $(BINARY_NAME)-windows-amd64.exe .
+
+# Download dependencies
+.PHONY: deps
+deps:
+	go mod download
+	go mod tidy
+
+# Run tests
+.PHONY: test
+test:
+	go test -v ./...
+
+# Clean build artifacts
+.PHONY: clean
+clean:
+	rm -f $(BINARY_NAME)
+	rm -f $(BINARY_NAME)-*
+	go clean
+
+# Install on current system
+.PHONY: install
+install: build
+	sudo cp $(BINARY_NAME) /usr/local/bin/
+
+# Create systemd service file
+.PHONY: systemd-service
+systemd-service:
+	@echo "Creating systemd service file..."
+	@cat > nvml-gpu-ha.service << EOF
+	[Unit]
+	Description=NVIDIA GPU Monitoring for Home Assistant
+	After=network.target
+	Wants=network.target
+	
+	[Service]
+	Type=simple
+	User=nobody
+	Group=nogroup
+	ExecStart=/usr/local/bin/$(BINARY_NAME)
+	Restart=always
+	RestartSec=10
+	
+	[Install]
+	WantedBy=multi-user.target
+	EOF
+	@echo "Service file created: nvml-gpu-ha.service"
+	@echo "NOTE: Configure settings in /etc/nvml-gpu-ha.conf before starting"
+	@echo "To install: sudo cp nvml-gpu-ha.service /etc/systemd/system/"
+	@echo "To copy config: sudo cp nvml-gpu-ha.conf.example /etc/nvml-gpu-ha.conf"
+	@echo "To enable: sudo systemctl enable nvml-gpu-ha"
+	@echo "To start: sudo systemctl start nvml-gpu-ha"
+
+# Docker build
+.PHONY: docker-build
+docker-build:
+	docker build -t nvml-gpu-ha .
+
+# Help
+.PHONY: help
+help:
+	@echo "Available targets:"
+	@echo "  build           - Build for current platform"
+	@echo "  build-linux     - Build for Linux amd64"
+	@echo "  build-linux-arm64 - Build for Linux arm64"
+	@echo "  build-windows   - Build for Windows amd64"
+	@echo "  deps            - Download and tidy dependencies"
+	@echo "  test            - Run tests"
+	@echo "  clean           - Clean build artifacts"
+	@echo "  install         - Install to /usr/local/bin"
+	@echo "  systemd-service - Create systemd service file"
+	@echo "  docker-build    - Build Docker image"
+	@echo "  help            - Show this help"

+ 406 - 0
README.md

@@ -0,0 +1,406 @@
+# NVML GPU Home Assistant Monitor
+
+A lightweight Go application that monitors NVIDIA GPU metrics using NVML (NVIDIA Management Library) and publishes them to Home Assistant via MQTT with automatic discovery support.
+
+## Features
+
+- **Real-time GPU monitoring** using NVIDIA's official go-nvml library
+- **Home Assistant MQTT Discovery** - sensors automatically appear in HA
+- **Multiple GPU support** - monitors all available NVIDIA GPUs
+- **Configurable polling interval** - adjust monitoring frequency
+- **Request protection** - prevents overlapping NVML requests that can cause slowdowns
+- **Concurrent monitoring** - parallel GPU monitoring for better performance
+- **MQTT LWT (Last Will and Testament)** support for availability tracking
+- **Systemd service** support for automatic startup
+- **Docker support** with NVIDIA Container Runtime
+
+## Monitored Metrics
+
+For each GPU, the following sensors are created in Home Assistant:
+
+- **Power Draw** (Watts) - Current power consumption
+- **Performance Level** (P0/P8/etc.) - Current P-State
+- **VRAM Usage** (%) - Memory utilization percentage  
+- **GPU Utilization** (%) - GPU core usage percentage
+- **GPU Temperature** (°C) - Current GPU temperature
+
+## GPU Naming Convention
+
+GPUs appear in Home Assistant with the format: `{HOSTNAME} {PCI ID} - NVIDIA {MODEL} {VRAM}`
+
+Example: `MY-SERVER 00:01:00.0 - NVIDIA GeForce RTX 3080 10GB`
+
+## Requirements
+
+### System Requirements
+- Linux (Ubuntu, CentOS, etc.) for production use
+- NVIDIA GPU with driver version 440.33 or newer
+- NVIDIA Management Library (libnvidia-ml.so)
+- Uses official [NVIDIA go-nvml](https://github.com/NVIDIA/go-nvml) library
+
+### Software Requirements
+- Go 1.21+ (for building from source)
+- MQTT broker (Mosquitto, Home Assistant built-in, etc.)
+- Home Assistant with MQTT integration
+
+## Installation
+
+### Option 1: Download Pre-built Binary
+
+```bash
+# Download the latest release
+wget https://github.com/pccr10001/nvml-gpu-ha/releases/latest/download/nvml-gpu-ha-linux-amd64
+chmod +x nvml-gpu-ha-linux-amd64
+sudo mv nvml-gpu-ha-linux-amd64 /usr/local/bin/nvml-gpu-ha
+```
+
+### Option 2: Build from Source
+
+```bash
+# Clone the repository
+git clone https://github.com/pccr10001/nvml-gpu-ha.git
+cd nvml-gpu-ha
+
+# Build and install
+make build-linux
+sudo cp nvml-gpu-ha-linux-amd64 /usr/local/bin/nvml-gpu-ha
+```
+
+### Option 3: Docker
+
+```bash
+# Pull the image
+docker pull ghcr.io/pccr10001/nvml-gpu-ha:latest
+
+# Or build locally
+docker build -t nvml-gpu-ha .
+```
+
+## Configuration
+
+### Configuration File (Recommended)
+
+The application reads configuration from `/etc/nvml-gpu-ha.conf` in TOML format. Command line flags can override config file settings.
+
+#### Example Configuration File
+
+```toml
+# /etc/nvml-gpu-ha.conf
+
+# Hostname prefix for GPU names (optional, uses system hostname if not specified)
+hostname = "MY-SERVER"
+
+# MQTT Broker Configuration
+mqtt_host = "192.168.1.100"
+mqtt_port = 1883
+mqtt_username = "homeassistant"
+mqtt_password = "your_secure_password"
+
+# MQTT Options
+mqtt_lwt_enable = true
+mqtt_retain = true
+
+# Monitoring Settings  
+polling_period = 30
+```
+
+#### Create Configuration File
+
+```bash
+# Copy example config
+sudo cp nvml-gpu-ha.conf.example /etc/nvml-gpu-ha.conf
+
+# Edit configuration
+sudo nano /etc/nvml-gpu-ha.conf
+
+# Set appropriate permissions
+sudo chmod 600 /etc/nvml-gpu-ha.conf
+sudo chown root:root /etc/nvml-gpu-ha.conf
+```
+
+### Command Line Options
+
+Command line flags override configuration file settings:
+
+```bash
+nvml-gpu-ha [flags]
+
+Flags:
+  --config string          Configuration file path (default "/etc/nvml-gpu-ha.conf")
+  --hostname string        Hostname prefix for GPU names (default: system hostname)
+  --mqtt-host string       MQTT broker host (default "localhost")
+  --mqtt-port int          MQTT broker port (default 1883)
+  --mqtt-username string   MQTT username
+  --mqtt-password string   MQTT password
+  --mqtt-lwt-enable        Enable MQTT Last Will and Testament (default true)
+  --mqtt-retain            Retain MQTT messages (default true)
+  --polling-period int     GPU polling period in seconds (default 30)
+  -h, --help              help for nvml-gpu-ha
+```
+
+### Example Usage
+
+```bash
+# Using configuration file (recommended)
+nvml-gpu-ha
+
+# Using custom hostname
+nvml-gpu-ha --hostname=MY-SERVER
+
+# Using custom config file location
+nvml-gpu-ha --config=/path/to/custom.conf
+
+# Override config file with command line flags
+nvml-gpu-ha --mqtt-host=192.168.1.200 --polling-period=10
+
+# Pure command line usage (no config file)
+nvml-gpu-ha \
+  --hostname=GAMING-PC \
+  --mqtt-host=192.168.1.100 \
+  --mqtt-port=1883 \
+  --mqtt-username=homeassistant \
+  --mqtt-password=secretpassword \
+  --polling-period=10
+
+# Minimal override  
+nvml-gpu-ha --mqtt-host=mqtt.local --hostname=SERVER-01
+```
+
+### Configuration Priority
+
+Configuration is loaded in the following order (later sources override earlier ones):
+
+1. **Default values** (built-in defaults)
+2. **Configuration file** (`/etc/nvml-gpu-ha.conf` or specified via `--config`)
+3. **Command line flags** (highest priority)
+
+This allows you to set base configuration in a file and override specific values via command line as needed.
+
+## Systemd Service Setup
+
+### Create Service File
+
+```bash
+# Generate service file
+make systemd-service
+
+# Install the service
+sudo cp nvml-gpu-ha.service /etc/systemd/system/
+sudo systemctl daemon-reload
+
+# Enable and start the service
+sudo systemctl enable nvml-gpu-ha
+sudo systemctl start nvml-gpu-ha
+
+# Check status
+sudo systemctl status nvml-gpu-ha
+```
+
+### Manual Service File
+
+Create `/etc/systemd/system/nvml-gpu-ha.service`:
+
+```ini
+[Unit]
+Description=NVIDIA GPU Monitoring for Home Assistant
+After=network.target
+Wants=network.target
+
+[Service]
+Type=simple
+User=nobody
+Group=nogroup
+ExecStart=/usr/local/bin/nvml-gpu-ha
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+```
+
+**Note**: With configuration file support, the service file is much cleaner. All settings are read from `/etc/nvml-gpu-ha.conf` automatically.
+
+## Docker Usage
+
+### Docker Compose
+
+```yaml
+version: '3.8'
+services:
+  nvml-gpu-ha:
+    image: ghcr.io/pccr10001/nvml-gpu-ha:latest
+    container_name: nvml-gpu-ha
+    restart: unless-stopped
+    runtime: nvidia
+    environment:
+      - NVIDIA_VISIBLE_DEVICES=all
+    command: [
+      "--mqtt-host=192.168.1.100",
+      "--mqtt-username=homeassistant", 
+      "--mqtt-password=secretpassword",
+      "--polling-period=30"
+    ]
+    deploy:
+      resources:
+        reservations:
+          devices:
+            - driver: nvidia
+              count: all
+              capabilities: [gpu]
+```
+
+### Docker Run
+
+```bash
+docker run -d \
+  --name nvml-gpu-ha \
+  --restart unless-stopped \
+  --runtime nvidia \
+  -e NVIDIA_VISIBLE_DEVICES=all \
+  ghcr.io/pccr10001/nvml-gpu-ha:latest \
+  --mqtt-host=192.168.1.100 \
+  --mqtt-username=homeassistant \
+  --mqtt-password=secretpassword
+```
+
+## Home Assistant Configuration
+
+### MQTT Integration
+
+Ensure MQTT integration is configured in Home Assistant:
+
+```yaml
+# configuration.yaml
+mqtt:
+  broker: localhost  # Your MQTT broker
+  username: homeassistant
+  password: secretpassword
+  discovery: true    # Enable MQTT Discovery
+```
+
+### Sensor Entities
+
+Once running, sensors will automatically appear in Home Assistant under:
+
+- **Device**: `{HOSTNAME} {PCI ID} - NVIDIA {MODEL} {VRAM}`
+- **Entities**:
+  - `sensor.{pci_id}_nvidia_{model}_{vram}_power_draw`
+  - `sensor.{pci_id}_nvidia_{model}_{vram}_performance_level`
+  - `sensor.{pci_id}_nvidia_{model}_{vram}_vram_usage`
+  - `sensor.{pci_id}_nvidia_{model}_{vram}_gpu_utilization`
+  - `sensor.{pci_id}_nvidia_{model}_{vram}_temperature`
+
+## Development
+
+### Building for Development
+
+```bash
+# Install dependencies
+make deps
+
+# Build for current platform
+make build
+
+# Build for all platforms
+make build-linux
+make build-linux-arm64
+make build-windows
+
+# Run tests
+make test
+```
+
+### Cross-compilation Notes
+
+- **Linux builds**: Include official NVIDIA go-nvml bindings (production)
+- **Windows builds**: Stub implementation for development only
+- **C String Handling**: Properly converts NVIDIA's C-style char arrays to Go strings
+- The application must run on Linux with NVIDIA drivers for full functionality
+
+## Performance & Reliability
+
+### Request Protection
+This version includes several performance improvements:
+
+- **Mutex-based request protection** - Prevents overlapping NVML calls that can cause slowdowns
+- **Timeout protection** - GPU metric requests timeout after 10 seconds to prevent hanging
+- **Concurrent monitoring** - Multiple GPUs are monitored in parallel for faster updates
+- **Smart scheduling** - Skips monitoring cycles if previous requests are still running
+
+### Version Information
+The application displays NVML and driver version information at startup for debugging:
+
+```
+2024/08/18 11:00:00 Hostname: MY-SERVER
+2024/08/18 11:00:00 NVML Version: 12.535.133.00
+2024/08/18 11:00:00 NVIDIA Driver Version: 535.133.00
+2024/08/18 11:00:00 Found 2 NVIDIA GPU(s)
+2024/08/18 11:00:00 GPU 0: GeForce RTX 3080 (00:01:00.0, 10.0GB)
+2024/08/18 11:00:00 GPU 1: GeForce RTX 4090 (00:02:00.0, 24.0GB)
+```
+
+## Troubleshooting
+
+### Common Issues
+
+1. **"Failed to initialize NVML"**
+   - Ensure NVIDIA drivers are installed and up-to-date
+   - Check that `/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1` exists
+   - Run with `nvidia-smi` first to verify GPU access
+
+2. **"No NVIDIA GPUs found"**
+   - Verify GPUs are detected: `nvidia-smi -L`
+   - Check driver compatibility
+   - Ensure process has GPU access permissions
+
+3. **MQTT connection issues**
+   - Verify broker address and credentials
+   - Check firewall settings
+   - Test with mosquitto client tools
+
+4. **Sensors not appearing in Home Assistant**
+   - Ensure MQTT discovery is enabled
+   - Check MQTT broker logs
+   - Verify topic structure in MQTT explorer
+
+### Debug Mode
+
+Enable verbose logging by checking the application logs:
+
+```bash
+# Systemd logs
+sudo journalctl -u nvml-gpu-ha -f
+
+# Docker logs
+docker logs -f nvml-gpu-ha
+```
+
+### Manual Testing
+
+Test MQTT connectivity:
+
+```bash
+# Subscribe to all topics
+mosquitto_sub -h YOUR_MQTT_HOST -u YOUR_USERNAME -P YOUR_PASSWORD -t "homeassistant/sensor/nvml-gpu/+"
+
+# Check discovery topics
+mosquitto_sub -h YOUR_MQTT_HOST -u YOUR_USERNAME -P YOUR_PASSWORD -t "homeassistant/sensor/nvml-gpu/+/config"
+```
+
+## Contributing
+
+1. Fork the repository
+2. Create a feature branch
+3. Make changes and test
+4. Submit a pull request
+
+## License
+
+This project is licensed under the MIT License - see the LICENSE file for details.
+
+## Acknowledgments
+
+- NVIDIA for the NVML API
+- Home Assistant community
+- Eclipse Paho MQTT Go client
+- Cobra CLI framework

+ 106 - 0
cmd/ha-gpu-ccd/README.md

@@ -0,0 +1,106 @@
+# ha-gpu-ccd - Home Assistant GPU CCD Temperature Monitor
+
+A tool that retrieves GPU temperature information from Home Assistant MQTT and writes it to sysfs format files.
+
+## Features
+
+- Listen to Home Assistant MQTT temperature topics (supports multiple subscription modes)
+- Convert temperature from Celsius to millidegrees (e.g., 80.5°C -> 80500)
+- Write temperature to `/tmp/temp_{DEVICEID}` files (sysfs format)
+- Support for monitoring specific devices or all devices
+
+## Build
+
+```bash
+go build
+```
+
+## Usage
+
+```bash
+./ha-gpu-ccd --mqtt-host <MQTT_BROKER> --mqtt-port <PORT> --mqtt-username <USERNAME> --mqtt-password <PASSWORD>
+```
+
+### Parameters
+
+- `--mqtt-host`: MQTT broker host (default: localhost)
+- `--mqtt-port`: MQTT broker port (default: 1883)
+- `--mqtt-username`: MQTT username (optional)
+- `--mqtt-password`: MQTT password (optional)
+- `--temp-dir`: Directory to write temperature files (default: /tmp)
+- `--device-id`: Specific GPU device ID to monitor (leave empty to monitor all devices)
+
+### Examples
+
+```bash
+# Connect to local MQTT broker, monitor all GPUs
+./ha-gpu-ccd
+
+# Connect to remote MQTT broker with authentication
+./ha-gpu-ccd --mqtt-host 192.168.1.100 --mqtt-username homeassistant --mqtt-password mypassword
+
+# Monitor only specific GPU device
+./ha-gpu-ccd --device-id 00_04_00_0
+
+# Custom temperature file directory
+./ha-gpu-ccd --temp-dir /var/lib/gpu-temps
+
+# Combined usage: specific device with authentication
+./ha-gpu-ccd --mqtt-host 192.168.1.100 --mqtt-username user --mqtt-password pass --device-id 00_04_00_0
+```
+
+## Output Format
+
+The tool creates a temperature file for each GPU device:
+- File name: `temp_{DEVICEID}`
+- Content: Temperature value in millidegrees (integer)
+- Location: Default in `/tmp/` directory
+
+Example:
+- GPU device ID: `00_04_00_0`
+- Temperature: 80.5°C
+- File: `/tmp/temp_00_04_00_0`
+- Content: `80500`
+
+## MQTT Topic Format
+
+The tool supports two subscription modes:
+
+### Monitor All Devices (Default Mode)
+Subscribe topic: `homeassistant/sensor/nvml-gpu/#`
+Filter condition: Only process messages containing `_temperature/state`
+
+### Monitor Specific Device
+Subscribe topic: `homeassistant/sensor/nvml-gpu/{DEVICEID}_temperature/state`
+
+Temperature topic format:
+```
+homeassistant/sensor/nvml-gpu/{DEVICEID}_temperature/state
+```
+
+Where `{DEVICEID}` is the device ID generated by the nvml-gpu-ha main program, e.g., `00_04_00_0`
+
+## Logging
+
+The tool outputs the following log messages:
+- Connection status
+- Subscription status
+- Received temperature data
+- File write status
+- Error messages
+
+## Troubleshooting
+
+1. **Connection Failed**: Check if MQTT broker address and port are correct
+2. **Authentication Failed**: Verify username and password are correct
+3. **Subscription Failed**: Ensure MQTT broker supports wildcard subscriptions
+4. **No Data Received**: Check if nvml-gpu-ha main program is running and sending data
+
+## Integration
+
+This tool is designed for integration with other systems, such as:
+- System monitoring software
+- Temperature control systems
+- Automation scripts
+
+By reading the `/tmp/temp_{DEVICEID}` files, other programs can obtain real-time GPU temperature information.

+ 216 - 0
cmd/ha-gpu-ccd/main.go

@@ -0,0 +1,216 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"os"
+	"os/signal"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"syscall"
+	"time"
+
+	mqtt "github.com/eclipse/paho.mqtt.golang"
+	"github.com/spf13/cobra"
+)
+
+var (
+	mqttHost     string
+	mqttPort     int
+	mqttUsername string
+	mqttPassword string
+	tempDir      string
+	deviceID     string
+
+	rootCmd = &cobra.Command{
+		Use:   "ha-gpu-ccd",
+		Short: "Home Assistant GPU CCD Temperature Monitor",
+		Long:  "Monitor GPU temperatures from Home Assistant MQTT and write to sysfs format files for CCD integration",
+		Run:   run,
+	}
+)
+
+func init() {
+	rootCmd.PersistentFlags().StringVar(&mqttHost, "mqtt-host", "localhost", "MQTT broker host")
+	rootCmd.PersistentFlags().IntVar(&mqttPort, "mqtt-port", 1883, "MQTT broker port")
+	rootCmd.PersistentFlags().StringVar(&mqttUsername, "mqtt-username", "", "MQTT username")
+	rootCmd.PersistentFlags().StringVar(&mqttPassword, "mqtt-password", "", "MQTT password")
+	rootCmd.PersistentFlags().StringVar(&tempDir, "temp-dir", "/tmp", "Directory to write temperature files")
+	rootCmd.PersistentFlags().StringVar(&deviceID, "device-id", "", "Specific GPU device ID to monitor (leave empty to monitor all devices)")
+}
+
+func main() {
+	if err := rootCmd.Execute(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func run(cmd *cobra.Command, args []string) {
+	log.Printf("Starting ha-gpu-ccd")
+	log.Printf("MQTT Broker: %s:%d", mqttHost, mqttPort)
+	log.Printf("Temperature directory: %s", tempDir)
+	log.Printf("MQTT Username: %s", func() string {
+		if mqttUsername != "" {
+			return mqttUsername
+		}
+		return "(none)"
+	}())
+
+	// Create temp directory if it doesn't exist
+	if err := os.MkdirAll(tempDir, 0755); err != nil {
+		log.Fatalf("Failed to create temp directory %s: %v", tempDir, err)
+	}
+
+	// Setup MQTT client
+	mqttClient := setupMQTTClient()
+	defer mqttClient.Disconnect(250)
+
+	// Setup graceful shutdown
+	quit := make(chan os.Signal, 1)
+	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+
+	log.Println("Monitoring GPU temperatures from Home Assistant...")
+	log.Println("Press Ctrl+C to stop")
+
+	<-quit
+	log.Println("Shutting down...")
+}
+
+func setupMQTTClient() mqtt.Client {
+	opts := mqtt.NewClientOptions()
+	opts.AddBroker(fmt.Sprintf("tcp://%s:%d", mqttHost, mqttPort))
+	opts.SetClientID("ha-gpu-ccd")
+	opts.SetUsername(mqttUsername)
+	opts.SetPassword(mqttPassword)
+	opts.SetAutoReconnect(true)
+	opts.SetConnectRetry(true)
+	opts.SetConnectRetryInterval(10 * time.Second)
+	opts.SetKeepAlive(60 * time.Second)
+
+	opts.SetOnConnectHandler(func(client mqtt.Client) {
+		log.Println("Connected to MQTT broker")
+
+		// Wait a moment for connection to stabilize
+		go func() {
+			time.Sleep(1 * time.Second)
+
+			// Check if connection is still active
+			if !client.IsConnected() {
+				log.Println("Connection lost during stabilization")
+				return
+			}
+
+			// Subscribe to temperature topics after successful connection
+			if err := subscribeToTemperatureTopics(client); err != nil {
+				log.Printf("Failed to subscribe to temperature topics: %v", err)
+			}
+		}()
+	})
+
+	opts.SetConnectionLostHandler(func(client mqtt.Client, err error) {
+		log.Printf("Connection lost to MQTT broker: %v", err)
+	})
+
+	client := mqtt.NewClient(opts)
+	if token := client.Connect(); token.Wait() && token.Error() != nil {
+		log.Fatalf("Failed to connect to MQTT broker: %v", token.Error())
+	}
+
+	// Wait a bit to ensure connection is stable
+	time.Sleep(2 * time.Second)
+
+	return client
+}
+
+func subscribeToTemperatureTopics(client mqtt.Client) error {
+	var topic string
+
+	if deviceID != "" {
+		// Subscribe to specific device temperature topic
+		topic = fmt.Sprintf("homeassistant/sensor/nvml-gpu/%s_temperature/state", deviceID)
+	} else {
+		// Subscribe to all GPU temperature topics using # wildcard
+		topic = "homeassistant/sensor/nvml-gpu/#"
+	}
+
+	// Wait for subscription with timeout
+	token := client.Subscribe(topic, 1, onTemperatureMessage)
+	if !token.WaitTimeout(10 * time.Second) {
+		return fmt.Errorf("timeout waiting for subscription to %s", topic)
+	}
+
+	if token.Error() != nil {
+		return fmt.Errorf("failed to subscribe to %s: %v", topic, token.Error())
+	}
+
+	log.Printf("Successfully subscribed to: %s", topic)
+	return nil
+}
+
+func onTemperatureMessage(client mqtt.Client, msg mqtt.Message) {
+	topic := msg.Topic()
+	payload := string(msg.Payload())
+
+	// Filter for temperature topics only
+	// Topic format: homeassistant/sensor/nvml-gpu/{DEVICEID}_temperature/state
+	if !strings.Contains(topic, "_temperature/state") {
+		// Ignore non-temperature topics
+		return
+	}
+
+	parts := strings.Split(topic, "/")
+	if len(parts) < 4 {
+		log.Printf("Invalid topic format: %s", topic)
+		return
+	}
+
+	deviceSensor := parts[3] // This should be {DEVICEID}_temperature
+	if !strings.HasSuffix(deviceSensor, "_temperature") {
+		log.Printf("Topic does not end with _temperature: %s", topic)
+		return
+	}
+
+	deviceID := strings.TrimSuffix(deviceSensor, "_temperature")
+
+	// Parse temperature from JSON payload
+	var temperature float64
+	if err := json.Unmarshal([]byte(payload), &temperature); err != nil {
+		log.Printf("Failed to parse temperature from payload '%s': %v", payload, err)
+		return
+	}
+
+	log.Printf("Received temperature for device %s: %.1f°C", deviceID, temperature)
+
+	// Convert temperature to sysfs format (millidegrees)
+	// Example: 80.5°C -> 80500
+	tempMillidegrees := int(temperature * 1000)
+
+	// Write to temp file
+	tempFile := filepath.Join(tempDir, fmt.Sprintf("temp_%s", deviceID))
+	if err := writeTemperatureFile(tempFile, tempMillidegrees); err != nil {
+		log.Printf("Failed to write temperature file %s: %v", tempFile, err)
+		return
+	}
+
+	log.Printf("Updated %s: %d (%.1f°C)", tempFile, tempMillidegrees, temperature)
+}
+
+func writeTemperatureFile(filename string, tempMillidegrees int) error {
+	content := strconv.Itoa(tempMillidegrees)
+
+	// Create or overwrite the file
+	file, err := os.Create(filename)
+	if err != nil {
+		return fmt.Errorf("failed to create file: %v", err)
+	}
+	defer file.Close()
+
+	// Write the temperature value
+	if _, err := file.WriteString(content); err != nil {
+		return fmt.Errorf("failed to write temperature: %v", err)
+	}
+
+	return nil
+}

+ 18 - 0
go.mod

@@ -0,0 +1,18 @@
+module github.com/pccr10001/nvml-gpu-ha
+
+go 1.21
+
+require (
+	github.com/BurntSushi/toml v1.3.2
+	github.com/NVIDIA/go-nvml v0.12.9-0
+	github.com/eclipse/paho.mqtt.golang v1.4.3
+	github.com/spf13/cobra v1.7.0
+)
+
+require (
+	github.com/gorilla/websocket v1.5.0 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	golang.org/x/net v0.10.0 // indirect
+	golang.org/x/sync v0.1.0 // indirect
+)

+ 29 - 0
go.sum

@@ -0,0 +1,29 @@
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/NVIDIA/go-nvml v0.12.9-0 h1:e344UK8ZkeMeeLkdQtRhmXRxNf+u532LDZPGMtkdus0=
+github.com/NVIDIA/go-nvml v0.12.9-0/go.mod h1:+KNA7c7gIBH7SKSJ1ntlwkfN80zdx8ovl4hrK3LmPt4=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
+github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
+github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 272 - 0
main.go

@@ -0,0 +1,272 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log"
+	"os"
+	"os/signal"
+	"sync"
+	"syscall"
+	"time"
+
+	mqtt "github.com/eclipse/paho.mqtt.golang"
+	"github.com/pccr10001/nvml-gpu-ha/pkg/config"
+	"github.com/pccr10001/nvml-gpu-ha/pkg/homeassistant"
+	"github.com/pccr10001/nvml-gpu-ha/pkg/nvidia"
+	"github.com/spf13/cobra"
+)
+
+var (
+	cfg             *config.Config
+	monitoringMutex sync.Mutex
+	isMonitoring    bool
+	lastMonitorTime time.Time
+	rootCmd         = &cobra.Command{
+		Use:   "nvml-gpu-ha",
+		Short: "NVIDIA GPU monitoring for Home Assistant via MQTT",
+		Long:  "Monitor NVIDIA GPU metrics and send them to Home Assistant via MQTT with auto-discovery support",
+		Run:   run,
+	}
+)
+
+func init() {
+	// Command line flags
+	rootCmd.PersistentFlags().String("config", "/etc/nvml-gpu-ha.conf", "Configuration file path")
+	rootCmd.PersistentFlags().String("hostname", "", "Hostname prefix for GPU names (default: system hostname)")
+	rootCmd.PersistentFlags().String("mqtt-host", "localhost", "MQTT broker host")
+	rootCmd.PersistentFlags().Int("mqtt-port", 1883, "MQTT broker port")
+	rootCmd.PersistentFlags().String("mqtt-username", "", "MQTT username")
+	rootCmd.PersistentFlags().String("mqtt-password", "", "MQTT password")
+	rootCmd.PersistentFlags().Bool("mqtt-lwt-enable", true, "Enable MQTT Last Will and Testament")
+	rootCmd.PersistentFlags().Bool("mqtt-retain", true, "Retain MQTT messages")
+	rootCmd.PersistentFlags().Int("polling-period", 30, "GPU polling period in seconds")
+}
+
+func main() {
+	if err := rootCmd.Execute(); err != nil {
+		log.Fatal(err)
+	}
+}
+
+func run(cmd *cobra.Command, args []string) {
+	var err error
+	cfg, err = config.LoadConfig(cmd)
+	if err != nil {
+		log.Fatal("Failed to load configuration:", err)
+	}
+
+	// If hostname is not provided, use system hostname
+	if cfg.Hostname == "" {
+		if hostname, err := os.Hostname(); err == nil {
+			cfg.Hostname = hostname
+		} else {
+			log.Printf("Warning: Failed to get system hostname, using 'localhost': %v", err)
+			cfg.Hostname = "localhost"
+		}
+	}
+
+	// Display configuration source
+	configFile, _ := cmd.Flags().GetString("config")
+	if _, err := os.Stat(configFile); err == nil {
+		log.Printf("Loaded configuration from: %s", configFile)
+	} else {
+		log.Printf("Using default configuration (no config file found at: %s)", configFile)
+	}
+
+	// Display key configuration values (without sensitive data)
+	log.Printf("Hostname: %s", cfg.Hostname)
+	log.Printf("MQTT Broker: %s:%d", cfg.MQTTHost, cfg.MQTTPort)
+	log.Printf("MQTT Username: %s", func() string {
+		if cfg.MQTTUsername != "" {
+			return cfg.MQTTUsername
+		} else {
+			return "(none)"
+		}
+	}())
+	log.Printf("Polling Period: %d seconds", cfg.PollingPeriod)
+	log.Printf("MQTT LWT Enabled: %v", cfg.MQTTLWTEnable)
+	log.Printf("MQTT Retain: %v", cfg.MQTTRetain)
+
+	// Initialize NVIDIA management library
+	if err := nvidia.Init(); err != nil {
+		log.Fatal("Failed to initialize NVIDIA management library:", err)
+	}
+	defer nvidia.Shutdown()
+
+	// Display version information
+	if nvmlVersion, err := nvidia.GetNVMLVersion(); err == nil {
+		log.Printf("NVML Version: %s", nvmlVersion)
+	}
+	if driverVersion, err := nvidia.GetDriverVersion(); err == nil {
+		log.Printf("NVIDIA Driver Version: %s", driverVersion)
+	}
+
+	// Get GPU information
+	gpus, err := nvidia.GetGPUDevices()
+	if err != nil {
+		log.Fatal("Failed to get GPU devices:", err)
+	}
+
+	if len(gpus) == 0 {
+		log.Fatal("No NVIDIA GPUs found")
+	}
+
+	log.Printf("Found %d NVIDIA GPU(s)", len(gpus))
+	for i, gpu := range gpus {
+		shortPCIID := nvidia.GetShortPCIBusID(gpu.PCIBusID)
+		log.Printf("GPU %d: %s (%s, %.1fGB)", i, gpu.Name, shortPCIID, float64(gpu.Memory)/(1024*1024*1024))
+	}
+
+	// Setup MQTT client
+	mqttClient := setupMQTTClient()
+	defer mqttClient.Disconnect(250)
+
+	// Setup Home Assistant discovery
+	haManager := homeassistant.NewManager(mqttClient, cfg)
+
+	// Register all GPU sensors with Home Assistant
+	for _, gpu := range gpus {
+		if err := haManager.RegisterGPUSensors(gpu, cfg.Hostname); err != nil {
+			log.Printf("Failed to register sensors for GPU %s: %v", gpu.Name, err)
+		}
+	}
+
+	// Setup graceful shutdown
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	go func() {
+		sigChan := make(chan os.Signal, 1)
+		signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
+		<-sigChan
+		log.Println("Received shutdown signal, stopping...")
+		cancel()
+	}()
+
+	// Main monitoring loop
+	ticker := time.NewTicker(time.Duration(cfg.PollingPeriod) * time.Second)
+	defer ticker.Stop()
+
+	log.Printf("Starting GPU monitoring loop (polling every %d seconds)", cfg.PollingPeriod)
+
+	for {
+		select {
+		case <-ctx.Done():
+			log.Println("Shutting down...")
+			return
+		case <-ticker.C:
+			monitorGPUs(mqttClient, gpus)
+		}
+	}
+}
+
+func setupMQTTClient() mqtt.Client {
+	opts := mqtt.NewClientOptions()
+	opts.AddBroker(fmt.Sprintf("tcp://%s:%d", cfg.MQTTHost, cfg.MQTTPort))
+	opts.SetClientID("nvml-gpu-ha")
+	opts.SetUsername(cfg.MQTTUsername)
+	opts.SetPassword(cfg.MQTTPassword)
+	opts.SetAutoReconnect(true)
+	opts.SetConnectRetry(true)
+	opts.SetConnectRetryInterval(10 * time.Second)
+
+	if cfg.MQTTLWTEnable {
+		opts.SetWill("homeassistant/sensor/nvml-gpu-ha/availability", "offline", 1, cfg.MQTTRetain)
+	}
+
+	opts.SetOnConnectHandler(func(client mqtt.Client) {
+		log.Println("Connected to MQTT broker")
+		if cfg.MQTTLWTEnable {
+			client.Publish("homeassistant/sensor/nvml-gpu-ha/availability", 1, cfg.MQTTRetain, "online")
+		}
+	})
+
+	opts.SetConnectionLostHandler(func(client mqtt.Client, err error) {
+		log.Printf("Connection lost to MQTT broker: %v", err)
+	})
+
+	client := mqtt.NewClient(opts)
+	if token := client.Connect(); token.Wait() && token.Error() != nil {
+		log.Fatal("Failed to connect to MQTT broker:", token.Error())
+	}
+
+	return client
+}
+
+func monitorGPUs(client mqtt.Client, gpus []nvidia.GPUDevice) {
+	// Prevent overlapping monitoring requests
+	monitoringMutex.Lock()
+	defer monitoringMutex.Unlock()
+
+	if isMonitoring {
+		log.Printf("Previous monitoring request still in progress, skipping this cycle")
+		return
+	}
+
+	// Check if enough time has passed since last monitoring
+	if time.Since(lastMonitorTime) < time.Duration(cfg.PollingPeriod/2)*time.Second {
+		log.Printf("Too soon since last monitoring, skipping this cycle")
+		return
+	}
+
+	isMonitoring = true
+	defer func() {
+		isMonitoring = false
+		lastMonitorTime = time.Now()
+	}()
+
+	log.Printf("Starting GPU monitoring cycle...")
+	startTime := time.Now()
+
+	var wg sync.WaitGroup
+	for _, gpu := range gpus {
+		wg.Add(1)
+		go func(gpu nvidia.GPUDevice) {
+			defer wg.Done()
+
+			metrics, err := nvidia.GetGPUMetrics(gpu)
+			if err != nil {
+				log.Printf("Failed to get metrics for GPU %s: %v", gpu.Name, err)
+				return
+			}
+
+			publishMetrics(client, gpu, metrics)
+		}(gpu)
+	}
+
+	wg.Wait()
+	duration := time.Since(startTime)
+	log.Printf("GPU monitoring cycle completed in %v", duration)
+}
+
+func publishMetrics(client mqtt.Client, gpu nvidia.GPUDevice, metrics nvidia.GPUMetrics) {
+	// Publish individual sensor values
+	sensors := map[string]interface{}{
+		"power_draw":        metrics.PowerDraw,
+		"performance_level": metrics.PerformanceLevel,
+		"memory_usage":      metrics.MemoryUsage,
+		"gpu_utilization":   metrics.GPUUtilization,
+		"temperature":       metrics.Temperature,
+	}
+
+	deviceID := nvidia.GetDeviceID(gpu)
+
+	for sensor, value := range sensors {
+		topic := fmt.Sprintf("homeassistant/sensor/nvml-gpu/%s_%s/state", deviceID, sensor)
+
+		payload, err := json.Marshal(value)
+		if err != nil {
+			log.Printf("Failed to marshal sensor data for %s: %v", sensor, err)
+			continue
+		}
+
+		token := client.Publish(topic, 1, cfg.MQTTRetain, payload)
+		if !token.WaitTimeout(5*time.Second) || token.Error() != nil {
+			log.Printf("Failed to publish %s data: %v", sensor, token.Error())
+		}
+	}
+
+	log.Printf("Published metrics for GPU: %s", gpu.Name)
+}

+ 32 - 0
nvrm-gpu-ha.conf.example

@@ -0,0 +1,32 @@
+# NVML GPU Home Assistant Monitor Configuration File
+# This is an example configuration file for nvml-gpu-ha
+# Copy this file to /etc/nvml-gpu-ha.conf and modify as needed
+
+# Hostname prefix for GPU names (optional, uses system hostname if not specified)
+# hostname = "my-server"
+
+# MQTT Broker Configuration
+mqtt_host = "localhost"
+mqtt_port = 1883
+mqtt_username = ""
+mqtt_password = ""
+
+# MQTT Options
+mqtt_lwt_enable = true
+mqtt_retain = true
+
+# Monitoring Settings
+polling_period = 30  # Polling period in seconds
+
+# Example with authentication:
+# mqtt_host = "192.168.1.100"
+# mqtt_username = "homeassistant"
+# mqtt_password = "your_secure_password"
+# polling_period = 10
+
+# Example for faster polling:
+# polling_period = 10
+
+# Example with custom MQTT settings:
+# mqtt_lwt_enable = false
+# mqtt_retain = false

+ 148 - 0
pkg/config/config.go

@@ -0,0 +1,148 @@
+package config
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/BurntSushi/toml"
+	"github.com/spf13/cobra"
+)
+
+// Config holds all configuration values
+type Config struct {
+	Hostname      string `toml:"hostname"`
+	MQTTHost      string `toml:"mqtt_host"`
+	MQTTPort      int    `toml:"mqtt_port"`
+	MQTTUsername  string `toml:"mqtt_username"`
+	MQTTPassword  string `toml:"mqtt_password"`
+	MQTTLWTEnable bool   `toml:"mqtt_lwt_enable"`
+	MQTTRetain    bool   `toml:"mqtt_retain"`
+	PollingPeriod int    `toml:"polling_period"`
+}
+
+// DefaultConfig returns a config with default values
+func DefaultConfig() *Config {
+	return &Config{
+		Hostname:      "",
+		MQTTHost:      "localhost",
+		MQTTPort:      1883,
+		MQTTUsername:  "",
+		MQTTPassword:  "",
+		MQTTLWTEnable: true,
+		MQTTRetain:    true,
+		PollingPeriod: 30,
+	}
+}
+
+// LoadConfigFromFile loads configuration from TOML file
+func LoadConfigFromFile(filename string) (*Config, error) {
+	config := DefaultConfig()
+
+	// Check if file exists
+	if _, err := os.Stat(filename); os.IsNotExist(err) {
+		// File doesn't exist, return default config
+		return config, nil
+	}
+
+	// Read and parse TOML file
+	if _, err := toml.DecodeFile(filename, config); err != nil {
+		return nil, fmt.Errorf("failed to parse config file %s: %v", filename, err)
+	}
+
+	return config, nil
+}
+
+// LoadConfig loads configuration from file first, then overrides with command line flags
+func LoadConfig(cmd *cobra.Command) (*Config, error) {
+	// First load from config file
+	configFile := "/etc/nvml-gpu-ha.conf"
+
+	// Allow override of config file path via flag
+	if cmd.Flags().Changed("config") {
+		var err error
+		configFile, err = cmd.Flags().GetString("config")
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	config, err := LoadConfigFromFile(configFile)
+	if err != nil {
+		return nil, err
+	}
+
+	// Override with command line flags if they were explicitly set
+	if cmd.Flags().Changed("hostname") {
+		config.Hostname, err = cmd.Flags().GetString("hostname")
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if cmd.Flags().Changed("mqtt-host") {
+		config.MQTTHost, err = cmd.Flags().GetString("mqtt-host")
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if cmd.Flags().Changed("mqtt-port") {
+		config.MQTTPort, err = cmd.Flags().GetInt("mqtt-port")
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if cmd.Flags().Changed("mqtt-username") {
+		config.MQTTUsername, err = cmd.Flags().GetString("mqtt-username")
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if cmd.Flags().Changed("mqtt-password") {
+		config.MQTTPassword, err = cmd.Flags().GetString("mqtt-password")
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if cmd.Flags().Changed("mqtt-lwt-enable") {
+		config.MQTTLWTEnable, err = cmd.Flags().GetBool("mqtt-lwt-enable")
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if cmd.Flags().Changed("mqtt-retain") {
+		config.MQTTRetain, err = cmd.Flags().GetBool("mqtt-retain")
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	if cmd.Flags().Changed("polling-period") {
+		config.PollingPeriod, err = cmd.Flags().GetInt("polling-period")
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return config, nil
+}
+
+// SaveToFile saves current configuration to a TOML file
+func (c *Config) SaveToFile(filename string) error {
+	file, err := os.Create(filename)
+	if err != nil {
+		return fmt.Errorf("failed to create config file %s: %v", filename, err)
+	}
+	defer file.Close()
+
+	encoder := toml.NewEncoder(file)
+	if err := encoder.Encode(c); err != nil {
+		return fmt.Errorf("failed to encode config to file %s: %v", filename, err)
+	}
+
+	return nil
+}

+ 206 - 0
pkg/homeassistant/discovery.go

@@ -0,0 +1,206 @@
+package homeassistant
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+
+	mqtt "github.com/eclipse/paho.mqtt.golang"
+	"github.com/pccr10001/nvml-gpu-ha/pkg/config"
+	"github.com/pccr10001/nvml-gpu-ha/pkg/nvidia"
+)
+
+// Manager handles Home Assistant MQTT Discovery
+type Manager struct {
+	client mqtt.Client
+	config *config.Config
+}
+
+// SensorConfig represents Home Assistant sensor configuration
+type SensorConfig struct {
+	Name                string      `json:"name"`
+	StateTopic          string      `json:"state_topic"`
+	UniqueID            string      `json:"unique_id"`
+	DeviceClass         string      `json:"device_class,omitempty"`
+	UnitOfMeasurement   string      `json:"unit_of_measurement,omitempty"`
+	Icon                string      `json:"icon,omitempty"`
+	Device              *DeviceInfo `json:"device"`
+	AvailabilityTopic   string      `json:"availability_topic,omitempty"`
+	PayloadAvailable    string      `json:"payload_available,omitempty"`
+	PayloadNotAvailable string      `json:"payload_not_available,omitempty"`
+	ValueTemplate       string      `json:"value_template,omitempty"`
+	StateClass          string      `json:"state_class,omitempty"`
+	ForceUpdate         bool        `json:"force_update,omitempty"`
+}
+
+// DeviceInfo represents device information for Home Assistant
+type DeviceInfo struct {
+	Identifiers  []string `json:"identifiers"`
+	Name         string   `json:"name"`
+	Model        string   `json:"model"`
+	Manufacturer string   `json:"manufacturer"`
+	SwVersion    string   `json:"sw_version,omitempty"`
+}
+
+// NewManager creates a new Home Assistant discovery manager
+func NewManager(client mqtt.Client, config *config.Config) *Manager {
+	return &Manager{
+		client: client,
+		config: config,
+	}
+}
+
+// RegisterGPUSensors registers all sensors for a GPU device
+func (m *Manager) RegisterGPUSensors(device nvidia.GPUDevice, hostname string) error {
+	deviceID := nvidia.GetDeviceID(device)
+	deviceName := nvidia.GetDeviceDisplayName(device, hostname)
+
+	deviceInfo := &DeviceInfo{
+		Identifiers:  []string{deviceID, device.UUID},
+		Name:         deviceName,
+		Model:        device.Name,
+		Manufacturer: "NVIDIA",
+		SwVersion:    "NVML",
+	}
+
+	sensors := []struct {
+		key         string
+		name        string
+		deviceClass string
+		unit        string
+		icon        string
+		stateClass  string
+		template    string
+	}{
+		{
+			key:         "power_draw",
+			name:        "Power Draw",
+			deviceClass: "power",
+			unit:        "W",
+			icon:        "mdi:lightning-bolt",
+			stateClass:  "measurement",
+		},
+		{
+			key:         "performance_level",
+			name:        "Performance Level",
+			deviceClass: "",
+			unit:        "",
+			icon:        "mdi:speedometer",
+			stateClass:  "",
+		},
+		{
+			key:         "memory_usage",
+			name:        "VRAM Usage",
+			deviceClass: "",
+			unit:        "%",
+			icon:        "mdi:memory",
+			stateClass:  "measurement",
+			template:    "{{ value | round(1) }}",
+		},
+		{
+			key:         "gpu_utilization",
+			name:        "GPU Utilization",
+			deviceClass: "",
+			unit:        "%",
+			icon:        "mdi:chip",
+			stateClass:  "measurement",
+		},
+		{
+			key:         "temperature",
+			name:        "GPU Temperature",
+			deviceClass: "temperature",
+			unit:        "°C",
+			icon:        "mdi:thermometer",
+			stateClass:  "measurement",
+		},
+	}
+
+	for _, sensor := range sensors {
+		if err := m.registerSensor(deviceID, deviceName, sensor.key, sensor.name,
+			sensor.deviceClass, sensor.unit, sensor.icon, sensor.stateClass,
+			sensor.template, deviceInfo); err != nil {
+			return fmt.Errorf("failed to register sensor %s: %v", sensor.key, err)
+		}
+	}
+
+	return nil
+}
+
+// registerSensor registers a single sensor with Home Assistant
+func (m *Manager) registerSensor(deviceID, deviceName, sensorKey, sensorName, deviceClass, unit, icon, stateClass, template string, deviceInfo *DeviceInfo) error {
+	uniqueID := fmt.Sprintf("nvml_gpu_%s_%s", deviceID, sensorKey)
+	stateTopic := fmt.Sprintf("homeassistant/sensor/nvml-gpu/%s_%s/state", deviceID, sensorKey)
+	configTopic := fmt.Sprintf("homeassistant/sensor/nvml-gpu/%s_%s/config", deviceID, sensorKey)
+
+	fullSensorName := sensorName
+
+	sensorConfig := SensorConfig{
+		Name:              fullSensorName,
+		StateTopic:        stateTopic,
+		UniqueID:          uniqueID,
+		DeviceClass:       deviceClass,
+		UnitOfMeasurement: unit,
+		Icon:              icon,
+		Device:            deviceInfo,
+		StateClass:        stateClass,
+		ForceUpdate:       true,
+	}
+
+	if template != "" {
+		sensorConfig.ValueTemplate = template
+	}
+
+	// Add availability if LWT is enabled
+	if m.config.MQTTLWTEnable {
+		sensorConfig.AvailabilityTopic = "homeassistant/sensor/nvml-gpu-ha/availability"
+		sensorConfig.PayloadAvailable = "online"
+		sensorConfig.PayloadNotAvailable = "offline"
+	}
+
+	configJSON, err := json.Marshal(sensorConfig)
+	if err != nil {
+		return fmt.Errorf("failed to marshal sensor config: %v", err)
+	}
+
+	token := m.client.Publish(configTopic, 1, m.config.MQTTRetain, configJSON)
+	if !token.WaitTimeout(5*1e9) || token.Error() != nil { // 5 seconds
+		return fmt.Errorf("failed to publish sensor config: %v", token.Error())
+	}
+
+	log.Printf("Registered sensor: %s", fullSensorName)
+	return nil
+}
+
+// RemoveGPUSensors removes all sensors for a GPU device
+func (m *Manager) RemoveGPUSensors(device nvidia.GPUDevice) error {
+	deviceID := nvidia.GetDeviceID(device)
+
+	sensors := []string{"power_draw", "performance_level", "memory_usage", "gpu_utilization", "temperature"}
+
+	for _, sensor := range sensors {
+		configTopic := fmt.Sprintf("homeassistant/sensor/nvml-gpu/%s_%s/config", deviceID, sensor)
+
+		// Send empty payload to remove the sensor
+		token := m.client.Publish(configTopic, 1, m.config.MQTTRetain, "")
+		if !token.WaitTimeout(5*1e9) || token.Error() != nil {
+			log.Printf("Failed to remove sensor %s: %v", sensor, token.Error())
+		}
+	}
+
+	return nil
+}
+
+// PublishAvailability publishes availability status
+func (m *Manager) PublishAvailability(status string) error {
+	if !m.config.MQTTLWTEnable {
+		return nil
+	}
+
+	topic := "homeassistant/sensor/nvml-gpu-ha/availability"
+	token := m.client.Publish(topic, 1, m.config.MQTTRetain, status)
+	if !token.WaitTimeout(5*1e9) || token.Error() != nil {
+		return fmt.Errorf("failed to publish availability: %v", token.Error())
+	}
+
+	return nil
+}

+ 279 - 0
pkg/nvidia/nvidia.go

@@ -0,0 +1,279 @@
+//go:build linux
+// +build linux
+
+package nvidia
+
+import (
+	"fmt"
+	"strings"
+	"sync"
+	"time"
+	"unsafe"
+
+	"github.com/NVIDIA/go-nvml/pkg/nvml"
+)
+
+// requestMutex prevents overlapping NVML requests to avoid slowdowns
+var requestMutex sync.Mutex
+
+// convertCString converts a C-style char array to a Go string
+func convertCString(cstr [32]int8) string {
+	n := 0
+	for i := 0; i < len(cstr); i++ {
+		if cstr[i] == 0 {
+			break
+		}
+		n++
+	}
+	return string((*[32]byte)(unsafe.Pointer(&cstr[0]))[:n])
+}
+
+// GPUDevice represents an NVIDIA GPU device
+type GPUDevice struct {
+	Index    int
+	Handle   nvml.Device
+	Name     string
+	PCIBusID string
+	Memory   uint64 // Total memory in bytes
+	UUID     string
+}
+
+// GPUMetrics contains current GPU metrics
+type GPUMetrics struct {
+	PowerDraw         float64 // Watts
+	PerformanceLevel  string  // P0, P8, etc.
+	MemoryUsage       float64 // Percentage
+	GPUUtilization    int     // Percentage
+	MemoryUtilization int     // Percentage
+	Temperature       int     // Celsius
+}
+
+// Init initializes the NVML library
+func Init() error {
+	requestMutex.Lock()
+	defer requestMutex.Unlock()
+
+	ret := nvml.Init()
+	if ret != nvml.SUCCESS {
+		return fmt.Errorf("failed to initialize NVML: %s", nvml.ErrorString(ret))
+	}
+	return nil
+}
+
+// Shutdown shuts down the NVML library
+func Shutdown() error {
+	requestMutex.Lock()
+	defer requestMutex.Unlock()
+
+	ret := nvml.Shutdown()
+	if ret != nvml.SUCCESS {
+		return fmt.Errorf("failed to shutdown NVML: %s", nvml.ErrorString(ret))
+	}
+	return nil
+}
+
+// GetGPUDevices returns all available GPU devices
+func GetGPUDevices() ([]GPUDevice, error) {
+	requestMutex.Lock()
+	defer requestMutex.Unlock()
+
+	count, ret := nvml.DeviceGetCount()
+	if ret != nvml.SUCCESS {
+		return nil, fmt.Errorf("failed to get device count: %s", nvml.ErrorString(ret))
+	}
+
+	devices := make([]GPUDevice, count)
+
+	for i := 0; i < count; i++ {
+		device, ret := nvml.DeviceGetHandleByIndex(i)
+		if ret != nvml.SUCCESS {
+			return nil, fmt.Errorf("failed to get device handle for index %d: %s", i, nvml.ErrorString(ret))
+		}
+
+		// Get device name
+		name, ret := device.GetName()
+		if ret != nvml.SUCCESS {
+			return nil, fmt.Errorf("failed to get device name: %s", nvml.ErrorString(ret))
+		}
+
+		// Get PCI Bus ID
+		pciInfo, ret := device.GetPciInfo()
+		if ret != nvml.SUCCESS {
+			return nil, fmt.Errorf("failed to get PCI info: %s", nvml.ErrorString(ret))
+		}
+
+		// Get memory info
+		memInfo, ret := device.GetMemoryInfo()
+		if ret != nvml.SUCCESS {
+			return nil, fmt.Errorf("failed to get memory info: %s", nvml.ErrorString(ret))
+		}
+
+		// Get UUID
+		uuid, ret := device.GetUUID()
+		if ret != nvml.SUCCESS {
+			return nil, fmt.Errorf("failed to get device UUID: %s", nvml.ErrorString(ret))
+		}
+
+		devices[i] = GPUDevice{
+			Index:    i,
+			Handle:   device,
+			Name:     name,
+			PCIBusID: convertCString(pciInfo.BusId),
+			Memory:   memInfo.Total,
+			UUID:     uuid,
+		}
+	}
+
+	return devices, nil
+}
+
+// GetGPUMetrics retrieves current metrics for a GPU device with timeout protection
+func GetGPUMetrics(device GPUDevice) (GPUMetrics, error) {
+	// Use a timeout channel to prevent hanging requests
+	done := make(chan struct {
+		metrics GPUMetrics
+		err     error
+	}, 1)
+
+	go func() {
+		metrics, err := getGPUMetricsInternal(device)
+		done <- struct {
+			metrics GPUMetrics
+			err     error
+		}{metrics, err}
+	}()
+
+	select {
+	case result := <-done:
+		return result.metrics, result.err
+	case <-time.After(10 * time.Second):
+		return GPUMetrics{}, fmt.Errorf("timeout getting GPU metrics for device %s", device.Name)
+	}
+}
+
+// getGPUMetricsInternal performs the actual NVML calls with mutex protection
+func getGPUMetricsInternal(device GPUDevice) (GPUMetrics, error) {
+	requestMutex.Lock()
+	defer requestMutex.Unlock()
+
+	metrics := GPUMetrics{}
+
+	// Get power draw
+	power, ret := device.Handle.GetPowerUsage()
+	if ret == nvml.SUCCESS {
+		metrics.PowerDraw = float64(power) / 1000.0 // Convert mW to W
+	} else if ret != nvml.ERROR_NOT_SUPPORTED {
+		return metrics, fmt.Errorf("failed to get power usage: %s", nvml.ErrorString(ret))
+	}
+
+	// Get performance state
+	perfState, ret := device.Handle.GetPerformanceState()
+	if ret == nvml.SUCCESS {
+		metrics.PerformanceLevel = fmt.Sprintf("P%d", int(perfState))
+	} else if ret != nvml.ERROR_NOT_SUPPORTED {
+		return metrics, fmt.Errorf("failed to get performance state: %s", nvml.ErrorString(ret))
+	}
+
+	// Get memory usage
+	memInfo, ret := device.Handle.GetMemoryInfo()
+	if ret == nvml.SUCCESS {
+		metrics.MemoryUsage = float64(memInfo.Used) / float64(memInfo.Total) * 100.0
+	} else {
+		return metrics, fmt.Errorf("failed to get memory info: %s", nvml.ErrorString(ret))
+	}
+
+	// Get utilization rates
+	utilization, ret := device.Handle.GetUtilizationRates()
+	if ret == nvml.SUCCESS {
+		metrics.GPUUtilization = int(utilization.Gpu)
+		metrics.MemoryUtilization = int(utilization.Memory)
+	} else if ret != nvml.ERROR_NOT_SUPPORTED {
+		return metrics, fmt.Errorf("failed to get utilization rates: %s", nvml.ErrorString(ret))
+	}
+
+	// Get temperature
+	temperature, ret := device.Handle.GetTemperature(nvml.TEMPERATURE_GPU)
+	if ret == nvml.SUCCESS {
+		metrics.Temperature = int(temperature)
+	} else if ret != nvml.ERROR_NOT_SUPPORTED {
+		return metrics, fmt.Errorf("failed to get temperature: %s", nvml.ErrorString(ret))
+	}
+
+	return metrics, nil
+}
+
+// GetShortPCIBusID formats PCI Bus ID from 00000000:04:00.0 to 00:04:00.0
+func GetShortPCIBusID(pciBusID string) string {
+	// Split by colon to separate domain:bus:device.function
+	parts := strings.Split(pciBusID, ":")
+	if len(parts) >= 3 {
+		// Format: domain should be 2 digits instead of 8
+		domain := parts[0]
+		if len(domain) > 2 {
+			// Take the last 2 characters of the domain
+			domain = domain[len(domain)-2:]
+		}
+		// Return formatted as domain:bus:device.function
+		return fmt.Sprintf("%s:%s:%s", domain, parts[1], parts[2])
+	}
+	// If format is unexpected, return as-is
+	return pciBusID
+}
+
+// GetDeviceID generates a unique device identifier for MQTT topics
+func GetDeviceID(device GPUDevice) string {
+	// Format PCI Bus ID to short format and remove unwanted characters
+	shortPCIBusID := GetShortPCIBusID(device.PCIBusID)
+	deviceID := strings.Replace(shortPCIBusID, ":", "_", -1)
+	deviceID = strings.Replace(deviceID, ".", "_", -1)
+	return strings.ToLower(deviceID)
+}
+
+// GetDeviceDisplayName generates a display name in the format: {HOSTNAME} {PCI ID} - NVIDIA {MODEL} {VRAM}
+func GetDeviceDisplayName(device GPUDevice, hostname string) string {
+	vramGB := float64(device.Memory) / (1024 * 1024 * 1024)
+
+	// Extract model name from device name (remove "NVIDIA" prefix if present)
+	modelName := device.Name
+	if strings.HasPrefix(strings.ToUpper(modelName), "NVIDIA ") {
+		modelName = strings.TrimPrefix(modelName, "NVIDIA ")
+	}
+
+	// Use short format PCI Bus ID (00:04:00.0 instead of 00000000:04:00.0)
+	shortPCIBusID := GetShortPCIBusID(device.PCIBusID)
+	return fmt.Sprintf("%s %s - NVIDIA %s %.0fGB", hostname, shortPCIBusID, modelName, vramGB)
+}
+
+// GetNVMLVersion returns the NVML version information
+func GetNVMLVersion() (string, error) {
+	requestMutex.Lock()
+	defer requestMutex.Unlock()
+
+	version, ret := nvml.SystemGetNVMLVersion()
+	if ret != nvml.SUCCESS {
+		return "", fmt.Errorf("failed to get NVML version: %s", nvml.ErrorString(ret))
+	}
+	return version, nil
+}
+
+// GetDriverVersion returns the NVIDIA driver version
+func GetDriverVersion() (string, error) {
+	requestMutex.Lock()
+	defer requestMutex.Unlock()
+
+	version, ret := nvml.SystemGetDriverVersion()
+	if ret != nvml.SUCCESS {
+		return "", fmt.Errorf("failed to get driver version: %s", nvml.ErrorString(ret))
+	}
+	return version, nil
+}
+
+// IsDeviceAvailable checks if a GPU device is still available and responsive
+func IsDeviceAvailable(device GPUDevice) bool {
+	requestMutex.Lock()
+	defer requestMutex.Unlock()
+
+	// Try to get device name as a simple health check
+	_, ret := device.Handle.GetName()
+	return ret == nvml.SUCCESS
+}

+ 79 - 0
pkg/nvidia/nvidia_windows.go

@@ -0,0 +1,79 @@
+//go:build windows
+// +build windows
+
+package nvidia
+
+import (
+	"errors"
+	"fmt"
+)
+
+// GPUDevice represents an NVIDIA GPU device
+type GPUDevice struct {
+	Index    int
+	Handle   interface{} // Placeholder for Windows
+	Name     string
+	PCIBusID string
+	Memory   uint64 // Total memory in bytes
+	UUID     string
+}
+
+// GPUMetrics contains current GPU metrics
+type GPUMetrics struct {
+	PowerDraw         float64 // Watts
+	PerformanceLevel  string  // P0, P8, etc.
+	MemoryUsage       float64 // Percentage
+	GPUUtilization    int     // Percentage
+	MemoryUtilization int     // Percentage
+	Temperature       int     // Celsius
+}
+
+// Init initializes the NVML library
+func Init() error {
+	return errors.New("NVML is not supported on Windows build. Please use Linux build for production")
+}
+
+// Shutdown shuts down the NVML library
+func Shutdown() error {
+	return nil
+}
+
+// GetGPUDevices returns all available GPU devices
+func GetGPUDevices() ([]GPUDevice, error) {
+	return nil, errors.New("NVML is not supported on Windows build. Please use Linux build for production")
+}
+
+// GetGPUMetrics retrieves current metrics for a GPU device
+func GetGPUMetrics(device GPUDevice) (GPUMetrics, error) {
+	return GPUMetrics{}, errors.New("NVML is not supported on Windows build. Please use Linux build for production")
+}
+
+// GetDeviceID generates a unique device identifier for MQTT topics
+func GetDeviceID(device GPUDevice) string {
+	return "mock_device_id"
+}
+
+// GetShortPCIBusID formats PCI Bus ID from 00000000:04:00.0 to 00:04:00.0 (Windows stub)
+func GetShortPCIBusID(pciBusID string) string {
+	return pciBusID
+}
+
+// GetDeviceDisplayName generates a display name in the format: {HOSTNAME} {PCI ID} - NVIDIA {MODEL} {VRAM}
+func GetDeviceDisplayName(device GPUDevice, hostname string) string {
+	return fmt.Sprintf("%s 00:01:00.0 - NVIDIA Mock GPU 0GB", hostname)
+}
+
+// GetNVMLVersion returns the NVML version information (Windows stub)
+func GetNVMLVersion() (string, error) {
+	return "", errors.New("NVML is not supported on Windows build. Please use Linux build for production")
+}
+
+// GetDriverVersion returns the NVIDIA driver version (Windows stub)
+func GetDriverVersion() (string, error) {
+	return "", errors.New("NVML is not supported on Windows build. Please use Linux build for production")
+}
+
+// IsDeviceAvailable checks if a GPU device is still available and responsive (Windows stub)
+func IsDeviceAvailable(device GPUDevice) bool {
+	return false
+}