From 41e0b81169f25f6165afe202149e60e786265555 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Sat, 8 Jan 2022 21:55:41 +0100 Subject: [PATCH] add a button that acts like a MQTT switch, e.g. to control some IoT devices --- .gitignore | 2 + example_conf.json | 30 +++++++++++ pkg/mqtt/buttons.go | 127 ++++++++++++++++++++++++++++++++++++++++++++ pkg/mqtt/client.go | 46 +++++++++++++--- pkg/mqtt/factory.go | 31 +++++++++-- 5 files changed, 226 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index a887812..7599f70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /hamdeck +/test_conf.json + /cmd/pulse.go /cmd/hamlib.go diff --git a/example_conf.json b/example_conf.json index bffbbc6..dc5c3c7 100644 --- a/example_conf.json +++ b/example_conf.json @@ -168,6 +168,36 @@ "sourceOutput": "Source Output Media Name", "label": "Source" }, + { + "type": "mqtt.Switch", + "index": 28, + "label": "Lights", + "inputTopic": "tasmota/smartplug1/stat/POWER", + "outputTopic": "tasmota/smartplug1/cmnd/POWER", + "onPayload": "ON", + "offPayload": "OFF", + "mode": "toggle" + }, + { + "type": "mqtt.Switch", + "index": 29, + "label": "On", + "inputTopic": "tasmota/smartplug1/stat/POWER", + "outputTopic": "tasmota/smartplug1/cmnd/POWER", + "onPayload": "ON", + "offPayload": "OFF", + "mode": "ON" + }, + { + "type": "mqtt.Switch", + "index": 20, + "label": "Off", + "inputTopic": "tasmota/smartplug1/stat/POWER", + "outputTopic": "tasmota/smartplug1/cmnd/POWER", + "onPayload": "ON", + "offPayload": "OFF", + "mode": "OFF" + }, { "type": "hamlib.MOX", "index": 31 diff --git a/pkg/mqtt/buttons.go b/pkg/mqtt/buttons.go index 99d2a36..3bddaf9 100644 --- a/pkg/mqtt/buttons.go +++ b/pkg/mqtt/buttons.go @@ -4,6 +4,7 @@ import ( "fmt" "image" "image/color" + "strings" "github.com/ftl/hamdeck/pkg/hamdeck" ) @@ -153,3 +154,129 @@ func (b *TuneButton) Pressed() { func (b *TuneButton) Released() { // ignore } + +/* + SwitchButton +*/ + +func NewSwitchButton(client *Client, label string, inputTopic, outputTopic, onPayload, offPayload string, mode SwitchMode) *SwitchButton { + if label == "" { + label = "SW" + } + + result := &SwitchButton{ + client: client, + enabled: client.Connected(), + label: label, + inputTopic: inputTopic, + outputTopic: outputTopic, + onPayload: onPayload, + offPayload: offPayload, + mode: mode, + } + + client.Subscribe(result, inputTopic) + + return result +} + +type SwitchButton struct { + hamdeck.BaseButton + client *Client + offImage image.Image + onImage image.Image + enabled bool + on bool + label string + inputTopic string + outputTopic string + onPayload string + offPayload string + mode SwitchMode +} + +type SwitchMode string + +const ( + SwitchModeOn SwitchMode = "ON" + SwitchModeOff SwitchMode = "OFF" + SwitchModeToggle SwitchMode = "TOGGLE" +) + +func (b *SwitchButton) Enable(enabled bool) { + if enabled == b.enabled { + return + } + b.enabled = enabled + b.Invalidate(true) +} + +func (b *SwitchButton) SetInput(topic string, payload string) { + if topic != b.inputTopic { + return + } + payload = strings.TrimSpace(strings.ToUpper(payload)) + + wasOn := b.on + if b.mode == SwitchModeOff { + b.on = (payload == b.offPayload) + } else { + b.on = (payload == b.onPayload) + } + + if b.on == wasOn { + return + } + b.Invalidate(false) +} + +func (b *SwitchButton) Image(gc hamdeck.GraphicContext, redrawImages bool) image.Image { + if b.offImage == nil || b.onImage == nil || redrawImages { + b.redrawImages(gc) + } + switch { + case b.on: + return b.onImage + default: + return b.offImage + } +} + +func (b *SwitchButton) redrawImages(gc hamdeck.GraphicContext) { + if b.enabled { + gc.SetForeground(hamdeck.White) + } else { + gc.SetForeground(hamdeck.DisabledGray) + } + b.offImage = gc.DrawSingleLineTextButton(b.label) + gc.SwapColors() + b.onImage = gc.DrawSingleLineTextButton(b.label) +} + +func (b *SwitchButton) Pressed() { + if !(b.enabled) { + return + } + var payload string + + switch b.mode { + case SwitchModeOn: + payload = b.onPayload + case SwitchModeOff: + payload = b.offPayload + case SwitchModeToggle: + if b.on { + payload = b.offPayload + } else { + payload = b.onPayload + } + default: + return + } + + b.client.Publish(b.outputTopic, payload) +} + +func (b *SwitchButton) Released() { + // ignore +} diff --git a/pkg/mqtt/client.go b/pkg/mqtt/client.go index 3f06a9d..e7efe22 100644 --- a/pkg/mqtt/client.go +++ b/pkg/mqtt/client.go @@ -15,11 +15,12 @@ const mqttWaitTimeout = 200 * time.Millisecond func NewClient(address string, username string, password string) *Client { result := &Client{ - address: address, - alive: make(map[string]bool), - tx: make(map[string]bool), - tuning: make(map[string]bool), - swr: make(map[string]float64), + address: address, + alive: make(map[string]bool), + tx: make(map[string]bool), + tuning: make(map[string]bool), + swr: make(map[string]float64), + subscribers: make(map[string][]Subscriber), } opts := mqtt.NewClientOptions() @@ -56,6 +57,12 @@ type Client struct { tx map[string]bool tuning map[string]bool swr map[string]float64 + + subscribers map[string][]Subscriber +} + +type Subscriber interface { + SetInput(topic string, payload string) } type atu100Data struct { @@ -94,8 +101,15 @@ func (c *Client) connectionLost(_ mqtt.Client, err error) { } func (c *Client) messageReceived(_ mqtt.Client, msg mqtt.Message) { - topic := strings.ToLower(msg.Topic()) - //log.Printf("received MQTT message from topic: %s", topic) + topic := strings.TrimSpace(msg.Topic()) + // log.Printf("received MQTT message from topic: %s", topic) + + topicSubscribers := c.subscribers[topic] + for _, subscriber := range topicSubscribers { + subscriber.SetInput(topic, string(msg.Payload())) + } + + // notify the ATU100Tune buttons path, suffix, ok := splitTopic(topic) if !ok { return @@ -125,6 +139,24 @@ func splitTopic(topic string) (string, string, bool) { return topic[0:splitterIndex], topic[splitterIndex+1:], true } +func (c *Client) Subscribe(s Subscriber, topics ...string) { + for _, topic := range topics { + topic = strings.TrimSpace(topic) + topicSubscribers, ok := c.subscribers[topic] + topicSubscribers = append(topicSubscribers, s) + c.subscribers[topic] = topicSubscribers + + if !ok { + log.Printf("subscribing to %s", topic) + c.client.Subscribe(topic, 1, nil).WaitTimeout(mqttWaitTimeout) + } + } +} + +func (c *Client) Publish(topic string, payload string) { + c.client.Publish(topic, 0, false, payload) +} + func (c *Client) AddPath(path string) { c.paths = append(c.paths, path) c.subscribePath(path) diff --git a/pkg/mqtt/factory.go b/pkg/mqtt/factory.go index 057a5b0..7368564 100644 --- a/pkg/mqtt/factory.go +++ b/pkg/mqtt/factory.go @@ -2,17 +2,24 @@ package mqtt import ( "log" + "strings" "github.com/ftl/hamdeck/pkg/hamdeck" ) const ( - ConfigLabel = "label" - ConfigPath = "path" + ConfigLabel = "label" + ConfigPath = "path" + ConfigInputTopic = "inputTopic" + ConfigOutputTopic = "outputTopic" + ConfigOnPayload = "onPayload" + ConfigOffPayload = "offPayload" + ConfigMode = "mode" ) const ( - TuneButtonType = "mqtt.AT100Tune" + TuneButtonType = "mqtt.AT100Tune" + SwitchButtonType = "mqtt.Switch" ) func NewButtonFactory(address string, username string, password string) *Factory { @@ -35,6 +42,8 @@ func (f *Factory) CreateButton(config map[string]interface{}) hamdeck.Button { switch config[hamdeck.ConfigType] { case TuneButtonType: return f.createTuneButton(config) + case SwitchButtonType: + return f.createSwitchButton(config) default: return nil } @@ -51,3 +60,19 @@ func (f *Factory) createTuneButton(config map[string]interface{}) hamdeck.Button return NewTuneButton(f.client, label, path) } + +func (f *Factory) createSwitchButton(config map[string]interface{}) hamdeck.Button { + label, haveLabel := hamdeck.ToString(config[ConfigLabel]) + inputTopic, haveInputTopic := hamdeck.ToString(config[ConfigInputTopic]) + outputTopic, haveOutputTopic := hamdeck.ToString(config[ConfigOutputTopic]) + onPayload, haveOnPayload := hamdeck.ToString(config[ConfigOnPayload]) + offPayload, haveOffPayload := hamdeck.ToString(config[ConfigOffPayload]) + mode, haveMode := hamdeck.ToString(config[ConfigMode]) + + if !(haveLabel && haveInputTopic && haveOutputTopic && haveOnPayload && haveOffPayload && haveMode) { + log.Print("A mqtt.Switch button must have label, inputTopic, outputTopic, onPayload, offPayload, and mode fields.") + return nil + } + + return NewSwitchButton(f.client, label, inputTopic, outputTopic, onPayload, offPayload, SwitchMode(strings.TrimSpace(strings.ToUpper(mode)))) +}