main.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. package main
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "encoding/hex"
  6. "encoding/json"
  7. "fmt"
  8. "log"
  9. "os"
  10. "os/signal"
  11. "sync"
  12. "syscall"
  13. "time"
  14. mqtt "github.com/eclipse/paho.mqtt.golang"
  15. "github.com/pccr10001/nvml-gpu-ha/pkg/config"
  16. "github.com/pccr10001/nvml-gpu-ha/pkg/homeassistant"
  17. "github.com/pccr10001/nvml-gpu-ha/pkg/nvidia"
  18. "github.com/spf13/cobra"
  19. )
  20. var (
  21. cfg *config.Config
  22. monitoringMutex sync.Mutex
  23. isMonitoring bool
  24. lastMonitorTime time.Time
  25. rootCmd = &cobra.Command{
  26. Use: "nvml-gpu-ha",
  27. Short: "NVIDIA GPU monitoring for Home Assistant via MQTT",
  28. Long: "Monitor NVIDIA GPU metrics and send them to Home Assistant via MQTT with auto-discovery support",
  29. Run: run,
  30. }
  31. )
  32. func init() {
  33. // Command line flags
  34. rootCmd.PersistentFlags().String("config", "/etc/nvml-gpu-ha.conf", "Configuration file path")
  35. rootCmd.PersistentFlags().String("hostname", "", "Hostname prefix for GPU names (default: system hostname)")
  36. rootCmd.PersistentFlags().String("mqtt-host", "localhost", "MQTT broker host")
  37. rootCmd.PersistentFlags().Int("mqtt-port", 1883, "MQTT broker port")
  38. rootCmd.PersistentFlags().String("mqtt-username", "", "MQTT username")
  39. rootCmd.PersistentFlags().String("mqtt-password", "", "MQTT password")
  40. rootCmd.PersistentFlags().Bool("mqtt-lwt-enable", true, "Enable MQTT Last Will and Testament")
  41. rootCmd.PersistentFlags().Bool("mqtt-retain", true, "Retain MQTT messages")
  42. rootCmd.PersistentFlags().Int("polling-period", 30, "GPU polling period in seconds")
  43. }
  44. func main() {
  45. if err := rootCmd.Execute(); err != nil {
  46. log.Fatal(err)
  47. }
  48. }
  49. func run(cmd *cobra.Command, args []string) {
  50. var err error
  51. cfg, err = config.LoadConfig(cmd)
  52. if err != nil {
  53. log.Fatal("Failed to load configuration:", err)
  54. }
  55. // If hostname is not provided, use system hostname
  56. if cfg.Hostname == "" {
  57. if hostname, err := os.Hostname(); err == nil {
  58. cfg.Hostname = hostname
  59. } else {
  60. log.Printf("Warning: Failed to get system hostname, using 'localhost': %v", err)
  61. cfg.Hostname = "localhost"
  62. }
  63. }
  64. // Display configuration source
  65. configFile, _ := cmd.Flags().GetString("config")
  66. if _, err := os.Stat(configFile); err == nil {
  67. log.Printf("Loaded configuration from: %s", configFile)
  68. } else {
  69. log.Printf("Using default configuration (no config file found at: %s)", configFile)
  70. }
  71. // Display key configuration values (without sensitive data)
  72. log.Printf("Hostname: %s", cfg.Hostname)
  73. log.Printf("MQTT Broker: %s:%d", cfg.MQTTHost, cfg.MQTTPort)
  74. log.Printf("MQTT Username: %s", func() string {
  75. if cfg.MQTTUsername != "" {
  76. return cfg.MQTTUsername
  77. } else {
  78. return "(none)"
  79. }
  80. }())
  81. log.Printf("Polling Period: %d seconds", cfg.PollingPeriod)
  82. log.Printf("MQTT LWT Enabled: %v", cfg.MQTTLWTEnable)
  83. log.Printf("MQTT Retain: %v", cfg.MQTTRetain)
  84. // Initialize NVIDIA management library
  85. if err := nvidia.Init(); err != nil {
  86. log.Fatal("Failed to initialize NVIDIA management library:", err)
  87. }
  88. defer nvidia.Shutdown()
  89. // Display version information
  90. if nvmlVersion, err := nvidia.GetNVMLVersion(); err == nil {
  91. log.Printf("NVML Version: %s", nvmlVersion)
  92. }
  93. if driverVersion, err := nvidia.GetDriverVersion(); err == nil {
  94. log.Printf("NVIDIA Driver Version: %s", driverVersion)
  95. }
  96. // Get GPU information
  97. gpus, err := nvidia.GetGPUDevices()
  98. if err != nil {
  99. log.Fatal("Failed to get GPU devices:", err)
  100. }
  101. if len(gpus) == 0 {
  102. log.Fatal("No NVIDIA GPUs found")
  103. }
  104. log.Printf("Found %d NVIDIA GPU(s)", len(gpus))
  105. for i, gpu := range gpus {
  106. shortPCIID := nvidia.GetShortPCIBusID(gpu.PCIBusID)
  107. log.Printf("GPU %d: %s (%s, %.1fGB)", i, gpu.Name, shortPCIID, float64(gpu.Memory)/(1024*1024*1024))
  108. }
  109. // Setup MQTT client
  110. mqttClient := setupMQTTClient()
  111. defer mqttClient.Disconnect(250)
  112. // Setup Home Assistant discovery
  113. haManager := homeassistant.NewManager(mqttClient, cfg)
  114. // Register all GPU sensors with Home Assistant
  115. for _, gpu := range gpus {
  116. if err := haManager.RegisterGPUSensors(gpu, cfg.Hostname); err != nil {
  117. log.Printf("Failed to register sensors for GPU %s: %v", gpu.Name, err)
  118. }
  119. }
  120. // Setup graceful shutdown
  121. ctx, cancel := context.WithCancel(context.Background())
  122. defer cancel()
  123. go func() {
  124. sigChan := make(chan os.Signal, 1)
  125. signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
  126. <-sigChan
  127. log.Println("Received shutdown signal, stopping...")
  128. cancel()
  129. }()
  130. // Main monitoring loop
  131. ticker := time.NewTicker(time.Duration(cfg.PollingPeriod) * time.Second)
  132. defer ticker.Stop()
  133. log.Printf("Starting GPU monitoring loop (polling every %d seconds)", cfg.PollingPeriod)
  134. for {
  135. select {
  136. case <-ctx.Done():
  137. log.Println("Shutting down...")
  138. return
  139. case <-ticker.C:
  140. monitorGPUs(mqttClient, gpus)
  141. }
  142. }
  143. }
  144. func setupMQTTClient() mqtt.Client {
  145. opts := mqtt.NewClientOptions()
  146. opts.AddBroker(fmt.Sprintf("tcp://%s:%d", cfg.MQTTHost, cfg.MQTTPort))
  147. // Generate random suffix for client ID to avoid conflicts
  148. randomBytes := make([]byte, 3)
  149. if _, err := rand.Read(randomBytes); err == nil {
  150. clientID := fmt.Sprintf("nvml-gpu-ha-%s", hex.EncodeToString(randomBytes))
  151. opts.SetClientID(clientID)
  152. } else {
  153. // Fallback to timestamp if random generation fails
  154. clientID := fmt.Sprintf("nvml-gpu-ha-%d", time.Now().Unix())
  155. opts.SetClientID(clientID)
  156. }
  157. opts.SetUsername(cfg.MQTTUsername)
  158. opts.SetPassword(cfg.MQTTPassword)
  159. opts.SetAutoReconnect(true)
  160. opts.SetConnectRetry(true)
  161. opts.SetConnectRetryInterval(10 * time.Second)
  162. if cfg.MQTTLWTEnable {
  163. opts.SetWill("homeassistant/sensor/nvml-gpu-ha/availability", "offline", 1, cfg.MQTTRetain)
  164. }
  165. opts.SetOnConnectHandler(func(client mqtt.Client) {
  166. log.Println("Connected to MQTT broker")
  167. if cfg.MQTTLWTEnable {
  168. client.Publish("homeassistant/sensor/nvml-gpu-ha/availability", 1, cfg.MQTTRetain, "online")
  169. }
  170. })
  171. opts.SetConnectionLostHandler(func(client mqtt.Client, err error) {
  172. log.Printf("Connection lost to MQTT broker: %v", err)
  173. })
  174. client := mqtt.NewClient(opts)
  175. if token := client.Connect(); token.Wait() && token.Error() != nil {
  176. log.Fatal("Failed to connect to MQTT broker:", token.Error())
  177. }
  178. return client
  179. }
  180. func monitorGPUs(client mqtt.Client, gpus []nvidia.GPUDevice) {
  181. // Prevent overlapping monitoring requests
  182. monitoringMutex.Lock()
  183. defer monitoringMutex.Unlock()
  184. if isMonitoring {
  185. log.Printf("Previous monitoring request still in progress, skipping this cycle")
  186. return
  187. }
  188. // Check if enough time has passed since last monitoring
  189. if time.Since(lastMonitorTime) < time.Duration(cfg.PollingPeriod/2)*time.Second {
  190. log.Printf("Too soon since last monitoring, skipping this cycle")
  191. return
  192. }
  193. isMonitoring = true
  194. defer func() {
  195. isMonitoring = false
  196. lastMonitorTime = time.Now()
  197. }()
  198. log.Printf("Starting GPU monitoring cycle...")
  199. startTime := time.Now()
  200. var wg sync.WaitGroup
  201. for _, gpu := range gpus {
  202. wg.Add(1)
  203. go func(gpu nvidia.GPUDevice) {
  204. defer wg.Done()
  205. metrics, err := nvidia.GetGPUMetrics(gpu)
  206. if err != nil {
  207. log.Printf("Failed to get metrics for GPU %s: %v", gpu.Name, err)
  208. return
  209. }
  210. publishMetrics(client, gpu, metrics)
  211. }(gpu)
  212. }
  213. wg.Wait()
  214. duration := time.Since(startTime)
  215. log.Printf("GPU monitoring cycle completed in %v", duration)
  216. }
  217. func publishMetrics(client mqtt.Client, gpu nvidia.GPUDevice, metrics nvidia.GPUMetrics) {
  218. // Publish individual sensor values
  219. sensors := map[string]interface{}{
  220. "power_draw": metrics.PowerDraw,
  221. "performance_level": metrics.PerformanceLevel,
  222. "memory_usage": metrics.MemoryUsage,
  223. "gpu_utilization": metrics.GPUUtilization,
  224. "temperature": metrics.Temperature,
  225. }
  226. deviceID := nvidia.GetDeviceID(gpu)
  227. for sensor, value := range sensors {
  228. topic := fmt.Sprintf("homeassistant/sensor/nvml-gpu/%s_%s/state", deviceID, sensor)
  229. payload, err := json.Marshal(value)
  230. if err != nil {
  231. log.Printf("Failed to marshal sensor data for %s: %v", sensor, err)
  232. continue
  233. }
  234. token := client.Publish(topic, 1, cfg.MQTTRetain, payload)
  235. if !token.WaitTimeout(5*time.Second) || token.Error() != nil {
  236. log.Printf("Failed to publish %s data: %v", sensor, token.Error())
  237. }
  238. }
  239. log.Printf("Published metrics for GPU: %s", gpu.Name)
  240. }