First commit
This commit is contained in:
commit
e4a00e0768
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/mqtt-sounder
|
||||||
|
/mqtt-sounder.exe
|
||||||
|
/.vscode
|
||||||
|
/sounds
|
||||||
|
/config.yaml
|
68
README.md
Normal file
68
README.md
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# MQTT sounder
|
||||||
|
|
||||||
|
Simple mqtt command executor to play sounds.
|
||||||
|
|
||||||
|
Config example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
mqtt:
|
||||||
|
address: "tcp://example.com:1883"
|
||||||
|
username: "mqtt"
|
||||||
|
password: "${MQTT_PASSWORD}"
|
||||||
|
client_id: "mqtt_sounder"
|
||||||
|
command_topic: "notify/sounder/command"
|
||||||
|
state_topic: "notify/sounder/state"
|
||||||
|
player:
|
||||||
|
default_program: ffplay
|
||||||
|
default_args:
|
||||||
|
- "-autoexit"
|
||||||
|
- "-nodisp"
|
||||||
|
- "-loglevel"
|
||||||
|
- "quiet"
|
||||||
|
sounds:
|
||||||
|
test:
|
||||||
|
args: ["sounds/test.mp3"] # args appended to default_args
|
||||||
|
doorbell:
|
||||||
|
args: ["sounds/doorbell.wav"]
|
||||||
|
|
||||||
|
# default:
|
||||||
|
# program: bash # default_program replaced to program
|
||||||
|
# args: # default_args replaced to args because program specified
|
||||||
|
# - "-c"
|
||||||
|
# - "echo sound not found"
|
||||||
|
|
||||||
|
# default:
|
||||||
|
# args:
|
||||||
|
# - "sounds/{payload}.mp3" # {payload} replaced to command payload, use only in trusted environment
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Password is provided via **MQTT_PASSWORD** environment variable.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```
|
||||||
|
mqtt-sounder config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Systemd unit example:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Play sound via mqtt commands
|
||||||
|
After=network-online.target
|
||||||
|
StartLimitIntervalSec=500
|
||||||
|
StartLimitBurst=5
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=mqtt_sounder
|
||||||
|
Environment=MQTT_PASSWORD=xxxxxxxxxxxxx
|
||||||
|
WorkingDirectory=/opt/mqtt-sounder/
|
||||||
|
ExecStart=/opt/mqtt-sounder/mqtt-sounder config.yaml
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
11
go.mod
Normal file
11
go.mod
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module mqtt-sounder
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.4.3 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
|
golang.org/x/net v0.8.0 // indirect
|
||||||
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
11
go.sum
Normal file
11
go.sum
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||||
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
127
main.go
Normal file
127
main.go
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var config = Config{}
|
||||||
|
|
||||||
|
var onCommandMessage mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) {
|
||||||
|
value := string(msg.Payload())
|
||||||
|
|
||||||
|
fmt.Printf("Requested %s\n", value)
|
||||||
|
|
||||||
|
soundDef, found := config.Player.Sounds[value]
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
soundDef, found = config.Player.Sounds["default"]
|
||||||
|
if !found {
|
||||||
|
fmt.Fprintf(os.Stderr, "No config entry found\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token := client.Publish(config.Mqtt.StateTopic, 0, false, "playing")
|
||||||
|
token.Wait()
|
||||||
|
|
||||||
|
var args []string
|
||||||
|
var program string
|
||||||
|
|
||||||
|
if soundDef.Program == "" {
|
||||||
|
program = config.Player.DefaultProgram
|
||||||
|
|
||||||
|
args = make([]string, len(config.Player.DefaultArgs))
|
||||||
|
copy(args, config.Player.DefaultArgs)
|
||||||
|
|
||||||
|
args = append(args, soundDef.Args...)
|
||||||
|
} else {
|
||||||
|
program = soundDef.Program
|
||||||
|
args = make([]string, len(soundDef.Args))
|
||||||
|
copy(args, soundDef.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range args {
|
||||||
|
args[i] = strings.ReplaceAll(args[i], "{payload}", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Running %v %v\n", program, args)
|
||||||
|
cmd := exec.Command(program, args...)
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
stdout, err := cmd.Output()
|
||||||
|
|
||||||
|
out := strings.TrimSpace(string(stdout))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Execution error: %v\n", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if out != "" {
|
||||||
|
fmt.Printf("Command output: %s\n", out)
|
||||||
|
}
|
||||||
|
|
||||||
|
token = client.Publish(config.Mqtt.StateTopic, 0, false, "stopped")
|
||||||
|
token.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
var onConnected mqtt.OnConnectHandler = func(client mqtt.Client) {
|
||||||
|
fmt.Println("Connected")
|
||||||
|
|
||||||
|
token := client.Subscribe(config.Mqtt.CommandTopic, 1, onCommandMessage)
|
||||||
|
token.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
var onDisconnected mqtt.ConnectionLostHandler = func(client mqtt.Client, err error) {
|
||||||
|
fmt.Fprintf(os.Stderr, "Connection lost: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "Usage: %s config.yaml\n", os.Args[0])
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
yamlData, err := os.ReadFile(os.Args[1])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
yamlStr := os.ExpandEnv(string(yamlData))
|
||||||
|
err = yaml.Unmarshal([]byte(yamlStr), &config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := mqtt.NewClientOptions()
|
||||||
|
opts.AddBroker(config.Mqtt.Address)
|
||||||
|
opts.SetClientID(config.Mqtt.ClientId)
|
||||||
|
opts.SetUsername(config.Mqtt.Username)
|
||||||
|
opts.SetPassword(config.Mqtt.Password)
|
||||||
|
opts.SetAutoReconnect(true)
|
||||||
|
|
||||||
|
opts.OnConnect = onConnected
|
||||||
|
opts.OnConnectionLost = onDisconnected
|
||||||
|
client := mqtt.NewClient(opts)
|
||||||
|
|
||||||
|
if token := client.Connect(); token.Wait() && token.Error() != nil {
|
||||||
|
panic(token.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait forever for interrupt
|
||||||
|
exit := make(chan os.Signal, 1)
|
||||||
|
defer close(exit)
|
||||||
|
signal.Notify(exit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-exit
|
||||||
|
|
||||||
|
client.Disconnect(250)
|
||||||
|
}
|
26
structs.go
Normal file
26
structs.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type MqttConfig struct {
|
||||||
|
Address string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
CommandTopic string `yaml:"command_topic"`
|
||||||
|
StateTopic string `yaml:"state_topic"`
|
||||||
|
ClientId string `yaml:"client_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SoundConfig struct {
|
||||||
|
Program string
|
||||||
|
Args []string `yaml:",flow"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlayerConfig struct {
|
||||||
|
DefaultProgram string `yaml:"default_program"`
|
||||||
|
DefaultArgs []string `yaml:"default_args,flow"`
|
||||||
|
Sounds map[string]SoundConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Mqtt MqttConfig
|
||||||
|
Player PlayerConfig
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user