diff options
Diffstat (limited to '')
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | config/config.go | 66 | ||||
-rw-r--r-- | go.mod | 8 | ||||
-rw-r--r-- | go.sum | 8 | ||||
-rw-r--r-- | main.go | 60 | ||||
-rw-r--r-- | mqtt/mqtt.go | 29 | ||||
-rw-r--r-- | wakeonlan/wakeonlan.go | 43 | ||||
-rw-r--r-- | wakeonlan/wakeonlan_test.go | 42 |
8 files changed, 258 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d506db3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.json +/catbus-actuator-wakeonlan diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..2d19506 --- /dev/null +++ b/config/config.go @@ -0,0 +1,66 @@ +package config + +import ( + "encoding/json" + "io/ioutil" + "net" +) + +type ( + Config struct { + Broker string + + MACsByTopic map[string]net.HardwareAddr + } + + config struct { + MQTTBroker string `json:"mqttBroker"` + Devices map[string]struct { + MAC mac `json:"mac"` + Topic string `json:"topic"` + } `json:"devices"` + } + + mac struct { + net.HardwareAddr + } +) + +func ParseFile(path string) (*Config, error) { + bytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + raw := config{} + if err := json.Unmarshal(bytes, &raw); err != nil { + return nil, err + } + + return configFromConfig(raw), nil +} + +func configFromConfig(raw config) *Config { + c := &Config{ + Broker: raw.MQTTBroker, + MACsByTopic: map[string]net.HardwareAddr{}, + } + + for _, v := range raw.Devices { + c.MACsByTopic[v.Topic] = v.MAC.HardwareAddr + } + + return c +} + +func (m mac) MarshalText() ([]byte, error) { + return []byte(m.String()), nil +} +func (m *mac) UnmarshalText(raw []byte) error { + mm, err := net.ParseMAC(string(raw)) + if err != nil { + return err + } + m.HardwareAddr = mm + return nil +} @@ -0,0 +1,8 @@ +module go.eth.moe/catbus-actuator-wakeonlan + +go 1.14 + +require ( + github.com/eclipse/paho.mqtt.golang v1.2.0 + golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect +) @@ -0,0 +1,8 @@ +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= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -0,0 +1,60 @@ +package main + +import ( + "flag" + "log" + + "go.eth.moe/catbus-actuator-wakeonlan/config" + "go.eth.moe/catbus-actuator-wakeonlan/mqtt" + "go.eth.moe/catbus-actuator-wakeonlan/wakeonlan" +) + +var ( + configPath = flag.String("config-path", "", "path to config") +) + +func main() { + flag.Parse() + + if *configPath == "" { + log.Fatal("must set -config-path") + } + + config, err := config.ParseFile(*configPath) + if err != nil { + log.Fatalf("could not parse config file: %q", err) + } + + brokerOptions := mqtt.NewClientOptions() + brokerOptions.AddBroker(config.Broker) + brokerOptions.SetAutoReconnect(true) + brokerOptions.SetConnectionLostHandler(func(_ mqtt.Client, err error) { + log.Printf("disconnected from MQTT broker %s: %v", config.Broker, err) + }) + brokerOptions.SetOnConnectHandler(func(broker mqtt.Client) { + log.Printf("connected to MQTT broker %v", config.Broker) + + for topic := range config.MACsByTopic { + token := broker.Subscribe(topic, mqtt.AtLeastOnce, func(_ mqtt.Client, msg mqtt.Message) { + mac, ok := config.MACsByTopic[msg.Topic()] + if !ok { + return + } + if err := wakeonlan.Wake(mac); err != nil { + log.Printf("could not send wake-on-lan: %v") + } + }) + if err := token.Error(); err != nil { + log.Printf("could not subscribe to %q: %v", topic, err) + + } + } + }) + + broker := mqtt.NewClient(brokerOptions) + if token := broker.Connect(); token.Error() != nil { + log.Fatalf("could not connect to MQTT broker: %v", token.Error()) + } + + select {} +} diff --git a/mqtt/mqtt.go b/mqtt/mqtt.go new file mode 100644 index 0000000..7736049 --- /dev/null +++ b/mqtt/mqtt.go @@ -0,0 +1,29 @@ +// Package mqtt wraps Paho MQTT with a few quality-of-life features. +package mqtt + +import ( + mqtt "github.com/eclipse/paho.mqtt.golang" +) + +type ( + Client = mqtt.Client + Message = mqtt.Message + MessageHandler = mqtt.MessageHandler +) + +const ( + AtMostOnce byte = iota + AtLeastOnce + ExactlyOnce +) + +const ( + Retain = true +) + +func NewClientOptions() *mqtt.ClientOptions { + return mqtt.NewClientOptions() +} +func NewClient(opts *mqtt.ClientOptions) mqtt.Client { + return mqtt.NewClient(opts) +} diff --git a/wakeonlan/wakeonlan.go b/wakeonlan/wakeonlan.go new file mode 100644 index 0000000..803d27f --- /dev/null +++ b/wakeonlan/wakeonlan.go @@ -0,0 +1,43 @@ +// Package wakeonlan implements Wake-On-Lan. +package wakeonlan + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" +) + +var ( + header = []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + + broadcastAddr = &net.UDPAddr{ + IP: net.IPv4bcast, + Port: 9, + } +) + +// Wake sends the Magic Packet to wake the computer with the given MAC address. +func Wake(mac net.HardwareAddr) error { + conn, err := net.DialUDP("udp", nil, broadcastAddr) + if err != nil { + return fmt.Errorf("could not create UDP broadcast: %w", err) + } + defer conn.Close() + + payload := packet(mac) + + if _, err := conn.Write(payload); err != nil { + return fmt.Errorf("could not send Magic Packet: %w", err) + } + return nil +} + +func packet(mac net.HardwareAddr) []byte { + var buf bytes.Buffer + binary.Write(&buf, binary.BigEndian, header) + for i := 0; i < 16; i++ { + binary.Write(&buf, binary.BigEndian, mac) + } + return buf.Bytes() +} diff --git a/wakeonlan/wakeonlan_test.go b/wakeonlan/wakeonlan_test.go new file mode 100644 index 0000000..245d580 --- /dev/null +++ b/wakeonlan/wakeonlan_test.go @@ -0,0 +1,42 @@ +package wakeonlan + +import ( + "encoding/hex" + "net" + "reflect" + "testing" +) + +func TestPacket(t *testing.T) { + tests := []struct { + mac string + packet string + }{ + { + "a8:23:22:ad:be:c7", + "ffffffffffffa82322adbec7a82322adbec7a82322adbec7a82322adbec7a82322adbec7a82322adbec7a82322adbec7a82322adbec7a82322adbec7a82322adbec7a82322adbec7a82322adbec7a82322adbec7a82322adbec7a82322adbec7a82322adbec7", + }, + } + + for i, test := range tests { + mac, err := net.ParseMAC(test.mac) + if err != nil { + t.Fatalf("invalid MAC address %q: %v", test.mac, err) + } + + if len(test.packet) != 102 { + if err != nil { + t.Fatalf("invalid packet length address: found %v, want 102", len(test.packet)) + } + } + want, err := hex.DecodeString(test.packet) + if err != nil { + t.Fatalf("invalid hex string: %v", err) + } + + got := packet(mac) + if !reflect.DeepEqual(want, got) { + t.Errorf("[test %d]\nwanted %v\nlen %d\ngot %v\nlen %d", i, want, len(want), got, len(got)) + } + } +} |