diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..012b1fb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +mpzbc \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..738ab6c --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# mpzbc - music player zigbee client +[zigbee2mqtt](https://www.zigbee2mqtt.io) + [IKEA E1744 SYMFONISK sound controller](https://www.zigbee2mqtt.io/devices/E1744.html) + mpzbc == wireless mpd remote + +## Usage +``` +go get github.com/feuerrot/mpzbc +MQTTSERVER=mqtthost:1883 MQTTTOPIC=zigbee2mqtt/friendly_e1744_name MPDSERVER=mpdhost:6600 mpzbc +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..47d6783 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/feuerrot/mpzbc + +go 1.14 + +require ( + github.com/eclipse/paho.mqtt.golang v1.2.0 + github.com/fhs/gompd v1.0.1 + golang.org/x/net v0.0.0-20201010224723-4f7140c49acb // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b27fa09 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t2y2qayIX0= +github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= +github.com/fhs/gompd v1.0.1 h1:kBcAhjnAPJQAylZXR0TeH+d2vpjawXlTtKYguqNlF4A= +github.com/fhs/gompd v1.0.1/go.mod h1:b219/mNa9PvRqvkUip51b23hGL3iX4d4q3gNXdtrD04= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/mpzbc.go b/mpzbc.go new file mode 100644 index 0000000..684bf3a --- /dev/null +++ b/mpzbc.go @@ -0,0 +1,166 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "strconv" + "time" + + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/fhs/gompd/mpd" +) + +const volumedelta int = 5 + +type mpzbc struct { + mqttClient mqtt.Client + mqttServer string + mqttTopic string + mpdClient *mpd.Client + mpdServer string + mpdStatus string + mpdVolume int +} + +type control struct { + Action string + Battery int + Linkquality int +} + +func (m *mpzbc) mqttMessage(client mqtt.Client, msg mqtt.Message) { + fmt.Printf("Message: %s\n", msg.Payload()) + m.updateMPD() + + ctrl := control{} + if err := json.Unmarshal(msg.Payload(), &ctrl); err != nil { + fmt.Printf("Unmarshal error: %v", err) + } + + switch ctrl.Action { + case "play_pause": + if m.mpdStatus == "play" { + m.mpdClient.Pause(true) + } else { + m.mpdClient.Play(-1) + } + case "rotate_left": + m.updateMPD() + if m.mpdVolume != -1 { + m.mpdClient.SetVolume(m.mpdVolume - volumedelta) + } + case "rotate_right": + m.updateMPD() + if m.mpdVolume != -1 { + m.mpdClient.SetVolume(m.mpdVolume + volumedelta) + } + case "skip_backward": + m.mpdClient.Previous() + case "skip_forward": + m.mpdClient.Next() + } +} + +func (m *mpzbc) connectMQTT() error { + fmt.Println("Build MQTT Client") + co := mqtt.NewClientOptions() + co.AddBroker("tcp://" + m.mqttServer) + co.SetClientID(fmt.Sprintf("mpzbc_%x", os.Getpid())) + co.SetAutoReconnect(true) + co.SetCleanSession(true) + + co.OnConnect = func(c mqtt.Client) { + if token := c.Subscribe(m.mqttTopic, 0, m.mqttMessage); token.Wait() && token.Error() != nil { + fmt.Printf("error during mqtt subscribe: %v\n", token.Error()) + } + } + + client := mqtt.NewClient(co) + fmt.Println("Connect to MQTT") + if token := client.Connect(); token.Wait() && token.Error() != nil { + return fmt.Errorf("error during mqtt connect: %v", token.Error()) + } + + return nil +} + +func (m *mpzbc) updateMPD() error { + status, err := m.mpdClient.Status() + if err != nil { + return fmt.Errorf("couldn't get MPD status: %v", err) + } + + state, ok := status["state"] + if !ok { + return fmt.Errorf("no state in MPD status") + } + m.mpdStatus = state + + volume, ok := status["volume"] + if !ok { + m.mpdVolume = -1 + } else { + m.mpdVolume, err = strconv.Atoi(volume) + if err != nil { + return fmt.Errorf("couldn't convert %s to integer: %v", volume, err) + } + } + + return nil +} + +func (m *mpzbc) connectMPD() error { + mpdClient, err := mpd.Dial("tcp", m.mpdServer) + if err != nil { + return fmt.Errorf("error while connecting to %s: %v", m.mpdServer, err) + } + m.mpdClient = mpdClient + + return nil +} + +func (m *mpzbc) getEnv() error { + m.mqttServer = os.Getenv("MQTTSERVER") + if m.mqttServer == "" { + return fmt.Errorf("MQTTSERVER is empty") + } + + m.mqttTopic = os.Getenv("MQTTTOPIC") + if m.mqttTopic == "" { + return fmt.Errorf("MQTTTOPIC is empty") + } + + m.mpdServer = os.Getenv("MPDSERVER") + if m.mpdServer == "" { + return fmt.Errorf("MPDSERVER is empty") + } + + return nil +} + +func (m *mpzbc) run() error { + if err := m.getEnv(); err != nil { + return fmt.Errorf("couldn't get settings: %v", err) + } + if err := m.connectMPD(); err != nil { + return fmt.Errorf("couldn't connect to MPD: %v", err) + } + if err := m.connectMQTT(); err != nil { + return fmt.Errorf("couldn't connect to MQTT: %v", err) + } + for { + if err := m.updateMPD(); err != nil { + return fmt.Errorf("couldn't update MPD state: %v", err) + } + fmt.Printf("state: %s\tvolume: %d\n", m.mpdStatus, m.mpdVolume) + time.Sleep(1 * time.Second) + } +} + +func main() { + client := mpzbc{} + if err := client.run(); err != nil { + fmt.Printf("error during client.run(): %v\n", err) + } +}