main.go 7.6 KB

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