Skip to content

Commit

Permalink
refactor homie (#2)
Browse files Browse the repository at this point in the history
* add GHA workflows

* publish dockerfiles

* refactor homie part
  • Loading branch information
svenwltr authored Aug 4, 2023
1 parent ba359b7 commit 09c501d
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 103 deletions.
83 changes: 49 additions & 34 deletions cmd/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"context"
"errors"
"fmt"
"net/url"
"strconv"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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")
}
100 changes: 40 additions & 60 deletions pkg/dal/homie/homie.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (

mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/sirupsen/logrus"
"golang.org/x/exp/slices"
)

const (
Expand All @@ -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)

Expand All @@ -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, "/")

Expand All @@ -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
Expand All @@ -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)
Expand Down
21 changes: 12 additions & 9 deletions pkg/dal/homie/types.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
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

Format string
Settable bool
Retained bool
Unit string

Value any
}

0 comments on commit 09c501d

Please sign in to comment.