diff --git a/cmd/bridge.go b/cmd/bridge.go index a292c8d..3c8c4a8 100644 --- a/cmd/bridge.go +++ b/cmd/bridge.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "errors" "fmt" "net/url" "strconv" @@ -76,7 +77,7 @@ func (r *HomieBridgeRunner) Run(ctx context.Context) error { } type HomieBridge struct { - Broker *homie.Device + Broker *homie.Broker Speakers map[string]raumfeld.Speaker } @@ -145,52 +146,66 @@ func (b *HomieBridge) HandleBrokerAction(nodeID, propertyID, value string) error func (b *HomieBridge) PublishHomieDefinitions(ctx context.Context) error { logrus.Infof("publishing homie nodes") - nodes := homie.Nodes{} - - for usn, speaker := range b.Speakers { - nodes[usn] = homie.Node{ - Name: speaker.FriendlyName(), - Type: "Speaker", - Properties: homie.Properties{ - "onoff": homie.Property{ - Name: "On/Off", - DataType: "boolean", - Retained: true, - Settable: true, - }, - "volume": homie.Property{ - Name: "Volume", - DataType: "float", - Format: "0:1", - Retained: true, - Settable: true, - }, - "mute": homie.Property{ - Name: "Mute", - DataType: "boolean", - Format: "0:1", - Retained: true, - Settable: true, - }, - }, + device := homie.Device{ + Name: "devilctl raumfeld-bridge", + Implementation: "github.com/svenwltr/devilctl", + } + + for nodeID, speaker := range b.Speakers { + device.NodeIDs = append(device.NodeIDs, nodeID) + err := errors.Join( + b.Broker.PublishNode(homie.Node{ + NodeID: nodeID, + Name: speaker.FriendlyName(), + Type: "Speaker", + PropertyIDs: []string{"onoff", "volume", "mute"}, + }), + b.Broker.PublishProperty(homie.Property{ + NodeID: nodeID, + PropertyID: "onoff", + Name: "On/Off", + DataType: "boolean", + Retained: true, + Settable: true, + }), + b.Broker.PublishProperty(homie.Property{ + NodeID: nodeID, + PropertyID: "volume", + Name: "Volume", + DataType: "float", + Format: "0:1", + Retained: true, + Settable: true, + }), + b.Broker.PublishProperty(homie.Property{ + NodeID: nodeID, + PropertyID: "mute", + Name: "Mute", + DataType: "boolean", + Format: "0:1", + Retained: true, + Settable: true, + }), + ) + if err != nil { + return err } } - b.Broker.Nodes = nodes - return b.Broker.PublishAll() + return b.Broker.PublishDevice(device) } func (b *HomieBridge) OnVolumeChange(s raumfeld.Speaker, volume int, channel string) { logrus.Infof("volume changed on speaker to %#v", volume) - b.Broker.Value(s.ID(), "volume", float64(volume)/100.) + b.Broker.PublishValue(s.ID(), "volume", float64(volume)/100.) } func (b *HomieBridge) OnMuteChange(s raumfeld.Speaker, muted bool, channel string) { logrus.Infof("mute changed on speaker to %#v", muted) - b.Broker.Value(s.ID(), "mute", muted) + b.Broker.PublishValue(s.ID(), "mute", muted) } func (b *HomieBridge) OnPowerStateChange(s raumfeld.Speaker, state string) { logrus.Infof("power state changed on speaker to %#v", state) - b.Broker.Value(s.ID(), "onoff", state != "MANUAL_STANDBY") + b.Broker.PublishValue(s.ID(), "onoff", state != "MANUAL_STANDBY") } diff --git a/pkg/dal/homie/homie.go b/pkg/dal/homie/homie.go index 3a95310..4bbfe2e 100644 --- a/pkg/dal/homie/homie.go +++ b/pkg/dal/homie/homie.go @@ -8,7 +8,6 @@ import ( mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/sirupsen/logrus" - "golang.org/x/exp/slices" ) const ( @@ -17,19 +16,18 @@ const ( QOSExactlyOnce = 2 ) -type Device struct { +type Broker struct { client mqtt.Client baseTopic string - Nodes Nodes ActionHandler func(string, string, string) error } -func New(broker string) (*Device, error) { +func New(server string) (*Broker, error) { baseTopic := "homie/raumfeld-bridge" opts := mqtt.NewClientOptions() - opts.AddBroker(broker) + opts.AddBroker(server) opts.SetAutoReconnect(true) opts.SetWill(path.Join(baseTopic, "$state"), "lost", QOSAtLeastOnce, true) @@ -40,25 +38,25 @@ func New(broker string) (*Device, error) { return nil, token.Error() } - device := &Device{ + broker := &Broker{ client: client, baseTopic: baseTopic, } - _ = client.Subscribe(path.Join(baseTopic, "+", "+", "set"), QOSAtMostOnce, device.handleAction) + _ = client.Subscribe(path.Join(baseTopic, "+", "+", "set"), QOSAtMostOnce, broker.handleAction) // not sure what to do which the token - return device, nil + return broker, nil } -func (d *Device) handleAction(client mqtt.Client, message mqtt.Message) { - if d.ActionHandler == nil { +func (b *Broker) handleAction(client mqtt.Client, message mqtt.Message) { + if b.ActionHandler == nil { message.Ack() return } topic := message.Topic() - topic = strings.TrimPrefix(topic, d.baseTopic) + topic = strings.TrimPrefix(topic, b.baseTopic) topic = strings.TrimSuffix(topic, "set") topic = strings.Trim(topic, "/") @@ -68,7 +66,7 @@ func (d *Device) handleAction(client mqtt.Client, message mqtt.Message) { return } - err := d.ActionHandler(nodeID, propertyID, string(message.Payload())) + err := b.ActionHandler(nodeID, propertyID, string(message.Payload())) if err != nil { logrus.Error(err) return @@ -77,76 +75,58 @@ func (d *Device) handleAction(client mqtt.Client, message mqtt.Message) { message.Ack() } -func (d *Device) MustClose() { - err := d.Close() +func (b *Broker) MustClose() { + err := b.Close() if err != nil { logrus.Error(err) } } -func (d *Device) Close() error { - err := d.publish("$state", "disconnected") +func (b *Broker) Close() error { + err := b.publish("$state", "disconnected") if err != nil { return err } - d.client.Disconnect(1000) + b.client.Disconnect(1000) return nil } -func (d *Device) PublishAll() error { - nodeIDs := []string{} - for nodeID, node := range d.Nodes { - nodeIDs = append(nodeIDs, nodeID) - - propertyIDs := []string{} - for propertyID, property := range node.Properties { - propertyIDs = append(propertyIDs, propertyID) - - err := errors.Join( - d.publish(path.Join(nodeID, propertyID, "$name"), property.Name), - d.publish(path.Join(nodeID, propertyID, "$datatype"), property.DataType), - d.publish(path.Join(nodeID, propertyID, "$format"), property.Format), - d.publish(path.Join(nodeID, propertyID, "$unit"), property.Unit), - d.publish(path.Join(nodeID, propertyID, "$settable"), fmt.Sprintf("%t", property.Settable)), - d.publish(path.Join(nodeID, propertyID, "$retained"), fmt.Sprintf("%t", property.Retained)), - ) - if err != nil { - return err - } - } - - slices.Sort(propertyIDs) - err := errors.Join( - d.publish(path.Join(nodeID, "$name"), node.Name), - d.publish(path.Join(nodeID, "$type"), node.Type), - d.publish(path.Join(nodeID, "$properties"), strings.Join(propertyIDs, ",")), - ) - if err != nil { - return err - } - } - - err := errors.Join( +func (d *Broker) PublishDevice(device Device) error { + return errors.Join( d.publish("$homie", "4.0.0"), - d.publish("$name", "Homie Raumfeld Bridge"), + d.publish("$name", device.Name), d.publish("$state", "ready"), - d.publish("$implementation", "github.com/svenwltr/devilctl"), - d.publish("$nodes", strings.Join(nodeIDs, ",")), + d.publish("$implementation", device.Implementation), + d.publish("$nodes", strings.Join(device.NodeIDs, ",")), ) +} - if err != nil { - return err - } +func (d *Broker) PublishNode(node Node) error { + return errors.Join( + d.publish(path.Join(node.NodeID, "$name"), node.Name), + d.publish(path.Join(node.NodeID, "$type"), node.Type), + d.publish(path.Join(node.NodeID, "$properties"), strings.Join(node.PropertyIDs, ",")), + ) +} - return nil +func (d *Broker) PublishProperty(property Property) error { + prefix := path.Join(property.NodeID, property.PropertyID) + return errors.Join( + d.publish(path.Join(prefix, "$name"), property.Name), + d.publish(path.Join(prefix, "$datatype"), property.DataType), + d.publish(path.Join(prefix, "$format"), property.Format), + d.publish(path.Join(prefix, "$unit"), property.Unit), + d.publish(path.Join(prefix, "$settable"), fmt.Sprintf("%t", property.Settable)), + d.publish(path.Join(prefix, "$retained"), fmt.Sprintf("%t", property.Retained)), + ) } -func (d *Device) Value(nodeID, propertyID string, value any) error { +func (d *Broker) PublishValue(nodeID, propertyID string, value any) error { return d.publish(path.Join(nodeID, propertyID), fmt.Sprint(value)) } -func (d *Device) publish(topic string, message string) error { +func (d *Broker) publish(topic string, message string) error { fullTopic := path.Join(d.baseTopic, topic) token := d.client.Publish(fullTopic, QOSAtLeastOnce, true, message) diff --git a/pkg/dal/homie/types.go b/pkg/dal/homie/types.go index a563b37..e284da9 100644 --- a/pkg/dal/homie/types.go +++ b/pkg/dal/homie/types.go @@ -1,17 +1,22 @@ package homie -type Nodes map[string]Node +type Device struct { + Name string + Implementation string + NodeIDs []string +} type Node struct { - Name string - Type string - - Properties Properties + NodeID string + Name string + Type string + PropertyIDs []string } -type Properties map[string]Property - type Property struct { + NodeID string + PropertyID string + Name string DataType string @@ -19,6 +24,4 @@ type Property struct { Settable bool Retained bool Unit string - - Value any }