package mq import ( "context" "fmt" "sync" "sync/atomic" "time" paho "github.com/eclipse/paho.mqtt.golang" ) type MQTT struct { client paho.Client mu sync.RWMutex closed bool reconnecting atomic.Bool } func NewMQTT(cfg Config) (*MQTT, error) { if !cfg.Enabled { return &MQTT{}, nil } if cfg.Broker == "" { return nil, fmt.Errorf("mqtt broker is empty") } if cfg.KeepAlive <= 0 { cfg.KeepAlive = 30 * time.Second } if cfg.ConnectTimeout <= 0 { cfg.ConnectTimeout = 10 * time.Second } if cfg.ReconnectInterval <= 0 { cfg.ReconnectInterval = 5 * time.Second } c := &MQTT{} opts := paho.NewClientOptions().AddBroker(cfg.Broker).SetClientID(cfg.ClientID).SetUsername(cfg.Username).SetPassword(cfg.Password) opts.SetKeepAlive(cfg.KeepAlive).SetCleanSession(cfg.CleanSession).SetConnectTimeout(cfg.ConnectTimeout).SetAutoReconnect(true) opts.SetMaxReconnectInterval(cfg.ReconnectInterval).SetResumeSubs(true).SetOrderMatters(false) opts.SetConnectionNotificationHandler(func(_ paho.Client, notification paho.ConnectionNotification) { switch notification.Type() { case paho.ConnectionNotificationTypeConnecting, paho.ConnectionNotificationTypeLost: c.reconnecting.Store(true) case paho.ConnectionNotificationTypeConnected: c.reconnecting.Store(false) } }) client := paho.NewClient(opts) connect := client.Connect() if !connect.WaitTimeout(cfg.ConnectTimeout) { client.Disconnect(0) return nil, fmt.Errorf("connect mqtt: %w", ErrUnavailable) } if err := connect.Error(); err != nil { client.Disconnect(0) return nil, err } c.client = client return c, nil } func (c *MQTT) Publish(ctx context.Context, topic string, payload []byte, qos byte, retain bool) error { if topic == "" { return fmt.Errorf("mqtt topic is empty") } if qos > ExactlyOnce { return fmt.Errorf("invalid mqtt qos %d", qos) } ctx = nonNilContext(ctx) client, err := c.activeClient() if err != nil || client == nil || !client.IsConnected() { return ErrUnavailable } select { case <-ctx.Done(): return ctx.Err() default: } return waitToken(ctx, client.Publish(topic, qos, retain, payload)) } func (c *MQTT) Subscribe(ctx context.Context, topic string, qos byte, handler Handler) error { if topic == "" { return fmt.Errorf("mqtt topic is empty") } if qos > ExactlyOnce { return fmt.Errorf("invalid mqtt qos %d", qos) } ctx = nonNilContext(ctx) client, err := c.activeClient() if err != nil || client == nil || !client.IsConnected() { return ErrUnavailable } select { case <-ctx.Done(): return ctx.Err() default: } if handler == nil { return fmt.Errorf("mqtt handler is nil") } return waitToken(ctx, client.Subscribe(topic, qos, func(_ paho.Client, msg paho.Message) { handler(context.Background(), Message{Topic: msg.Topic(), Payload: append([]byte(nil), msg.Payload()...), QoS: msg.Qos(), Retain: msg.Retained()}) })) } func (c *MQTT) Unsubscribe(ctx context.Context, topics ...string) error { if len(topics) == 0 { return fmt.Errorf("mqtt topics are empty") } ctx = nonNilContext(ctx) client, err := c.activeClient() if err != nil || client == nil || !client.IsConnected() { return ErrUnavailable } select { case <-ctx.Done(): return ctx.Err() default: } return waitToken(ctx, client.Unsubscribe(topics...)) } func waitToken(ctx context.Context, token paho.Token) error { ctx = nonNilContext(ctx) if token == nil { return ErrUnavailable } select { case <-ctx.Done(): return ctx.Err() case <-token.Done(): return token.Error() } } func nonNilContext(ctx context.Context) context.Context { if ctx == nil { return context.Background() } return ctx } func (c *MQTT) activeClient() (paho.Client, error) { if c == nil { return nil, ErrUnavailable } c.mu.RLock() client := c.client closed := c.closed c.mu.RUnlock() if closed || client == nil { return nil, ErrUnavailable } return client, nil } func (c *MQTT) Connected() bool { client, err := c.activeClient() return err == nil && client.IsConnected() } // Reconnecting reports whether the MQTT driver is recovering its connection. // The reloadable integration waits for this state instead of replacing a // client while driver-level recovery is still in progress. func (c *MQTT) Reconnecting() bool { if c == nil { return false } c.mu.RLock() closed := c.closed c.mu.RUnlock() return !closed && c.reconnecting.Load() } func (c *MQTT) Close() error { c.mu.Lock() if c.closed { c.mu.Unlock() return nil } c.closed = true client := c.client c.client = nil c.mu.Unlock() if client != nil { client.Disconnect(250) } return nil }