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 | |
| parent | aecc47c3f558dc3f0548d4c8e69f20ed893f5196 (diff) | |
| download | sia-server-5c7284640a6f0ddc0aa80178d5a3b91a5123b6c8.tar sia-server-5c7284640a6f0ddc0aa80178d5a3b91a5123b6c8.tar.zst sia-server-5c7284640a6f0ddc0aa80178d5a3b91a5123b6c8.zip | |
Add TP-Link HS100 support
| -rw-r--r-- | main.go | 4 | ||||
| -rw-r--r-- | meson.build | 1 | ||||
| -rw-r--r-- | tplink.go | 125 |
3 files changed, 129 insertions, 1 deletions
@@ -20,10 +20,12 @@ func main() { tx := make(chan MQTTMessage) coverMovement := NewRoute("cover/+/movement", QoS2) + plugAction := NewRoute("plug/+/action", QoS2) - go MQTTRun(config.MQTT, tx, coverMovement) + go MQTTRun(config.MQTT, tx, coverMovement, plugAction) go HomematicRun(config.Homematic, tx) go ShellyRun(config.Shelly, coverMovement) + go TPLinkRun(config.TPLink, plugAction) Await(syscall.SIGTERM, syscall.SIGINT) } diff --git a/meson.build b/meson.build index e8878ce..0738894 100644 --- a/meson.build +++ b/meson.build @@ -24,6 +24,7 @@ sia_server_linux_amd64 = custom_target( meson.current_source_dir() / 'config.go', meson.current_source_dir() / 'flags.go', meson.current_source_dir() / 'shelly.go', + meson.current_source_dir() / 'tplink.go', ], output : 'sia-server-linux-amd64', env : {'GOOS': 'linux', 'GOARCH': 'amd64'}, 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 +} |
