discovery.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. package homeassistant
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "log"
  6. mqtt "github.com/eclipse/paho.mqtt.golang"
  7. "github.com/pccr10001/nvml-gpu-ha/pkg/config"
  8. "github.com/pccr10001/nvml-gpu-ha/pkg/nvidia"
  9. )
  10. // Manager handles Home Assistant MQTT Discovery
  11. type Manager struct {
  12. client mqtt.Client
  13. config *config.Config
  14. }
  15. // SensorConfig represents Home Assistant sensor configuration
  16. type SensorConfig struct {
  17. Name string `json:"name"`
  18. StateTopic string `json:"state_topic"`
  19. UniqueID string `json:"unique_id"`
  20. DeviceClass string `json:"device_class,omitempty"`
  21. UnitOfMeasurement string `json:"unit_of_measurement,omitempty"`
  22. Icon string `json:"icon,omitempty"`
  23. Device *DeviceInfo `json:"device"`
  24. AvailabilityTopic string `json:"availability_topic,omitempty"`
  25. PayloadAvailable string `json:"payload_available,omitempty"`
  26. PayloadNotAvailable string `json:"payload_not_available,omitempty"`
  27. ValueTemplate string `json:"value_template,omitempty"`
  28. StateClass string `json:"state_class,omitempty"`
  29. ForceUpdate bool `json:"force_update,omitempty"`
  30. }
  31. // DeviceInfo represents device information for Home Assistant
  32. type DeviceInfo struct {
  33. Identifiers []string `json:"identifiers"`
  34. Name string `json:"name"`
  35. Model string `json:"model"`
  36. Manufacturer string `json:"manufacturer"`
  37. SwVersion string `json:"sw_version,omitempty"`
  38. }
  39. // NewManager creates a new Home Assistant discovery manager
  40. func NewManager(client mqtt.Client, config *config.Config) *Manager {
  41. return &Manager{
  42. client: client,
  43. config: config,
  44. }
  45. }
  46. // RegisterGPUSensors registers all sensors for a GPU device
  47. func (m *Manager) RegisterGPUSensors(device nvidia.GPUDevice, hostname string) error {
  48. deviceID := nvidia.GetDeviceID(device)
  49. deviceName := nvidia.GetDeviceDisplayName(device, hostname)
  50. deviceInfo := &DeviceInfo{
  51. Identifiers: []string{deviceID, device.UUID},
  52. Name: deviceName,
  53. Model: device.Name,
  54. Manufacturer: "NVIDIA",
  55. SwVersion: "NVML",
  56. }
  57. sensors := []struct {
  58. key string
  59. name string
  60. deviceClass string
  61. unit string
  62. icon string
  63. stateClass string
  64. template string
  65. }{
  66. {
  67. key: "power_draw",
  68. name: "Power Draw",
  69. deviceClass: "power",
  70. unit: "W",
  71. icon: "mdi:lightning-bolt",
  72. stateClass: "measurement",
  73. },
  74. {
  75. key: "performance_level",
  76. name: "Performance Level",
  77. deviceClass: "",
  78. unit: "",
  79. icon: "mdi:speedometer",
  80. stateClass: "",
  81. },
  82. {
  83. key: "memory_usage",
  84. name: "VRAM Usage",
  85. deviceClass: "",
  86. unit: "%",
  87. icon: "mdi:memory",
  88. stateClass: "measurement",
  89. template: "{{ value | round(1) }}",
  90. },
  91. {
  92. key: "gpu_utilization",
  93. name: "GPU Utilization",
  94. deviceClass: "",
  95. unit: "%",
  96. icon: "mdi:chip",
  97. stateClass: "measurement",
  98. },
  99. {
  100. key: "temperature",
  101. name: "GPU Temperature",
  102. deviceClass: "temperature",
  103. unit: "°C",
  104. icon: "mdi:thermometer",
  105. stateClass: "measurement",
  106. },
  107. }
  108. for _, sensor := range sensors {
  109. if err := m.registerSensor(deviceID, deviceName, sensor.key, sensor.name,
  110. sensor.deviceClass, sensor.unit, sensor.icon, sensor.stateClass,
  111. sensor.template, deviceInfo); err != nil {
  112. return fmt.Errorf("failed to register sensor %s: %v", sensor.key, err)
  113. }
  114. }
  115. return nil
  116. }
  117. // registerSensor registers a single sensor with Home Assistant
  118. func (m *Manager) registerSensor(deviceID, deviceName, sensorKey, sensorName, deviceClass, unit, icon, stateClass, template string, deviceInfo *DeviceInfo) error {
  119. uniqueID := fmt.Sprintf("nvml_gpu_%s_%s", deviceID, sensorKey)
  120. stateTopic := fmt.Sprintf("homeassistant/sensor/nvml-gpu/%s_%s/state", deviceID, sensorKey)
  121. configTopic := fmt.Sprintf("homeassistant/sensor/nvml-gpu/%s_%s/config", deviceID, sensorKey)
  122. fullSensorName := sensorName
  123. sensorConfig := SensorConfig{
  124. Name: fullSensorName,
  125. StateTopic: stateTopic,
  126. UniqueID: uniqueID,
  127. DeviceClass: deviceClass,
  128. UnitOfMeasurement: unit,
  129. Icon: icon,
  130. Device: deviceInfo,
  131. StateClass: stateClass,
  132. ForceUpdate: true,
  133. }
  134. if template != "" {
  135. sensorConfig.ValueTemplate = template
  136. }
  137. // Add availability if LWT is enabled
  138. if m.config.MQTTLWTEnable {
  139. sensorConfig.AvailabilityTopic = "homeassistant/sensor/nvml-gpu-ha/availability"
  140. sensorConfig.PayloadAvailable = "online"
  141. sensorConfig.PayloadNotAvailable = "offline"
  142. }
  143. configJSON, err := json.Marshal(sensorConfig)
  144. if err != nil {
  145. return fmt.Errorf("failed to marshal sensor config: %v", err)
  146. }
  147. token := m.client.Publish(configTopic, 1, m.config.MQTTRetain, configJSON)
  148. if !token.WaitTimeout(5*1e9) || token.Error() != nil { // 5 seconds
  149. return fmt.Errorf("failed to publish sensor config: %v", token.Error())
  150. }
  151. log.Printf("Registered sensor: %s", fullSensorName)
  152. return nil
  153. }
  154. // RemoveGPUSensors removes all sensors for a GPU device
  155. func (m *Manager) RemoveGPUSensors(device nvidia.GPUDevice) error {
  156. deviceID := nvidia.GetDeviceID(device)
  157. sensors := []string{"power_draw", "performance_level", "memory_usage", "gpu_utilization", "temperature"}
  158. for _, sensor := range sensors {
  159. configTopic := fmt.Sprintf("homeassistant/sensor/nvml-gpu/%s_%s/config", deviceID, sensor)
  160. // Send empty payload to remove the sensor
  161. token := m.client.Publish(configTopic, 1, m.config.MQTTRetain, "")
  162. if !token.WaitTimeout(5*1e9) || token.Error() != nil {
  163. log.Printf("Failed to remove sensor %s: %v", sensor, token.Error())
  164. }
  165. }
  166. return nil
  167. }
  168. // PublishAvailability publishes availability status
  169. func (m *Manager) PublishAvailability(status string) error {
  170. if !m.config.MQTTLWTEnable {
  171. return nil
  172. }
  173. topic := "homeassistant/sensor/nvml-gpu-ha/availability"
  174. token := m.client.Publish(topic, 1, m.config.MQTTRetain, status)
  175. if !token.WaitTimeout(5*1e9) || token.Error() != nil {
  176. return fmt.Errorf("failed to publish availability: %v", token.Error())
  177. }
  178. return nil
  179. }