From c959a92c1a0fca1e9860f091ac75957eb6825555 Mon Sep 17 00:00:00 2001 From: claes Date: Mon, 9 Dec 2024 21:47:22 +0100 Subject: [PATCH] Promising snapcast, but needs lots of cleanup --- cmd/regelverk-tmp/main.go | 21 +++ flake.nix | 2 +- go.mod | 11 +- go.sum | 14 ++ internal/bridge-snapcast.go | 29 +++ internal/cmd.go | 2 + internal/loop-snapcast.go | 354 ++++++++++++++++++++++++++++++++++++ internal/regelverk.go | 1 + internal/statemachine.go | 49 +++++ 9 files changed, 478 insertions(+), 5 deletions(-) create mode 100644 cmd/regelverk-tmp/main.go create mode 100644 internal/bridge-snapcast.go create mode 100644 internal/loop-snapcast.go diff --git a/cmd/regelverk-tmp/main.go b/cmd/regelverk-tmp/main.go new file mode 100644 index 0000000..e619d7a --- /dev/null +++ b/cmd/regelverk-tmp/main.go @@ -0,0 +1,21 @@ +package main + +import ( + internal "github.com/claes/regelverk/internal" +) + +func main() { + + config, debug, dryRun := internal.ParseConfig() + + loops := []internal.ControlLoop{ + &internal.SnapcastLoop{}, + } + + bridgeWrappers := []internal.BridgeWrapper{ + &internal.SnapcastBridgeWrapper{}, + &internal.PulseaudioBridgeWrapper{}, + } + + internal.StartRegelverk(config, loops, &bridgeWrappers, dryRun, debug) +} diff --git a/flake.nix b/flake.nix index b92aeed..bb1a153 100644 --- a/flake.nix +++ b/flake.nix @@ -48,7 +48,7 @@ buildInputs = [pkgs.libcec pkgs.libcec_platform]; - vendorHash = "sha256-BdzFcBx5Se0acPCwhMol2EGCD3AxCuv/gI+thUKCoaI="; + vendorHash = "sha256-Ucs+T6gdaVUAh5LjvnckjnlO9ekCC9ZQ8Tbqr1LHiAM="; }; }); diff --git a/go.mod b/go.mod index adc3121..cfc7e3f 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/claes/cec v0.0.0-20240820185959-6db0712de894 github.com/claes/cec-mqtt v0.0.0-20241117210133-9685c24250c0 github.com/claes/mpd-mqtt v0.0.0-20241117211350-5c216a578a24 - github.com/claes/pulseaudio-mqtt v0.0.0-20241117205947-6a36f61c1560 + github.com/claes/pulseaudio-mqtt v0.0.0-20241207101230-d8f01a653ffc github.com/claes/rotel-mqtt v0.0.0-20241117210930-508800a2173f github.com/claes/routeros-mqtt v0.0.0-20241120202123-a86f39417801 github.com/claes/samsung-mqtt v0.0.0-20241117210402-2bf065d544e3 @@ -19,12 +19,15 @@ require ( ) require ( + github.com/ConnorsApps/snapcast-go v0.2.0 // indirect + github.com/claes/snapcast-mqtt v0.0.0-20241204191410-562224f67682 // indirect github.com/fhs/gompd/v2 v2.3.0 // indirect github.com/go-routeros/routeros/v3 v3.0.0 // indirect github.com/jfreymuth/pulse v0.1.1 // indirect github.com/qmuntal/stateless v1.7.1 // indirect github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/time v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index 3088575..67d71ac 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/ConnorsApps/snapcast-go v0.2.0 h1:KBESELR+5aq+yfofh3JKeDdnT8JWHQTrXr23owzjU1o= +github.com/ConnorsApps/snapcast-go v0.2.0/go.mod h1:/UP8u37AdEwdmfZjtbbr76Wzorq7z/fEjpQWy0IVTtk= github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg= github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8= github.com/claes/cec v0.0.0-20240820185959-6db0712de894 h1:cZOiacVo+F8/VV+dEfqzNoqzbZVWiPbEDZ6rCKPK9uo= @@ -14,6 +16,8 @@ github.com/claes/pulseaudio-mqtt v0.0.0-20241117140819-d43e31611f50 h1:xmtP03YiF github.com/claes/pulseaudio-mqtt v0.0.0-20241117140819-d43e31611f50/go.mod h1:NbLAajteKoKa/m1YqSaJj/X6RCMUADqW2SovdFemRJA= github.com/claes/pulseaudio-mqtt v0.0.0-20241117205947-6a36f61c1560 h1:yEQ16G4a762kAuBqsggoOMY0ARgY6dhNefLmOF0tvxw= github.com/claes/pulseaudio-mqtt v0.0.0-20241117205947-6a36f61c1560/go.mod h1:NbLAajteKoKa/m1YqSaJj/X6RCMUADqW2SovdFemRJA= +github.com/claes/pulseaudio-mqtt v0.0.0-20241207101230-d8f01a653ffc h1:vL2+JfHmQMutbstu/GaXr5epmPbYw5mX11aLGEeDP0A= +github.com/claes/pulseaudio-mqtt v0.0.0-20241207101230-d8f01a653ffc/go.mod h1:NbLAajteKoKa/m1YqSaJj/X6RCMUADqW2SovdFemRJA= github.com/claes/rotel-mqtt v0.0.0-20240606131203-59f7974c7ca3 h1:I+xs3PivUxXv2yYCYt6CsjRTju/tMz49Pd+PTMA/fS0= github.com/claes/rotel-mqtt v0.0.0-20240606131203-59f7974c7ca3/go.mod h1:DXNpasrX+83LQrnnPGJmTIOexwA9XkeyU22atNDevD4= github.com/claes/rotel-mqtt v0.0.0-20241117210930-508800a2173f h1:GAyguJRdXwm6/ruS6B+DiYMDHU1DJo/1lpT2yg5IK7E= @@ -26,6 +30,8 @@ github.com/claes/samsung-mqtt v0.0.0-20241117142225-c52df5c2aa93 h1:u3L4jMhjmFrR github.com/claes/samsung-mqtt v0.0.0-20241117142225-c52df5c2aa93/go.mod h1:0rHVPioCmrVMl9tOe2jVXGr6a8myw4jw0ijXlZjwKbQ= github.com/claes/samsung-mqtt v0.0.0-20241117210402-2bf065d544e3 h1:7VTrwjgqAT7xwOkgLo9xqUn41b2rHsiSO7xglHeEqx4= github.com/claes/samsung-mqtt v0.0.0-20241117210402-2bf065d544e3/go.mod h1:0rHVPioCmrVMl9tOe2jVXGr6a8myw4jw0ijXlZjwKbQ= +github.com/claes/snapcast-mqtt v0.0.0-20241204191410-562224f67682 h1:n9dYEqsHoeAhNbeH1YnV6349KYuwa/6BXiRm8pWRn4c= +github.com/claes/snapcast-mqtt v0.0.0-20241204191410-562224f67682/go.mod h1:a1wlHTrQhBN+mNV9zpQBd2/fgJU3SST0mTaP1Vs3Rmk= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= @@ -50,9 +56,17 @@ github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUY github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/bridge-snapcast.go b/internal/bridge-snapcast.go new file mode 100644 index 0000000..6f5504d --- /dev/null +++ b/internal/bridge-snapcast.go @@ -0,0 +1,29 @@ +package regelverk + +import ( + "log/slog" + + snapcastmqtt "github.com/claes/snapcast-mqtt/lib" + mqtt "github.com/eclipse/paho.mqtt.golang" +) + +type SnapcastBridgeWrapper struct { + bridge *snapcastmqtt.SnapcastMQTTBridge +} + +func (l *SnapcastBridgeWrapper) InitializeBridge(mqttClient mqtt.Client, config Config) error { + var err error + snapConfig := snapcastmqtt.SnapClientConfig{SnapServerAddress: config.SnapcastServer} + l.bridge, err = snapcastmqtt.NewSnapcastMQTTBridge(snapConfig, mqttClient, config.MQTTTopicPrefix) + if err != nil { + slog.Error("Could not create snapcast bridge", "error", err) + return err + } + return nil +} + +func (l *SnapcastBridgeWrapper) Run() error { + go l.bridge.MainLoop() + slog.Info("Snapcast bridge started") + return nil +} diff --git a/internal/cmd.go b/internal/cmd.go index 4051fa5..20153a3 100644 --- a/internal/cmd.go +++ b/internal/cmd.go @@ -19,6 +19,7 @@ func ParseConfig() (Config, *bool, *bool) { httpListenAddress := flag.String("httpListenAddress", ":8080", "HTTP listen address") rotelSerialPort := flag.String("rotelSerialPort", "", "Rotel serial port") samsungTVAddress := flag.String("samsungTVAddress", "", "Samsung TV address") + snapcastServer := flag.String("snapcastServer", "", "Snapcast server address") pulseServer := flag.String("pulseServer", "", "Pulse server") mpdServer := flag.String("mpdServer", "", "MPD server") mpdPasswordFile := flag.String("mpdPasswordFile", "", "MPD password file") @@ -51,6 +52,7 @@ func ParseConfig() (Config, *bool, *bool) { WebAddress: *httpListenAddress, RotelSerialPort: *rotelSerialPort, SamsungTvAddress: *samsungTVAddress, + SnapcastServer: *snapcastServer, MpdServer: *mpdServer, MpdPasswordFile: *mpdPasswordFile, RouterAddress: *routerAddress, diff --git a/internal/loop-snapcast.go b/internal/loop-snapcast.go new file mode 100644 index 0000000..27921b0 --- /dev/null +++ b/internal/loop-snapcast.go @@ -0,0 +1,354 @@ +package regelverk + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "reflect" + "regexp" + "strconv" + "time" + + pulsemqtt "github.com/claes/pulseaudio-mqtt/lib" + snapcastmqtt "github.com/claes/snapcast-mqtt/lib" + "github.com/qmuntal/stateless" +) + +type snapcastState int + +var topicStreamRe = regexp.MustCompile(`snapcast/stream/([^/]+)$`) +var topicClientRe = regexp.MustCompile(`snapcast/client/([^/]+)$`) +var topicGroupRe = regexp.MustCompile(`snapcast/group/([^/]+)$`) + +const ( + stateSnapcastOn snapcastState = iota + stateSnapcastOff +) + +type SnapcastLoop struct { + statusLoop + stateMachineMQTTBridge StateMachineMQTTBridge + isInitialized bool + snapcastServerState snapcastmqtt.SnapcastServer + pulseAudioState pulsemqtt.PulseAudioState +} + +// New approach +// Snapcast clietn with applicatio KodiDriver / process.binary "kodi.bin" +// Sink Snapcast (#1) +// Snapclient with -s = 1 argument (PulseAudio) + +// SINK INPUTS / Pavucontrol "Playback" +// Sink input #62 : Sink 1 Client: 128 kodi audio stream KodiSink Binary kodi.bin +// Sink input #71 : Sink 25 Client: 150 ALSA playback / ALSA plug-in [snapclient] Binary "snapclient" + +// SINKS / Pavucontrol "Output Devices" +// Sink #1 Snapcast + +// Sink #25 alsooutput_pci-... +// Active port: hdmi-output-0 +// or iec958-stereo-output + +// Changing something renumbers the sinks etc so need to be done in quick order +// So: general approach +// 1) Redirect the KODI sink input to user the Snapcast sink +// 2) ALSA Plugin for snapclient should go to appropriate out (via HDMI or soundcard optical) +// Make sure snapclient uses the default PA sink: /bin/snapclient -s 1 -h 127.0.0.1 --hostID livingroom +// 3) Set stream for Snapcast group to Pulseaudio +// 4) Set appropriate amp source according to ALSO Plugin (HDMI OPT or soundcard OPT) +/* + 0) determine sink input index for Kodi etc through pulseaudiostate + + 1) pulseaudio/sinkinput/req '{ "Command": "movesink", "SinkInputIndex": 168, "SinkName": "Snapcast" }' + + + 3) Topic: "snapcast/client/livingroom/stream/set", + Payload: "pulseaudio", + 4) Topic: "rotel/command/send", + Payload: "opt2!", + + + pulseaudio/sinkinput/req '{ "Command": "movesink", "SinkInputIndex": 168, "SinkName": "alsa_output.pci-0000_00_1f.3.analog-stereo" }' + Topic: "zigbee2mqtt/ikea_uttag/set", + Payload: "{\"state\": \"ON\", \"power_on_behavior\": \"ON\"}", + Qos: 2, + Retained: false, + Wait: 0 * time.Second, +*/ + +func snapcastOnOutputTmp(sinkInputIndex uint32) []MQTTPublish { + + slog.Info("SETTING SNAPCAST OUTPUT", "sinkInputIndex", sinkInputIndex) + result := []MQTTPublish{ + { + Topic: "pulseaudio/sinkinput/req", + Payload: fmt.Sprintf(`{ "Command": "movesink", "SinkInputIndex": %d, "SinkName": "Snapcast" }`, sinkInputIndex), + Qos: 2, + Retained: false, + Wait: 0 * time.Second, + }, + { + Topic: "snapcast/client/livingroom/stream/set", + Payload: "pulseaudio", + Qos: 2, + Retained: false, + Wait: 0 * time.Second, + }, + { + Topic: "rotel/command/send", + Payload: "opt2!", + Qos: 2, + Retained: false, + Wait: 0 * time.Second, + }, + } + return result +} + +func snapcastOffOutputTmp(sinkInputIndex uint32, sinkName string) []MQTTPublish { + + slog.Info("SETTING SNAPCAST OUTPUT", "sinkInputIndex", sinkInputIndex) + result := []MQTTPublish{ + { + Topic: "pulseaudio/sinkinput/req", + Payload: fmt.Sprintf(`{ "Command": "movesink", "SinkInputIndex": %d, "SinkName": "%s" }`, sinkInputIndex, sinkName), + Qos: 2, + Retained: false, + Wait: 0 * time.Second, + }, + { + Topic: "snapcast/client/livingroom/stream/set", + Payload: "default", + Qos: 2, + Retained: false, + Wait: 0 * time.Second, + }, + { + Topic: "rotel/command/send", + Payload: "opt1!", + Qos: 2, + Retained: false, + Wait: 0 * time.Second, + }, + } + return result +} + +func (l *SnapcastLoop) turnOnSnapcastTmp(_ context.Context, _ ...any) error { + + // kodiBinary := "kodi.bin" + // kodiApplicationName := "KodiSink" + // kodiMediaName := "kodi audio stream" + + // ChromeBinary := "chrome" + // ChromeApplicationName := "Google Chrome" + // ChromeMediaName := "Playback" + + slog.Info("Attempting to move sink", "len", len(l.pulseAudioState.SinkInputs)) + found := false + for _, sinkInput := range l.pulseAudioState.SinkInputs { + slog.Info("Looping over sink input", "sinkIndex", sinkInput.SinkIndex, "props", sinkInput.Properties) + if sinkInput.Properties["application.process.binary"] == "kodi.bin" { + slog.Info("Sink input match", "sinkIndex", sinkInput.SinkIndex) + l.stateMachineMQTTBridge.addEventsToPublish(snapcastOnOutputTmp(sinkInput.SinkInputIndex)) + found = true + } + } + if found { + return nil + } else { + return fmt.Errorf("Could not find sink input to turn on Snapcast for") + } +} + +func (l *SnapcastLoop) turnOffSnapcastTmp(_ context.Context, _ ...any) error { + + slog.Info("Attempting to move sink", "len", len(l.pulseAudioState.SinkInputs)) + found := false + for _, sinkInput := range l.pulseAudioState.SinkInputs { + slog.Info("Looping over sink input", "sinkIndex", sinkInput.SinkIndex, "props", sinkInput.Properties) + if sinkInput.Properties["application.process.binary"] == "kodi.bin" { + slog.Info("Sink input match", "sinkIndex", sinkInput.SinkIndex) + events := snapcastOffOutputTmp(sinkInput.SinkInputIndex, "alsa_output.pci-0000_00_0e.0.hdmi-stereo") + l.stateMachineMQTTBridge.addEventsToPublish(events) + found = true + } + } + if found { + return nil + } else { + return fmt.Errorf("Could not find sink input to turn on Snapcast for") + } +} + +/* +type MoveSinkInput struct { + SinkInputIndex uint32 + DeviceIndex uint32 -1 + DeviceName string //sink name +} + +func (c *PulseClient) MoveSinkInput(sinkInputIndex uint32, deviceName string) error { + err := c.protoClient.Request(&proto.MoveSinkInput{SinkInputIndex: sinkInputIndex, DeviceIndex: proto.Undefined, DeviceName: deviceName}, nil) + if err != nil { + return err + } + return nil +} + + +*/ + +// PA: +// GetClientInfo * not existing +// GetClientInfoReply +// GetClientInfoList +// GetClientInfoListReply + +// GetSinkInfo * komplettera +// GetSinkInfoList * +// GetSinkInfoReply * +// GetSinkInfoListReply * + +// GetSinkInputInfo * not existing +// GetSinkInputInfoList +// GetSinkInputInfoReply +// GetSinkInputInfoListReply +// Identify using client info + +func (l *SnapcastLoop) Init(m *mqttMessageHandler, config Config) { + l.stateMachineMQTTBridge = CreateStateMachineMQTTBridge("snapcast") + + // Variants: + // stream mediaflix pulseaudio to snapcast + // stream mediaflix mpd to snapcast + // stream mediaflix gmediarender to snapcast + // inspect currently playing source using for example + // pactl list sink-inputs + // inspect + + sm := stateless.NewStateMachine(stateSnapcastOff) // can this be reliable determined early on? probably not + sm.SetTriggerParameters("mqttEvent", reflect.TypeOf(MQTTEvent{})) + + sm.Configure(stateSnapcastOn). + OnEntry(l.turnOnSnapcastTmp). + Permit("mqttEvent", stateSnapcastOff, l.stateMachineMQTTBridge.guardStateSnapcastOff) + // Set mediaflix pulseaudio port/card/profile (?) to HDMI or diabled to avoid conflict with snapclient + // Something like command + // pactl set-card-profile alsa_card.pci-0000_00_0e.0 output:hdmi-stereo + // or + // pactl set-card-profile alsa_card.pci-0000_00_0e.0 off + // pulseaudio/cardprofile/0/set output:hdmi-stereo + // or + // pulseaudio/cardprofile/0/set off + + // Set mediaflix pulseaudio sink = Snapcast + // pulseaudio/sink/default/set alsa_output.pci-0000_00_0e.0.hdmi-stereo + // or + // pulseaudio/sink/default/set Snapcast + + // Set snapcast server stream=pulseaudio + + sm.Configure(stateSnapcastOff). + OnEntry(l.turnOffSnapcastTmp). + Permit("mqttEvent", stateSnapcastOn, l.stateMachineMQTTBridge.guardStateSnapcastOn) + l.stateMachineMQTTBridge.stateMachine = sm + l.isInitialized = true + slog.Debug("FSM initialized") + + slog.Debug("Initialized Snapcast SM") + +} + +func (l *SnapcastLoop) ProcessEvent(ev MQTTEvent) []MQTTPublish { + if l.isInitialized { + slog.Debug("Process event", "topic", ev.Topic) + + l.parseSnapcastClient(ev) + l.parseSnapcastGroup(ev) + l.parseSnapcastStream(ev) + l.parsePulseaudio(ev) + l.foo(ev) + + l.stateMachineMQTTBridge.stateValueMap.LogState() + slog.Debug("Fire event") + beforeState := l.stateMachineMQTTBridge.stateMachine.MustState() + l.stateMachineMQTTBridge.stateMachine.Fire("mqttEvent", ev) + + eventsToPublish := l.stateMachineMQTTBridge.getAndResetEventsToPublish() + slog.Info("Event fired", "topic", ev.Topic, "fsm", l.stateMachineMQTTBridge.name, "beforeState", beforeState, + "afterState", l.stateMachineMQTTBridge.stateMachine.MustState()) + return eventsToPublish + } else { + slog.Debug("Cannot process event: is not initialized") + return []MQTTPublish{} + } +} + +func (l *SnapcastLoop) parseSnapcastStream(ev MQTTEvent) { + matches := topicStreamRe.FindStringSubmatch(ev.Topic) + if len(matches) == 2 { + streamID := matches[1] + var snapcastStream snapcastmqtt.SnapcastStream + err := json.Unmarshal(ev.Payload.([]byte), &snapcastStream) + if err != nil { + slog.Error("Could not parse payload for snapcast stream", "error", err, "topic", ev.Topic) + } + if l.snapcastServerState.Streams == nil { + l.snapcastServerState.Streams = make(map[string]snapcastmqtt.SnapcastStream) + } + l.snapcastServerState.Streams[streamID] = snapcastStream + } +} + +func (l *SnapcastLoop) parseSnapcastClient(ev MQTTEvent) { + matches := topicClientRe.FindStringSubmatch(ev.Topic) + if len(matches) == 2 { + clientID := matches[1] + var snapcastClient snapcastmqtt.SnapcastClient + err := json.Unmarshal(ev.Payload.([]byte), &snapcastClient) + if err != nil { + slog.Error("Could not parse payload for snapcast client", "error", err, "topic", ev.Topic) + } + if l.snapcastServerState.Clients == nil { + l.snapcastServerState.Clients = make(map[string]snapcastmqtt.SnapcastClient) + } + l.snapcastServerState.Clients[clientID] = snapcastClient + } +} + +func (l *SnapcastLoop) parseSnapcastGroup(ev MQTTEvent) { + matches := topicGroupRe.FindStringSubmatch(ev.Topic) + if len(matches) == 2 { + groupID := matches[1] + var snapcastGroup snapcastmqtt.SnapcastGroup + err := json.Unmarshal(ev.Payload.([]byte), &snapcastGroup) + if err != nil { + slog.Error("Could not parse payload for snapcast group", "error", err, "topic", ev.Topic) + } + if l.snapcastServerState.Groups == nil { + l.snapcastServerState.Groups = make(map[string]snapcastmqtt.SnapcastGroup) + } + l.snapcastServerState.Groups[groupID] = snapcastGroup + } +} + +func (l *SnapcastLoop) parsePulseaudio(ev MQTTEvent) { + if ev.Topic == "pulseaudio/state" { + err := json.Unmarshal(ev.Payload.([]byte), &l.pulseAudioState) + if err != nil { + slog.Error("Could not parse payload for topic", "topic", ev.Topic, "error", err) + } + } +} + +func (l *SnapcastLoop) foo(ev MQTTEvent) { + if ev.Topic == "foo" { + snapcast, err := strconv.ParseBool(string(ev.Payload.([]byte))) + if err != nil { + slog.Info("Could not parse payload", "topic", "foo", "error", err) + } + l.stateMachineMQTTBridge.stateValueMap.setState("snapcast", snapcast) + } +} diff --git a/internal/regelverk.go b/internal/regelverk.go index b3d5284..383f1ce 100644 --- a/internal/regelverk.go +++ b/internal/regelverk.go @@ -23,6 +23,7 @@ type Config struct { WebAddress string RotelSerialPort string SamsungTvAddress string + SnapcastServer string MpdServer string MpdPasswordFile string Pulseserver string diff --git a/internal/statemachine.go b/internal/statemachine.go index 7540af9..39a3a62 100644 --- a/internal/statemachine.go +++ b/internal/statemachine.go @@ -173,7 +173,34 @@ func tvPowerOnOutput() []MQTTPublish { result = append(result, p) } return result +} +// TODO FIX!! +func snapcastOnOutput() []MQTTPublish { + result := []MQTTPublish{ + { + Topic: "zigbee2mqtt/ikea_uttag/set", + Payload: "{\"state\": \"ON\", \"power_on_behavior\": \"ON\"}", + Qos: 2, + Retained: false, + Wait: 0 * time.Second, + }, + } + return result +} + +// TODO FIX!! +func snapcastOffOutput() []MQTTPublish { + result := []MQTTPublish{ + { + Topic: "zigbee2mqtt/ikea_uttag/set", + Payload: "{\"state\": \"ON\", \"power_on_behavior\": \"ON\"}", + Qos: 2, + Retained: false, + Wait: 0 * time.Second, + }, + } + return result } func mpdPlayOutput() []MQTTPublish { @@ -238,6 +265,18 @@ func (l *StateMachineMQTTBridge) guardStateTvOffLong(_ context.Context, _ ...any return check } +func (l *StateMachineMQTTBridge) guardStateSnapcastOn(_ context.Context, _ ...any) bool { + check := l.stateValueMap.requireTrue("snapcast") + slog.Info("guardStateSnapcastOn", "check", check) + return check +} + +func (l *StateMachineMQTTBridge) guardStateSnapcastOff(_ context.Context, _ ...any) bool { + check := l.stateValueMap.requireFalse("snapcast") + slog.Info("guardStateSnapcastOff", "check", check) + return check +} + func (l *StateMachineMQTTBridge) guardStateKitchenAmpOn(_ context.Context, _ ...any) bool { check := l.stateValueMap.requireTrue("kitchenaudioplaying") slog.Info("guardStateKitchenAmpOn", "check", check) @@ -274,6 +313,16 @@ func (l *StateMachineMQTTBridge) turnOffLivingroomFloorlamp(_ context.Context, _ return nil } +func (l *StateMachineMQTTBridge) turnOnSnapcast(_ context.Context, _ ...any) error { + l.addEventsToPublish(snapcastOnOutput()) + return nil +} + +func (l *StateMachineMQTTBridge) turnOffSnapcast(_ context.Context, _ ...any) error { + l.addEventsToPublish(snapcastOffOutput()) + return nil +} + func (l *StateMachineMQTTBridge) turnOnTvAppliances(_ context.Context, _ ...any) error { l.addEventsToPublish(tvPowerOnOutput()) return nil