initial commit
This commit is contained in:
		
							
								
								
									
										41
									
								
								cmd/tpstate/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								cmd/tpstate/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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, | ||||
| 	}, | ||||
| } | ||||
							
								
								
									
										136
									
								
								cmd/tpstate/init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								cmd/tpstate/init.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| } | ||||
							
								
								
									
										14
									
								
								cmd/tpstate/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								cmd/tpstate/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										44
									
								
								cmd/tpstate/sunCalculations.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								cmd/tpstate/sunCalculations.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
							
								
								
									
										8
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| ) | ||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							| @@ -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= | ||||
							
								
								
									
										249
									
								
								internal/tplink/tplink.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										249
									
								
								internal/tplink/tplink.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user