mirror of
https://github.com/nikdoof/hapz2m.git
synced 2026-01-30 03:28:18 +00:00
Maintain MQTT message order during processing
Previously the message handler spawned a goroutine for each message and hoped for the best, but obviously it didn't work. Z2M often emitted the current (old) state first, before sending out the updated state. Without ordering, the old state might get processed later and overwrote the new, correct state. Message handling is now serialized, but decoupled, using a buffered channel and a separate goroutine for processing.
This commit is contained in:
59
bridge.go
59
bridge.go
@@ -79,10 +79,17 @@ type Bridge struct {
|
||||
pendingUpdates sync.Map // queued updates before HAP init
|
||||
|
||||
mqttClient mqtt.Client
|
||||
mqttMessages chan *mqttMessage
|
||||
|
||||
updateListeners sync.Map
|
||||
}
|
||||
|
||||
type mqttMessage struct {
|
||||
Tstamp time.Time
|
||||
Topic string
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
type BridgeDevice struct {
|
||||
Device *Device
|
||||
Accessory *accessory.A
|
||||
@@ -98,6 +105,8 @@ func NewBridge(ctx context.Context, storeDir string) *Bridge {
|
||||
|
||||
hapInitCh: make(chan struct{}),
|
||||
devices: make(map[string]*BridgeDevice),
|
||||
|
||||
mqttMessages: make(chan *mqttMessage, 100),
|
||||
}
|
||||
|
||||
br.bridgeAcc = accessory.NewBridge(accessory.Info{
|
||||
@@ -259,6 +268,16 @@ func (br *Bridge) ConnectMQTT() error {
|
||||
return tok.Error()
|
||||
}
|
||||
|
||||
// start MQTT message processor
|
||||
go func() {
|
||||
// only start processing after init
|
||||
br.WaitConfigured()
|
||||
|
||||
for m := range br.mqttMessages {
|
||||
br.UpdateAccessoryState(m.Topic, m.Payload, m.Tstamp)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -383,8 +402,8 @@ func (br *Bridge) handleMqttMessage(_ mqtt.Client, msg mqtt.Message) {
|
||||
}
|
||||
}
|
||||
|
||||
// spawn a goroutine to handle message, since mutex might block
|
||||
go func() {
|
||||
isBridgeTopic := strings.HasPrefix(topic, "bridge/")
|
||||
|
||||
if br.DebugMode && topic != "bridge/logging" {
|
||||
log.Printf("received MQTT %s: %s", topic, payload)
|
||||
}
|
||||
@@ -392,13 +411,19 @@ func (br *Bridge) handleMqttMessage(_ mqtt.Client, msg mqtt.Message) {
|
||||
br.hapInitMutex.RLock()
|
||||
defer br.hapInitMutex.RUnlock()
|
||||
|
||||
isBridgeTopic := strings.HasPrefix(topic, "bridge/")
|
||||
|
||||
// check if HAP bridge device has been initialized
|
||||
if !br.hapInitDone {
|
||||
if isBridgeTopic {
|
||||
// need to look out for bridge/devices for intiial setup
|
||||
if topic == "bridge/devices" {
|
||||
go func() {
|
||||
br.hapInitMutex.Lock()
|
||||
defer br.hapInitMutex.Unlock()
|
||||
|
||||
// re-verify that init has not been done
|
||||
if br.hapInitDone {
|
||||
return
|
||||
}
|
||||
|
||||
err := br.AddDevicesFromJSON(payload)
|
||||
if err != nil {
|
||||
log.Printf("unable to add devices from JSON: %v", err)
|
||||
@@ -410,13 +435,6 @@ func (br *Bridge) handleMqttMessage(_ mqtt.Client, msg mqtt.Message) {
|
||||
log.Printf("cannot load z2m state: %s", err)
|
||||
}
|
||||
|
||||
// "upgrade" to write lock for modifications
|
||||
br.hapInitMutex.RUnlock() // unlock now, relock again later
|
||||
defer br.hapInitMutex.RLock() // ... before defer kicks in
|
||||
|
||||
br.hapInitMutex.Lock()
|
||||
defer br.hapInitMutex.Unlock()
|
||||
|
||||
// dequeue and apply state updates
|
||||
log.Print("applying deferred updates...")
|
||||
br.pendingUpdates.Range(func(k, v any) bool {
|
||||
@@ -428,17 +446,21 @@ func (br *Bridge) handleMqttMessage(_ mqtt.Client, msg mqtt.Message) {
|
||||
br.hapInitDone = true
|
||||
|
||||
close(br.hapInitCh) // signal to waiting threads
|
||||
}
|
||||
} else {
|
||||
}()
|
||||
} else if !isBridgeTopic {
|
||||
// queue other messages for state updating after setup
|
||||
// only keep latest message for each device
|
||||
log.Printf("queueing updates for %s", topic)
|
||||
br.pendingUpdates.Store(topic, payload)
|
||||
}
|
||||
} else if !isBridgeTopic {
|
||||
br.UpdateAccessoryState(topic, payload, recvTime)
|
||||
// queue up MQTT messages
|
||||
select {
|
||||
case br.mqttMessages <- &mqttMessage{recvTime, topic, payload}:
|
||||
default:
|
||||
log.Printf("cannot queue MQTT message %s: %s", topic, payload)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Gets a list of all added accessories
|
||||
@@ -597,8 +619,7 @@ wait:
|
||||
}
|
||||
}
|
||||
|
||||
//br.updateListeners.CompareAndDelete(key, ch) // needs go 1.20
|
||||
br.updateListeners.Delete(key)
|
||||
br.updateListeners.CompareAndDelete(key, ch)
|
||||
|
||||
if !updated {
|
||||
err = ErrUpdateTimeout
|
||||
@@ -704,7 +725,7 @@ func (br *Bridge) UpdateAccessoryState(devName string, payload []byte, tstamp ti
|
||||
updateKey := makeUpdateKey(dev.Device, mapping)
|
||||
if ch, waiting := br.updateListeners.Load(updateKey); waiting {
|
||||
if BRIDGE_DEVMODE {
|
||||
log.Printf("sending new value for %q via chan", updateKey)
|
||||
log.Printf("sending new value %+v for %q via chan", newVal, updateKey)
|
||||
}
|
||||
select {
|
||||
case ch.(chan any) <- newVal:
|
||||
|
||||
Reference in New Issue
Block a user