diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..969ab85 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.drjosh.dev/josh/plugctl + +go 1.19 diff --git a/plug.go b/plug.go new file mode 100644 index 0000000..293a427 --- /dev/null +++ b/plug.go @@ -0,0 +1,76 @@ +// Package plugctl provides an API for Kasa smart home switches. +package plugctl + +import ( + "io" + "net" +) + +// Plug provides an API for a single Kasa smart home switch. +type Plug struct { + Addr net.TCPAddr +} + +type msg struct { + System *systemMsg `json:"system,omitempty"` + EMeter *eMeterMsg `json:"emeter,omitempty"` +} + +type eMeterMsg struct { + GetRealtime *getRealtimeMsg `json:"get_realtime"` +} + +type getRealtimeMsg struct{} + +type systemMsg struct { + GetSysInfo *getSysInfoMsg `json:"get_sysinfo"` + SetRelayState *setRelayStateMsg `json:"set_relay_state,omitempty"` +} + +type getSysInfoMsg struct{} + +type setRelayStateMsg struct { + State int `json:"state"` +} + +type decryptReader struct { + r io.Reader + c byte +} + +func newDecrypter(r io.Reader) *decryptReader { + return &decryptReader{ + r: r, + c: 0xab, + } +} + +func (d *decryptReader) Read(b []byte) (int, error) { + n, err := d.r.Read(b) + for i, x := range b { + b[i] = d.c ^ x + d.c = x + } + return n, err +} + +type encryptWriter struct { + w io.Writer + c byte +} + +func newEncrypter(w io.Writer) *encryptWriter { + return &encryptWriter{ + w: w, + c: 0xab, + } +} + +func (e *encryptWriter) Write(b []byte) (int, error) { + eb := make([]byte, len(b)) + for i, x := range b { + e.c ^= x + eb[i] = e.c + } + return e.w.Write(eb) +} diff --git a/plug_test.go b/plug_test.go new file mode 100644 index 0000000..110a16a --- /dev/null +++ b/plug_test.go @@ -0,0 +1,54 @@ +package plugctl + +import ( + "bytes" + "encoding/base64" + "io" + "testing" +) + +func TestDecrypt(t *testing.T) { + // on message from hs100.sh + in64 := "AAAAKtDygfiL/5r31e+UtsWg1Iv5nPCR6LfEsNGlwOLYo4HyhueT9tTu36Lfog==" + in, err := base64.StdEncoding.DecodeString(in64) + if err != nil { + t.Fatalf("base64.DecodeString(%q) error: %v", in64, err) + } + in = in[4:] + want := []byte(`{"system":{"set_relay_state":{"state":1}}}`) + dec := newDecrypter(bytes.NewReader(in)) + got, err := io.ReadAll(dec) + if err != nil { + t.Fatalf("io.ReadAll(decryptReader) error: %v", err) + } + if !bytes.Equal(got, want) { + t.Errorf("decrypt(%02x) = %q, want %q", in, got, want) + } +} + +func TestEncrypt(t *testing.T) { + // on message from hs100.sh + want64 := "AAAAKtDygfiL/5r31e+UtsWg1Iv5nPCR6LfEsNGlwOLYo4HyhueT9tTu36Lfog==" + want, err := base64.StdEncoding.DecodeString(want64) + if err != nil { + t.Fatalf("base64.DecodeString(%q) error: %v", want64, err) + } + want = want[4:] + in := []byte(`{"system":{"set_relay_state":{"state":1}}}`) + var buf bytes.Buffer + newEncrypter(&buf).Write(in) + + if got := buf.Bytes(); !bytes.Equal(got, want) { + t.Errorf("encrypt(%q) = %02x, want %02x", in, got, want) + } +} + +// func TestEncryptDecrypt(t *testing.T) { +// want := make([]byte, 64) +// if _, err := rand.Read(want); err != nil { +// t.Fatalf("rand.Read(want) error: %v", err) +// } +// if got := decrypt(encrypt(want)); !bytes.Equal(got, want) { +// t.Errorf("decrypt(encrypt(%02x)) = %02x, want %02x", want, got, want) +// } +// }