aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEthel Morgan <eth@ethulhu.co.uk>2020-06-19 23:50:49 +0100
committerEthel Morgan <eth@ethulhu.co.uk>2020-06-19 23:50:49 +0100
commit029f90de6895b68b5f3d1999858b09d055429679 (patch)
tree091541f5cb5d9e3d948ae69be5a5ac53aa7954c0
parentd544da3d08be66807831c03bf0f421c0addd8e9f (diff)
basic wake-on-lan actuator
Diffstat (limited to '')
-rw-r--r--.gitignore2
-rw-r--r--config/config.go66
-rw-r--r--go.mod8
-rw-r--r--go.sum8
-rw-r--r--main.go60
-rw-r--r--mqtt/mqtt.go29
-rw-r--r--wakeonlan/wakeonlan.go43
-rw-r--r--wakeonlan/wakeonlan_test.go42
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
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c4ce0c1
--- /dev/null
+++ b/go.mod
@@ -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
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..d996e7c
--- /dev/null
+++ b/go.sum
@@ -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=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..219cf8d
--- /dev/null
+++ b/main.go
@@ -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))
+ }
+ }
+}