| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- 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
- }
|