diff --git a/config-template.json b/config-template.json new file mode 100644 index 0000000..e33e65a --- /dev/null +++ b/config-template.json @@ -0,0 +1,21 @@ +{ + "token": "token", + "defaultColor": "2700", + "dusk": { + "colorStart": "2700", + "colorEnd": "6000", + "steps": 50, + "duration": 1, + "turnOffRange": 20 + }, + "devices": [ + { + "coordinates": { + "latitude": 0, + "longitude": 0 + }, + "id": "id", + "name": "name" + } + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8419c09 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/rikimaru-39/LIFX + +go 1.14 + +require ( + github.com/gorilla/mux v1.8.0 // indirect + github.com/kelvins/sunrisesunset v0.0.0-20170601204625-14f1915ad4b4 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9a06280 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/kelvins/sunrisesunset v0.0.0-20170601204625-14f1915ad4b4 h1:8GEzGYjqXcb1PW2RFrkbsv7Gzq4v9ykbjy6lUc9nbnM= +github.com/kelvins/sunrisesunset v0.0.0-20170601204625-14f1915ad4b4/go.mod h1:3oZ7G+fb8Z8KF+KPHxeDO3GWpEjgvk/f+d/yaxmDRT4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b22fbc1 --- /dev/null +++ b/main.go @@ -0,0 +1,460 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "math/rand" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/gorilla/mux" + "github.com/kelvins/sunrisesunset" +) + +const baseURL = "https://api.lifx.com/v1/lights/" + +//LIFX base structs +type coordinates struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +type dusk struct { + ColorStart string `json:"colorStart"` + ColorEnd string `json:"colorEnd"` + Steps int `json:"steps"` + Duration int `json:"duration"` + TurnOffRange int `json:"turnOffRange"` +} + +type device struct { + Coordinates coordinates `json:"coordinates"` + ID string `json:"id"` + Name string `json:"name"` +} + +// configuration json +type config struct { + Token string `json:"token"` + DefaultColor string `json:"defaultColor"` + Dusk dusk `json:"dusk"` + Devices []device `json:"devices"` +} + +// LIFX "state" +type state struct { + Selector string `json:"selector"` + Power string `json:"power"` + Color string `json:"color"` + Brightness float32 `json:"brightness"` + Duration float32 `json:"duration"` + Fast bool `json:"fast"` +} + +// LIFX "multiple states" format (up to 50) +type states struct { + States []state `json:"states"` +} + +var myConfig config + +func main() { + r := mux.NewRouter() + r.HandleFunc("/bulb/{selector}/{action}", BulbHandler).Methods("GET") + r.HandleFunc("/test/{text}", TextHandler).Methods("GET") + http.Handle("/", r) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 30 * time.Second, + ReadTimeout: 30 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} + +func initialiseConfig(configType string) { + var myConfig config + var myDevice device + var myCoordinates coordinates + + if configType == "new" { + reader := bufio.NewReader(os.Stdin) + fmt.Print("Enter LIFX token: ") + token, _ := reader.ReadString('\n') + myConfig.Token = strings.Replace(token, "\n", "", -1) + + fmt.Print("Enter default light color (Kelvins): ") + defaultColor, _ := reader.ReadString('\n') + defaultColor = strings.Replace(defaultColor, "\n", "", -1) + _, errDefaultColor := strconv.Atoi(strings.Replace(defaultColor, "\n", "", -1)) + errorLog(errDefaultColor, "Default color must be an integer") + myConfig.DefaultColor = defaultColor + + fmt.Print("Enter dusk light color to start with (Kelvins): ") + duskColorStart, _ := reader.ReadString('\n') + _, errColorStart := strconv.Atoi(strings.Replace(duskColorStart, "\n", "", -1)) + errorLog(errColorStart, "Dusk color must be an integer") + myConfig.Dusk.ColorStart = strings.Replace(duskColorStart, "\n", "", -1) + + fmt.Print("Enter dusk light color to end with (Kelvins): ") + duskColorEnd, _ := reader.ReadString('\n') + _, errColorEnd := strconv.Atoi(strings.Replace(duskColorEnd, "\n", "", -1)) + errorLog(errColorEnd, "Dusk color must be an integer") + myConfig.Dusk.ColorEnd = strings.Replace(duskColorEnd, "\n", "", -1) + + fmt.Print("Enter dusk steps: ") + duskSteps, _ := reader.ReadString('\n') + mySteps, errSteps := strconv.Atoi(strings.Replace(duskSteps, "\n", "", -1)) + errorLog(errSteps, "Dusk stepts must be an integer") + myConfig.Dusk.Steps = mySteps + + fmt.Print("Enter dusk duration (minutes): ") + duskDuration, _ := reader.ReadString('\n') + myDuration, errDuration := strconv.Atoi(strings.Replace(duskDuration, "\n", "", -1)) + errorLog(errDuration, "Dusk duration must be an integer") + myConfig.Dusk.Duration = myDuration + + fmt.Print("Enter dusk turn off range (minutes): ") + turnOffRange, _ := reader.ReadString('\n') + myRange, errRange := strconv.Atoi(strings.Replace(turnOffRange, "\n", "", -1)) + errorLog(errRange, "Dusk stepts turn off range be an integer") + myConfig.Dusk.TurnOffRange = myRange + + myConfig.setConfig() + } else if configType == "device" { + myConfig.getConfig() + + reader := bufio.NewReader(os.Stdin) + + fmt.Print("Enter device or group name: ") + mySelector, _ := reader.ReadString('\n') + myDevice.Name = strings.Replace(mySelector, "\n", "", -1) + + fmt.Print("Enter device or group ID: ") + myID, _ := reader.ReadString('\n') + myDevice.ID = strings.Replace(myID, "\n", "", -1) + + fmt.Print("Enter device or group latitude: ") + myLat, _ := reader.ReadString('\n') + myLatitude, errLatitude := strconv.ParseFloat(strings.Replace(myLat, "\n", "", -1), 64) + errorLog(errLatitude, "Latitude must be a float") + myCoordinates.Latitude = myLatitude + + fmt.Print("Enter device or group longitude: ") + myLng, _ := reader.ReadString('\n') + myLongitude, errLongitude := strconv.ParseFloat(strings.Replace(myLng, "\n", "", -1), 64) + errorLog(errLongitude, "Longitude must be a float") + myCoordinates.Longitude = myLongitude + + myDevice.Coordinates = myCoordinates + + myConfig.Devices = append(myConfig.Devices, myDevice) + + myConfig.setConfig() + } else { + fmt.Println("Parameter not valid") + } +} + +func (myConfig *config) getConfig() { + data, err := ioutil.ReadFile("./config.json") + if err != nil { + errorLog(err, "No configuration file found") + panic(err) + } + err = json.Unmarshal(data, &myConfig) + if err != nil { + errorLog(err, "Configuration file not valid") + panic(err) + } +} + +func (myConfig *config) setConfig() { + data, err := json.Marshal(myConfig) + + err = ioutil.WriteFile("config.json", data, 0644) + if err != nil { + errorLog(err, "SetConfig error") + return + } +} + +func takeAction(action string, myDevice device) { + switch action { + case "state": + allStates := getState() + fmt.Println(allStates) + case "toggle": + toggle(myDevice.ID) + case "on": + setPower(myDevice.ID, "on", 1) + case "off": + setPower(myDevice.ID, "off", 0) + case "dusk": + startDusk(myDevice) + case "duskBasic": + startDuskBasic(myDevice) + case "duskBeta": + startDuskBeta(myDevice) + default: + fmt.Println("No action found for " + action) + } +} + +func setPower(selector string, power string, brightness float32) { + myState := state{selector, power, "keilvin:" + myConfig.DefaultColor, brightness, 0, false} + setState(myState) +} + +func toggle(selector string) { + client := &http.Client{} + + req, err := http.NewRequest("POST", baseURL+selector+"/toggle", nil) + if err != nil { + errorLog(err, "Bad 'toggle' format") + } + + req.Header.Set("Authorization", "Bearer "+myConfig.Token) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := client.Do(req) + if err != nil { + errorLog(err, "Bad 'toggle' request") + } + + defer resp.Body.Close() +} + +func startDusk(myDevice device) { + p := sunrisesunset.Parameters{ + Latitude: myDevice.Coordinates.Latitude, + Longitude: myDevice.Coordinates.Longitude, + UtcOffset: 2.0, + Date: time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC), + } + + _, sunset, err := p.GetSunriseSunset() + + if err != nil { + errorLog(err, "Start dusk error") + } + + if sunset.After(time.Now()) && sunset.Before(time.Now().Local().Add(time.Minute*time.Duration(1))) { + durationStep := float32(myConfig.Dusk.Duration * 60 / myConfig.Dusk.Steps) + myColorStart, _ := strconv.ParseInt(myConfig.Dusk.ColorStart, 10, 0) + myColorEnd, _ := strconv.ParseInt(myConfig.Dusk.ColorEnd, 10, 0) + + for n := 1; n <= myConfig.Dusk.Steps; n++ { + brightnessLevel := float32(n*2) / 100 + newState := state{myDevice.ID, "on", "kelvin:" + strconv.FormatInt(myColorStart+(myColorEnd-myColorStart)*int64(n)/int64(myConfig.Dusk.Steps), 10), brightnessLevel, durationStep, true} + setState(newState) + if n == myConfig.Dusk.Steps { + midnight := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Add(time.Hour*time.Duration(24)).Day(), 0, 0, 0, 0, time.Local) + diff := midnight.Sub(time.Now()) + seconds := int(diff.Seconds()) + durationStep = float32(seconds - myConfig.Dusk.TurnOffRange*30 + rand.Intn(myConfig.Dusk.TurnOffRange*60)) + } + time.Sleep(time.Duration(durationStep * 1000000000)) + } + + setPower(myDevice.ID, "off", 0) + } +} + +func startDuskBasic(myDevice device) { + p := sunrisesunset.Parameters{ + Latitude: myDevice.Coordinates.Latitude, + Longitude: myDevice.Coordinates.Longitude, + UtcOffset: 2.0, + Date: time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC), + } + + _, sunset, err := p.GetSunriseSunset() + + if err != nil { + errorLog(err, "Start dusk basic error") + } + + if sunset.After(time.Now()) && sunset.Before(time.Now().Local().Add(time.Minute*time.Duration(1))) { + newState := state{myDevice.ID, "on", "kelvin:" + myConfig.Dusk.ColorEnd, 1, float32(myConfig.Dusk.Duration * 60), true} + setState(newState) + } +} + +func startDuskBeta(myDevice device) { + p := sunrisesunset.Parameters{ + Latitude: myDevice.Coordinates.Latitude, + Longitude: myDevice.Coordinates.Longitude, + UtcOffset: 2.0, + Date: time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC), + } + + _, sunset, err := p.GetSunriseSunset() + + if err != nil { + errorLog(err, "Start dusk beta error") + } + + if sunset.After(time.Now()) && sunset.Before(time.Now().Local().Add(time.Minute*time.Duration(1))) { + var duskStates []state + var brightnessLevel float32 + durationStep := float32(myConfig.Dusk.Duration * 60 / myConfig.Dusk.Steps) + myColorStart, _ := strconv.ParseInt(myConfig.Dusk.ColorStart, 10, 0) + myColorEnd, _ := strconv.ParseInt(myConfig.Dusk.ColorEnd, 10, 0) + + for n := 1; n <= 8; n++ { + brightnessLevel = float32(n) / 10 + newState := state{myDevice.ID, "on", "kelvin:" + strconv.FormatInt(myColorStart+(myColorEnd-myColorStart)*int64(n)/int64(myConfig.Dusk.Steps), 10), brightnessLevel, durationStep, true} + if n == myConfig.Dusk.Steps { + midnight := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Add(time.Hour*time.Duration(24)).Day(), 0, 0, 0, 0, time.Local) + diff := midnight.Sub(time.Now()) + seconds := int(diff.Seconds()) + durationStep = float32(seconds - myConfig.Dusk.TurnOffRange*30 + rand.Intn(myConfig.Dusk.TurnOffRange*60)) + } + + duskStates = append(duskStates, newState) + } + + midnight := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Add(time.Hour*time.Duration(24)).Day(), 0, 0, 0, 0, time.Local) + diff := midnight.Sub(time.Now()) + seconds := int(diff.Seconds()) + durationStep = float32(seconds - myConfig.Dusk.TurnOffRange*30 + rand.Intn(myConfig.Dusk.TurnOffRange*60)) + + stateOn := state{myDevice.ID, "on", "kelvin:" + myConfig.Dusk.ColorEnd, brightnessLevel, durationStep, true} + duskStates = append(duskStates, stateOn) + + stateOff := state{myDevice.ID, "off", "kelvin:2700", 0, 0, true} + duskStates = append(duskStates, stateOff) + + var myStates states + myStates.States = duskStates + setStates(myDevice.ID, myStates) + } +} + +func nightWakeUp() { + wakeStart := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 2, 0, 0, 0, time.UTC) + wakeEnd := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 4, 0, 0, 0, time.UTC) + + if wakeEnd.After(time.Now()) && wakeStart.Before(time.Now().Local().Add(time.Minute*time.Duration(1))) { + fmt.Println("wake up") + } +} + +func getState() string { + req, err := http.NewRequest("GET", baseURL+"/all", nil) + if err != nil { + errorLog(err, "Bad 'set state' format") + } + + req.Header.Set("Authorization", "Bearer "+myConfig.Token) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + errorLog(err, "Bad 'set state' request") + } + + defer resp.Body.Close() + + responseData, err := ioutil.ReadAll(resp.Body) + errorLog(err, "Error with 'get state'") + + return string(responseData) +} + +func setState(myState state) { + body := strings.NewReader("power=" + myState.Power + ";color=" + fmt.Sprintf("%f", myState.Color) + ";brightness=" + fmt.Sprintf("%f", myState.Brightness) + ";duration=" + fmt.Sprintf("%f", myState.Duration) + ";fast=" + strconv.FormatBool(myState.Fast)) + + req, err := http.NewRequest("PUT", baseURL+myState.Selector+"/state", body) + if err != nil { + errorLog(err, "Bad 'set state' format") + } + + req.Header.Set("Authorization", "Bearer "+myConfig.Token) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + errorLog(err, "Bad 'set state' request") + } + + defer resp.Body.Close() +} + +//for cycles +func setStates(mySelector string, myStates states) { + jsonValue, _ := json.Marshal(myStates) + body := strings.NewReader(string(jsonValue)) + + req, err := http.NewRequest("POST", baseURL+mySelector+"/cycle", body) + if err != nil { + errorLog(err, "Bad 'set states' format") + } + + req.Header.Set("Authorization", "Bearer "+myConfig.Token) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + errorLog(err, "Bad 'set states' request") + } + + defer resp.Body.Close() +} + +func errorLog(err error, message string) { + //todo + if err != nil { + fmt.Println(message) + panic(err) + } +} + +//BulbHandler takes care of controlling a bulb or group of bulbs +func BulbHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + w.WriteHeader(http.StatusOK) + + selectorPtr := vars["selector"] + actionPtr := vars["action"] + + if _, err := os.Stat("./config.json"); os.IsNotExist(err) { + fmt.Fprintln(w, "No configuration found") + } else { + myConfig.getConfig() + + if selectorPtr == "" { + fmt.Fprintln(w, "Missing selector") + } else { + var selector device + for i := range myConfig.Devices { + if myConfig.Devices[i].Name == selectorPtr { + selector = myConfig.Devices[i] + } + } + + if selector.ID != "" { + takeAction(actionPtr, selector) + fmt.Fprintln(w, "Should be done by now...") + } else { + fmt.Fprintln(w, "Selector not found") + } + } + } +} + +//TextHandler just shows that Mux works +func TextHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + w.WriteHeader(http.StatusOK) + fmt.Fprintln(w, vars["text"]) +}