diff options
| author | xengineering <me@xengineering.eu> | 2026-03-26 17:46:54 +0100 |
|---|---|---|
| committer | xengineering <me@xengineering.eu> | 2026-03-26 17:46:54 +0100 |
| commit | 5c7284640a6f0ddc0aa80178d5a3b91a5123b6c8 (patch) | |
| tree | 958f4e62fa3489b7f306e243fbf78d0776a8b48d /tplink.go | |
| parent | aecc47c3f558dc3f0548d4c8e69f20ed893f5196 (diff) | |
| download | sia-server-5c7284640a6f0ddc0aa80178d5a3b91a5123b6c8.tar sia-server-5c7284640a6f0ddc0aa80178d5a3b91a5123b6c8.tar.zst sia-server-5c7284640a6f0ddc0aa80178d5a3b91a5123b6c8.zip | |
Add TP-Link HS100 support
Diffstat (limited to 'tplink.go')
| -rw-r--r-- | tplink.go | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/tplink.go b/tplink.go new file mode 100644 index 0000000..11d8ab1 --- /dev/null +++ b/tplink.go @@ -0,0 +1,125 @@ +package main + +import ( + "context" + "encoding/binary" + "fmt" + "log" + "net" + "time" + "strings" +) + +const ( + MAX_PAYLOAD = 4294967295 // TP-Link WiFi plug protocol: max. 2^32-1 bytes + TPLink_HS100_ON = true + TPLink_HS100_OFF = false +) + +func TPLinkRun(config TPLinkConfigs, route Route) { + for message := range route.Destination { + ip, action, err := tplinkParseMessage(config, message) + if err != nil { + log.Println(err) + continue + } + + err = set(*ip, action) + if err != nil { + log.Printf("Could not send action '%v' to %v: %v", action, ip, err) + } + } +} + +func tplinkParseMessage(config TPLinkConfigs, m MQTTMessage) (ip *net.IP, action bool, err error) { + elements := strings.Split(m.Topic, "/") + + if len(elements) != 3 { + return nil, false, fmt.Errorf( + "Expected three topic levels but got %d in '%s'.", + len(elements), m.Topic, + ) + } + + if elements[0] != "plug" || elements[2] != "action" { + return nil, false, fmt.Errorf("Expected plug/<id>/action but got: %s", m.Topic) + } + + switch string(m.Payload) { + case "on": + action = TPLink_HS100_ON + case "off": + action = TPLink_HS100_OFF + default: + return nil, false, fmt.Errorf("Invalid payload '%s'.", m.Payload) + } + + id := elements[1] + + for _, c := range config { + if c.ID == id { + ip := net.ParseIP(c.IP) + return &ip, action, nil + } + } + + return nil, false, fmt.Errorf("Got message for unknown plug '%s'", id) +} + +func encrypt(data []byte) ([]byte, error) { + if len(data) > MAX_PAYLOAD { + return []byte{}, fmt.Errorf("Too many bytes to encrypt (%d > %d)!\n", + len(data), MAX_PAYLOAD) + } + length := uint32(len(data)) + + out := make([]byte, 4) + binary.BigEndian.PutUint32(out, length) + + key := byte(171) + for _, value := range data { + key = key ^ value + out = append(out, byte(key)) + } + + return out, nil +} + +func send(address string, data []byte) error { + var d net.Dialer + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + conn, err := d.DialContext(ctx, "tcp", address) + if err != nil { + return fmt.Errorf("Failed to dial: %v", err) + } + defer conn.Close() + + _, err = conn.Write(data) + if err != nil { + return fmt.Errorf("Could not write data: %v", err) + } + + return nil +} + +func set(ip net.IP, state bool) error { + cmd := "" + + switch state { + case TPLink_HS100_ON: + cmd = `{"system":{"set_relay_state":{"state":1}}}` + case TPLink_HS100_OFF: + cmd = `{"system":{"set_relay_state":{"state":0}}}` + } + + address := fmt.Sprintf("%s:9999", ip.String()) + data, err := encrypt([]byte(cmd)) + if err != nil { + return err + } + err = send(address, data) + + return err +} |
