Files
home_automatization/services/device-control-service/internal/mqttclient/client.go
T
cacto 3eb84aae6c Реализация device-control-service: gRPC + MQTT + Device Shadow в Redis
Добавлен gRPC-контракт proto/device_control (TurnOn/TurnOff/SetLevel),
общий Go-модуль proto/ для переиспользования сгенерированного кода.
device-control-service принимает команды по gRPC, обновляет desired_state
в Redis и публикует их в MQTT; слушает devices/+/telemetry и devices/+/ack
для отметки живости устройства и обновления reported_state; горутина
health-check переводит устройство в offline по таймауту и публикует
событие в RabbitMQ (device.status_changed) для будущего notification-service.

Проверено сквозным тестом через docker compose: grpcurl TurnOn/SetLevel
меняет desired_state и уходит в MQTT, mosquitto_pub с ack обновляет
reported_state и статус, health-check автоматически переводит устройство
в offline по истечении таймаута.
2026-07-26 18:40:36 +05:00

108 lines
2.9 KiB
Go

// Package mqttclient wraps paho for device-control-service's two-way MQTT
// use: subscribing to telemetry/ack topics for liveness and reported state,
// and publishing commands to devices.
package mqttclient
import (
"fmt"
"log/slog"
"sync"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
)
// Handler receives one message's topic, raw payload, and arrival time.
type Handler func(topic string, payload []byte, receivedAt time.Time)
type Config struct {
BrokerURL string // e.g. tcp://mosquitto:1883
ClientID string
}
type subscription struct {
topic string
qos byte
handler Handler
}
// Client wraps a paho client, replaying every registered subscription on
// each (re)connect so a dropped connection resubscribes automatically.
type Client struct {
client mqtt.Client
logger *slog.Logger
mu sync.Mutex
subs []subscription
}
func Connect(cfg Config, logger *slog.Logger) (*Client, error) {
c := &Client{logger: logger}
opts := mqtt.NewClientOptions().
AddBroker(cfg.BrokerURL).
SetClientID(cfg.ClientID).
SetAutoReconnect(true).
SetConnectRetry(true).
SetOnConnectHandler(func(mc mqtt.Client) {
c.resubscribeAll(mc)
}).
SetConnectionLostHandler(func(_ mqtt.Client, err error) {
logger.Warn("mqtt connection lost", "error", err)
})
mqttClient := mqtt.NewClient(opts)
token := mqttClient.Connect()
token.Wait()
if err := token.Error(); err != nil {
return nil, fmt.Errorf("connect to mqtt broker %s: %w", cfg.BrokerURL, err)
}
c.client = mqttClient
return c, nil
}
// Subscribe registers a handler for topic. It subscribes immediately if
// already connected, and will be replayed on every future reconnect.
func (c *Client) Subscribe(topic string, qos byte, handler Handler) error {
c.mu.Lock()
c.subs = append(c.subs, subscription{topic: topic, qos: qos, handler: handler})
c.mu.Unlock()
if !c.client.IsConnectionOpen() {
return nil
}
return c.subscribeOne(c.client, subscription{topic: topic, qos: qos, handler: handler})
}
func (c *Client) resubscribeAll(mc mqtt.Client) {
c.mu.Lock()
subs := append([]subscription(nil), c.subs...)
c.mu.Unlock()
for _, s := range subs {
if err := c.subscribeOne(mc, s); err != nil {
c.logger.Error("mqtt subscribe failed", "topic", s.topic, "error", err)
}
}
}
func (c *Client) subscribeOne(mc mqtt.Client, s subscription) error {
token := mc.Subscribe(s.topic, s.qos, func(_ mqtt.Client, msg mqtt.Message) {
s.handler(msg.Topic(), msg.Payload(), time.Now())
})
token.Wait()
return token.Error()
}
// Publish sends payload to topic and waits for the publish to complete.
func (c *Client) Publish(topic string, qos byte, retained bool, payload []byte) error {
token := c.client.Publish(topic, qos, retained, payload)
token.Wait()
return token.Error()
}
// Close disconnects, waiting up to 250ms for in-flight work to settle.
func (c *Client) Close() {
c.client.Disconnect(250)
}