Skip to content

Commit

Permalink
add a button that acts like a MQTT switch, e.g. to control some IoT d…
Browse files Browse the repository at this point in the history
…evices
  • Loading branch information
ftl committed Jan 8, 2022
1 parent ad7940e commit 41e0b81
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 10 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/hamdeck
/test_conf.json

/cmd/pulse.go
/cmd/hamlib.go

Expand Down
30 changes: 30 additions & 0 deletions example_conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
127 changes: 127 additions & 0 deletions pkg/mqtt/buttons.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"image"
"image/color"
"strings"

"github.com/ftl/hamdeck/pkg/hamdeck"
)
Expand Down Expand Up @@ -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
}
46 changes: 39 additions & 7 deletions pkg/mqtt/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 28 additions & 3 deletions pkg/mqtt/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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))))
}

0 comments on commit 41e0b81

Please sign in to comment.