Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
joeshaw committed Jun 21, 2020
0 parents commit 0a7a9a9
Show file tree
Hide file tree
Showing 11 changed files with 470 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.git
.gitignore
Dockerfile
LICENSE
README.md
kasa-homecontrol
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
kasa-homecontrol
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM golang:alpine as builder

RUN echo "nobody:x:1:1:nobody:/:/bin/sh" >> /etc/passwd
RUN echo "nobody:x:1:" >> /etc/group

RUN apk update && apk add --no-cache ca-certificates git

WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 go build -installsuffix 'static' -o /usr/local/bin/kasa-homecontrol .

FROM scratch

COPY --from=builder /etc/passwd /etc/group /etc/
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/local/bin/kasa-homecontrol /usr/local/bin/

USER nobody:nobody
ENTRYPOINT ["/usr/local/bin/kasa-homecontrol"]
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2020 Joe Shaw

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@

# kasa-homecontrol

Apple HomeKit support for TP-Link Kasa smart home devices using
[HomeControl](https://github.com/brutella/hc).

Devices are detected and communicated with via the local network APIs.
This module does not use the cloud APIs and does not require you to log
into the Kasa cloud service.

Currently this service only supports Kasa HS1xx Smart Plugs.

Once the device is paired with your iOS Home app, you can control it
with any service that integrates with HomeKit, including Siri ("Turn
off the Christmas tree") and Apple Watch. If you have a home hub like
an Apple TV or iPad, you can control the device remotely.

## Installing

The tool can be installed with:

go get -u github.com/joeshaw/kasa-homecontrol

Then you can run the service:

kasa-homecontrol

The service will search for Kasa devices on your local network at
startup, and every 5 seconds afterward.

To pair, open up your Home iOS app, click the + icon, choose "Add
Accessory" and then tap "Don't have a Code or Can't Scan?" You should
see the Leaf under "Nearby Accessories." Tap that and enter the PIN
00102003. You should see one entry appear for each Kasa device on
your network.

## Contributing

This code is fairly hacky, with some hardcoded values like timeouts.
It also has limited device support.

Issues and pull requests are welcome. When filing a PR, please make
sure the code has been run through `gofmt`.

## License

Copyright 2020 Joe Shaw

`kasa-homecontrol` is licensed under the MIT License. See the LICENSE
file for details.


24 changes: 24 additions & 0 deletions crypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package main

// lol, "crypto"
const initialKey = byte(0xab)

func encrypt(in []byte) []byte {
out := make([]byte, len(in))
key := initialKey
for i := 0; i < len(in); i++ {
out[i] = in[i] ^ key
key = out[i]
}
return out
}

func decrypt(in []byte) []byte {
out := make([]byte, len(in))
key := initialKey
for i := 0; i < len(in); i++ {
out[i] = in[i] ^ key
key = in[i]
}
return out
}
47 changes: 47 additions & 0 deletions device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package main

import (
"encoding/binary"
"net"
)

type Device struct {
Addr net.Addr
Name string
DeviceName string
Model string
DeviceID string
SoftwareVersion string
RelayState bool
OnTime int
}

func (d *Device) Set(on bool) error {
const (
onMsg = `{"system":{"set_relay_state":{"state":1}}}`
offMsg = `{"system":{"set_relay_state":{"state":0}}}`
)

conn, err := net.Dial("tcp4", d.Addr.String())
if err != nil {
return err
}
defer conn.Close()

msg := []byte(offMsg)
if on {
msg = []byte(onMsg)
}

if err := binary.Write(conn, binary.BigEndian, uint32(len(msg))); err != nil {
return err
}

if _, err := conn.Write(encrypt(msg)); err != nil {
return err
}

buf := make([]byte, 1024)
_, err = conn.Read(buf)
return err
}
108 changes: 108 additions & 0 deletions discovery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package main

import (
"encoding/json"
"log"
"net"
"time"
)

const discoveryMsg = `{"system":{"get_sysinfo":null},"emeter":{"get_realtime":null}}`

type discoveryResponse struct {
System struct {
GetSysinfo struct {
ErrCode int `json:"err_code"`
ErrMsg string `json:"err_msg"`

SwVer string `json:"sw_ver"`
HwVer string `json:"hw_ver"`
Model string `json:"model"`
DeviceID string `json:"deviceId"`
OemID string `json:"oemId"`
HwID string `json:"hwId"`
Rssi int `json:"rssi"`
LongitudeI int `json:"longitude_i"`
LatitudeI int `json:"latitude_i"`
Alias string `json:"alias"`
Status string `json:"status"`
MicType string `json:"mic_type"`
Feature string `json:"feature"`
Mac string `json:"mac"`
Updating int `json:"updating"`
LedOff int `json:"led_off"`
RelayState int `json:"relay_state"`
OnTime int `json:"on_time"`
ActiveMode string `json:"active_mode"`
IconHash string `json:"icon_hash"`
DevName string `json:"dev_name"`
NextAction struct {
Type int `json:"type"`
} `json:"next_action"`
} `json:"get_sysinfo"`
} `json:"system"`
Emeter struct {
ErrCode int `json:"err_code"`
ErrMsg string `json:"err_msg"`

// FIXME: I don't have an energy monitoring plug
} `json:"emeter"`
}

func discover() ([]Device, error) {
broadcastAddr, err := net.ResolveUDPAddr("udp4", "255.255.255.255:9999")
if err != nil {
return nil, err
}

conn, err := net.ListenUDP("udp4", nil)
if err != nil {
return nil, err
}

_, err = conn.WriteToUDP(encrypt([]byte(discoveryMsg)), broadcastAddr)
if err != nil {
return nil, err
}

var devices []Device

for {
conn.SetReadDeadline(time.Now().Add(1 * time.Second))

buf := make([]byte, 1024)
n, raddr, err := conn.ReadFromUDP(buf)
if err != nil {
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
return devices, nil
}

return devices, err
}

var resp discoveryResponse
if err := json.Unmarshal(decrypt(buf[:n]), &resp); err != nil {
log.Printf("Unable to parse discovery JSON response from %s", raddr)
continue
}

si := resp.System.GetSysinfo

if si.ErrCode != 0 {
log.Printf("Error response from %s: error code %d, msg: %s", raddr, si.ErrCode, si.ErrMsg)
continue
}

d := Device{
Addr: raddr,
Name: si.Alias,
DeviceName: si.DevName,
Model: si.Model,
DeviceID: si.DeviceID,
SoftwareVersion: si.SwVer,
RelayState: si.RelayState == 1,
OnTime: si.OnTime,
}
devices = append(devices, d)
}
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/joeshaw/kasa-homecontrol

go 1.14

require github.com/brutella/hc v1.2.2
29 changes: 29 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
github.com/brutella/dnssd v1.1.1 h1:Ar5ytE2Z9x5DTmuNnASlMTBpcQWQLm9ceHb326s0ykg=
github.com/brutella/dnssd v1.1.1/go.mod h1:9gIcMKQSJvYlO2x+HR50cqqjghb9IWK9hvykmyveVVs=
github.com/brutella/hc v1.2.2 h1:1idJyTuZTmxcOD+UkGEoXfoKbQjDp/7PHyh0iaDGiUU=
github.com/brutella/hc v1.2.2/go.mod h1:zknCv+aeiYM27tBXr3WFL49C8UPHMxP2IVY9c5TpMOY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0=
github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tadglines/go-pkgs v0.0.0-20140924210655-1f86682992f1 h1:ms/IQpkxq+t7hWpgKqCE5KjAUQWC24mqBrnL566SWgE=
github.com/tadglines/go-pkgs v0.0.0-20140924210655-1f86682992f1/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed h1:Gjnw8buhv4V8qXaHtAWPnKXNpCNx62heQpjO8lOY0/M=
github.com/xiam/to v0.0.0-20191116183551-8328998fc0ed/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Loading

0 comments on commit 0a7a9a9

Please sign in to comment.