commit 8d8f68957f6fa70c0f0cc719e2952b52f09b80a6 Author: The_Spider Date: Sat Dec 4 10:15:16 2021 -0600 initial commit diff --git a/cmd/tpstate/config.go b/cmd/tpstate/config.go new file mode 100644 index 0000000..a3bed43 --- /dev/null +++ b/cmd/tpstate/config.go @@ -0,0 +1,41 @@ +package main + +import ( + "os" + "time" + + "github.com/hashicorp/logutils" + "github.com/kelvins/sunrisesunset" +) + +type configStructure struct { + // time configuration + TimeFormat string `json:"time_format"` + TimeZone *time.Location `json:"time_zone"` + + // logging + LogLevel int `json:"log_level"` + Log *logutils.LevelFilter `json:"log_level_fileter"` + + //sunriseset configuration + SunRiseSet *sunrisesunset.Parameters `json:"sun_rise_set_configuration"` + + // mode of operation + CalculateDate time.Time `json:"sunrise_sunset_calculation_date"` + SecondsUntil bool `json:"seconds_until"` + NextSunriseSunset bool `json:"next_sunrise_sunset"` +} + +// Set Defaults +var config = configStructure{ + LogLevel: 50, + TimeFormat: "2006-01-02 15:04:05", + Log: &logutils.LevelFilter{ + Levels: []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARNING", "ERROR"}, + Writer: os.Stderr, + }, + SunRiseSet: &sunrisesunset.Parameters{ + Latitude: 38.749020, + Longitude: -90.521360, + }, +} diff --git a/cmd/tpstate/init.go b/cmd/tpstate/init.go new file mode 100644 index 0000000..76d3e9d --- /dev/null +++ b/cmd/tpstate/init.go @@ -0,0 +1,136 @@ +package main + +import ( + "flag" + "log" + "os" + "strconv" + "time" + + "github.com/hashicorp/logutils" +) + +func getEnvFloat64(env string, def float64) (val float64) { + osVal := os.Getenv(env) + + if osVal == "" { + return def + } + + var err error + if val, err = strconv.ParseFloat(osVal, 64); err != nil { + log.Fatalf("[ERROR] Unable to parse floating point number from environment variable (%s): %v", env, err) + } + return +} + +func getEnvInt(env string, def int) (ret int) { + val := os.Getenv(env) + + if val == "" { + return def + } + + ret, err := strconv.Atoi(val) + if err != nil { + log.Fatalf("[ERROR] Environment variable is not of numeric type: %v\n", env) + } + + return +} + +func getEnvString(env, def string) (val string) { + val = os.Getenv(env) + + if val == "" { + return def + } + + return +} + +func getEnvBool(env string, def bool) (val bool) { + osVal := os.Getenv(env) + + if osVal == "" { + return def + } + + var err error + if val, err = strconv.ParseBool(osVal); err != nil { + log.Fatalf("[ERROR] Environment variable is not of boolean type: %v\n", env) + } + + return +} + +func setLogLevel(l int) { + switch { + case l <= 20: + config.Log.SetMinLevel(logutils.LogLevel("ERROR")) + case l > 20 && l <= 40: + config.Log.SetMinLevel(logutils.LogLevel("WARNING")) + case l > 40 && l <= 60: + config.Log.SetMinLevel(logutils.LogLevel("INFO")) + case l > 60 && l <= 80: + config.Log.SetMinLevel(logutils.LogLevel("DEBUG")) + case l > 80: + config.Log.SetMinLevel(logutils.LogLevel("TRACE")) + } +} + +func initialize() { + var ( + tz string + dt string + err error + ) + + flag.IntVar(&config.LogLevel, + "log", + getEnvInt("LOG_LEVEL", 0), + "(LOG_LEVEL) Set the log verbosity.", + ) + flag.Float64Var(&config.SunRiseSet.Latitude, + "latitude", + getEnvFloat64("LATITUDE", 38.749020), + "(LATITUDE) Latitude for the sunset/sunrise calculation.", + ) + flag.Float64Var(&config.SunRiseSet.Longitude, + "longitude", + getEnvFloat64("LONGITUDE", -90.521360), + "(LONGITUDE) Longitude for the sunrise/sunset calculation.", + ) + flag.StringVar(&tz, + "timezone", + getEnvString("TIMEZONE", "America/Chicago"), + "(TIMEZONE) Timezone for the sunrise/sunset calculation.", + ) + flag.StringVar(&dt, + "date", + getEnvString("DATE", time.Now().Format(config.TimeFormat)), + "(DATE) Date to use for sunrise/sunset calculation.", + ) + flag.BoolVar(&config.NextSunriseSunset, + "next", + getEnvBool("NEXT", true), + "(NEXT) Determine and calculate the next occuring sunrise/sunset date & time.", + ) + flag.Parse() + + setLogLevel(config.LogLevel) + log.SetOutput(config.Log) + + config.CalculateDate, err = time.Parse(config.TimeFormat, dt) + if err != nil { + log.Fatalf("[ERROR] Unable to parse time: %v\n", err) + } + + config.TimeZone, err = time.LoadLocation(tz) + if err != nil { + log.Fatalf("[ERROR] Unable to parse time zone: %v\n", err) + } + + _, tzOffset := config.CalculateDate.In(config.TimeZone).Zone() + config.SunRiseSet.UtcOffset = float64(tzOffset / 60 / 60) +} diff --git a/cmd/tpstate/main.go b/cmd/tpstate/main.go new file mode 100644 index 0000000..a95e101 --- /dev/null +++ b/cmd/tpstate/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "log" +) + +func main() { + initialize() + + _, _, err := nextSunriseSunsetTime(config.CalculateDate) + if err != nil { + log.Fatalf("[ERROR] Unable to calculate sunrise/sunset: %v\n", err) + } +} diff --git a/cmd/tpstate/sunCalculations.go b/cmd/tpstate/sunCalculations.go new file mode 100644 index 0000000..4881f76 --- /dev/null +++ b/cmd/tpstate/sunCalculations.go @@ -0,0 +1,44 @@ +package main + +import ( + "log" + "time" +) + +func nextSunriseSunsetTime(t time.Time) (time.Time, time.Time, error) { + s := config.SunRiseSet + + s.Date = t + currentSR, currentSS, err := s.GetSunriseSunset() + if err != nil { + return time.Time{}, time.Time{}, err + } + + s.Date = t.Add(24 * time.Hour) + nextSR, nextSS, err := s.GetSunriseSunset() + if err != nil { + return time.Time{}, time.Time{}, err + } + + var ( + nSR time.Time + nSS time.Time + ) + + if currentSR.After(t) { + nSR = currentSR + } else { + nSR = nextSR + } + + if currentSS.After(t) { + nSS = currentSS + } else { + nSS = nextSS + } + + log.Printf("[TRACE] Next calculated sunrise: %s\n", nSR.Format("2006-01-02 15:04:05")) + log.Printf("[TRACE] Next calculated sunset : %s\n", nSS.Format("2006-01-02 15:04:05")) + + return nSR, nSS, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..69a2b1e --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module tplink + +go 1.17 + +require ( + github.com/hashicorp/logutils v1.0.0 + github.com/kelvins/sunrisesunset v0.0.0-20210220141756-39fa1bd816d5 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a93efce --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/kelvins/sunrisesunset v0.0.0-20210220141756-39fa1bd816d5 h1:ouekCqYkMw4QXFCaLyYqjBe99/MUW4Qf3DJhCRh1G18= +github.com/kelvins/sunrisesunset v0.0.0-20210220141756-39fa1bd816d5/go.mod h1:3oZ7G+fb8Z8KF+KPHxeDO3GWpEjgvk/f+d/yaxmDRT4= diff --git a/internal/tplink/tplink.go b/internal/tplink/tplink.go new file mode 100644 index 0000000..4518291 --- /dev/null +++ b/internal/tplink/tplink.go @@ -0,0 +1,249 @@ +package tplink + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "net" + "time" + + "encoding/binary" + "encoding/json" +) + +// This is the key by which all bytes sent/received from tp-link hardware are +// obfuscated. +const hashKey byte = 0xAB + +// SysInfo A type used to return information from tplink devices +type SysInfo struct { + System struct { + GetSysinfo struct { + SwVer string `json:"sw_ver"` + HwVer string `json:"hw_ver"` + Type string `json:"type"` + Model string `json:"model"` + Mac string `json:"mac"` + DevName string `json:"dev_name"` + Alias string `json:"alias"` + RelayState int `json:"relay_state"` + OnTime int `json:"on_time"` + ActiveMode string `json:"active_mode"` + Feature string `json:"feature"` + Updating int `json:"updating"` + IconHash string `json:"icon_hash"` + Rssi int `json:"rssi"` + LedOff int `json:"led_off"` + LongitudeI int `json:"longitude_i"` + LatitudeI int `json:"latitude_i"` + HwID string `json:"hwId"` + FwID string `json:"fwId"` + DeviceID string `json:"deviceId"` + OemID string `json:"oemId"` + Children []struct { + ID string `json:"id"` + State int `json:"state"` + Alias string `json:"alias"` + OnTime int `json:"on_time"` + NextAction struct { + Type int `json:"type"` + } `json:"next_action"` + } `json:"children"` + ChildNum int `json:"child_num"` + NtcState int `json:"ntc_state"` + NextAction struct { + Type int `json:"type"` + } `json:"next_action"` + ErrCode int `json:"err_code"` + MicType string `json:"mic_type"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + } `json:"get_sysinfo"` + } `json:"system"` +} + +// Tplink Device host identification +type Tplink struct { + Host string + SwitchID int +} + +type getSysInfo struct { + System struct { + SysInfo struct { + } `json:"get_sysinfo"` + } `json:"system"` +} + +type changeState struct { + System struct { + SetRelayState struct { + State int `json:"state"` + } `json:"set_relay_state"` + } `json:"system"` +} + +type changeStateMultiSwitch struct { + Context struct { + ChildIds []string `json:"child_ids"` + } `json:"context"` + System struct { + SetRelayState struct { + State int `json:"state"` + } `json:"set_relay_state"` + } `json:"system"` +} + +func encrypt(plaintext string) []byte { + n := len(plaintext) + buf := new(bytes.Buffer) + if err := binary.Write(buf, binary.BigEndian, uint32(n)); err != nil { + log.Printf("[WARNING] tplink.go: Unable to write to buffer %v\n", err) + } + ciphertext := buf.Bytes() + + key := hashKey + payload := make([]byte, n) + for i := 0; i < n; i++ { + payload[i] = plaintext[i] ^ key + key = payload[i] + } + + for i := 0; i < len(payload); i++ { + ciphertext = append(ciphertext, payload[i]) + } + + return ciphertext +} + +func decrypt(ciphertext []byte) string { + n := len(ciphertext) + key := hashKey + var nextKey byte + for i := 0; i < n; i++ { + nextKey = ciphertext[i] + ciphertext[i] ^= key + key = nextKey + } + return string(ciphertext) +} + +func send(host string, dataSend []byte) ([]byte, error) { + var header = make([]byte, 4) + + // establish connection (two second timeout) + conn, err := net.DialTimeout("tcp", host+":9999", time.Second*2) //nolint:gomnd + if err != nil { + return []byte{}, err + } + defer func() { + if err := conn.Close(); err != nil { //nolint:govet + log.Fatalf("[ERROR] tplink.go: Unable to close connection: %v\n", err) + } + }() + + // submit data to device + writer := bufio.NewWriter(conn) + _, err = writer.Write(dataSend) + if err != nil { + return []byte{}, err + } + if err := writer.Flush(); err != nil { //nolint:govet + return []byte{}, err + } + + // read response header to determine response size + headerReader := io.LimitReader(conn, int64(4)) //nolint:gomnd + _, err = headerReader.Read(header) + if err != nil { + return []byte{}, err + } + + // read response + respSize := int64(binary.BigEndian.Uint32(header)) + respReader := io.LimitReader(conn, respSize) + var response = make([]byte, respSize) + _, err = respReader.Read(response) + if err != nil { + return []byte{}, err + } + + return response, nil +} + +func getDevID(s *Tplink) (string, error) { + info, err := s.SystemInfo() + if err != nil { + return "", err + } + return info.System.GetSysinfo.DeviceID, nil +} + +// SystemInfo Returns information from targeted device +func (s *Tplink) SystemInfo() (SysInfo, error) { + var ( + payload getSysInfo + jsonResp SysInfo + ) + + j, _ := json.Marshal(payload) + + data := encrypt(string(j)) + resp, err := send(s.Host, data) + if err != nil { + return jsonResp, err + } + + if err := json.Unmarshal([]byte(decrypt(resp)), &jsonResp); err != nil { + return jsonResp, err + } + return jsonResp, nil +} + +// ChangeState changes the power state of a single port device +// True = on +// False = off +func (s *Tplink) ChangeState(state bool) error { + var payload changeState + + if state { + payload.System.SetRelayState.State = 1 + } else { + payload.System.SetRelayState.State = 0 + } + + j, _ := json.Marshal(payload) + data := encrypt(string(j)) + if _, err := send(s.Host, data); err != nil { + return err + } + return nil +} + +// ChangeStateMultiSwitch changes the power state of a device on with multiple outlets/switches +// True = on +// False = off +func (s *Tplink) ChangeStateMultiSwitch(state bool) error { + var payload changeStateMultiSwitch + + devID, err := getDevID(s) + if err != nil { + return err + } + + payload.Context.ChildIds = append(payload.Context.ChildIds, devID+fmt.Sprintf("%02d", s.SwitchID)) + if state { + payload.System.SetRelayState.State = 1 + } else { + payload.System.SetRelayState.State = 0 + } + + j, _ := json.Marshal(payload) + data := encrypt(string(j)) + if _, err := send(s.Host, data); err != nil { + return err + } + return nil +}