summaryrefslogtreecommitdiff
path: root/tplink.go
blob: 11d8ab1ad6e227d62105bf8f4e72fcd64e7b1f0b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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
}