diff --git a/config.go b/config.go new file mode 100644 index 0000000..29a621e --- /dev/null +++ b/config.go @@ -0,0 +1,4 @@ +package main +var BridgeId = "" +var clientSecret = "" +var clientId = "" diff --git a/dumpjson.go b/dumpjson.go new file mode 100644 index 0000000..6b7dc99 --- /dev/null +++ b/dumpjson.go @@ -0,0 +1,61 @@ +package main + +import "encoding/json" +import "fmt" +import "io/ioutil" + +type Status struct { + ShutterStatus map[string]int32 + AirQuality map[string]int32 + Co2 map[string]int32 + Temperature map[string]int32 + Humidity map[string]int32 + Lux map[string]int32 + BatteryPercent map[string]int32 + RfStrength map[string]int32 +} + +func DumpJSON(state *State, outfile string) { + var status = new(Status) + status.AirQuality = make(map[string]int32) + status.Co2 = make(map[string]int32) + status.Temperature = make(map[string]int32) + status.Humidity = make(map[string]int32) + status.Lux = make(map[string]int32) + status.ShutterStatus = make(map[string]int32) + status.BatteryPercent = make(map[string]int32) + status.RfStrength = make(map[string]int32) + + for _, r := range state.RoomStatus { + roomName := state.NameForRoom[r.Id] + if r.Temperature != 0 { + status.AirQuality[roomName] = r.AirQuality + status.Co2[roomName] = r.Co2 + status.Temperature[roomName] = r.Temperature + status.Humidity[roomName] = r.Humidity + status.Lux[roomName] = r.Lux + } + } + + for _, m := range state.ModuleStatus { + moduleName := state.NameForModule[m.Id] + if m.Type_ == "NXO" { + status.ShutterStatus[moduleName] = m.TargetPosition + } else if m.Type_ == "NXG" { + // bridge -- noop + } else { + status.BatteryPercent[moduleName] = m.BatteryPercent + status.RfStrength[moduleName] = m.RfStrength + } + } + + jsonOut, _ := json.Marshal(status) + if outfile == "-" { + fmt.Println(string(jsonOut)) + } else { + err := ioutil.WriteFile(outfile, jsonOut, 0644) + if err != nil { + panic(err) + } + } +} diff --git a/fetchdata.go b/fetchdata.go new file mode 100644 index 0000000..190ca53 --- /dev/null +++ b/fetchdata.go @@ -0,0 +1,73 @@ +package main + +import "context" +import sw "./go-client" + +type State struct { + HomeId string + BridgeId string + Api *sw.DefaultApiService + Auth context.Context + NameForRoom map[string]string + RoomForName map[string]string + RoomForModule map[string]string + ModulesForRoom map[string][]string + NameForModule map[string]string + ModuleForName map[string]string + ModuleStatus map[string]sw.ModuleStatus + RoomStatus map[string]sw.RoomStatus +} + +func fetchData(tokenFile string) *State { + token := refreshToken(tokenFile) + + var state = &State{ + Api: sw.NewAPIClient(sw.NewConfiguration()).DefaultApi, + BridgeId: BridgeId, + Auth: context.WithValue(context.Background(), sw.ContextAccessToken, token.AccessToken), + NameForRoom: make(map[string]string), + RoomForName: make(map[string]string), + RoomForModule: make(map[string]string), + ModulesForRoom: make(map[string][]string), + NameForModule: make(map[string]string), + ModuleForName: make(map[string]string), + ModuleStatus: make(map[string]sw.ModuleStatus), + RoomStatus: make(map[string]sw.RoomStatus), + } + + r, _, err := state.Api.HomesData(state.Auth) + if err != nil { + panic(err) + } + + state.HomeId = r.Body.Homes[0].Id + + for _, r := range r.Body.Homes[0].Rooms { + state.NameForRoom[r.Id] = r.Name + state.NameForRoom[r.Name] = r.Id + for _, m := range r.Modules { + state.RoomForModule[m] = r.Id + } + state.ModulesForRoom[r.Id] = r.Modules + } + + for _, m := range r.Body.Homes[0].Modules { + state.NameForModule[m.Id] = m.Name + state.ModuleForName[m.Name] = m.Id + } + + r2, _, err := state.Api.HomeStatus(state.Auth, sw.Body{HomeId: state.HomeId}) + if err != nil { + panic(err) + } + + for _, m := range r2.Body.Home.Modules { + state.ModuleStatus[m.Id] = m + } + + for _, r := range r2.Body.Home.Rooms { + state.RoomStatus[r.Id] = r + } + + return state +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..61b85b5 --- /dev/null +++ b/main.go @@ -0,0 +1,57 @@ +package main + +import "os" +import "fmt" +import "flag" +import "strings" + +type arrayFlags []string + +func (i *arrayFlags) String() string { + return strings.Join([]string(*i), ",") +} + +func (i *arrayFlags) Set(value string) error { + *i = append(*i, value) + return nil +} + +func main() { + if len(os.Args) == 1 { + fmt.Println("usage: velux-cli []") + fmt.Println("The most commonly used commands are: ") + fmt.Println(" print Shows status") + fmt.Println(" dump Writs json into file") + fmt.Println(" moveShutters moves shutters") + return + } + + switch os.Args[1] { + case "print": + printCommand := flag.NewFlagSet("print", flag.ExitOnError) + tokenpath := printCommand.String("tokenfile", "/openhab/conf/token.json", "file with access token") + printCommand.Parse(os.Args[2:]) + state := fetchData(*tokenpath) + PrintStatus(state) + case "dump": + dumpCommand := flag.NewFlagSet("print", flag.ExitOnError) + tokenpath := dumpCommand.String("tokenfile", "/openhab/conf/token.json", "file with access token") + jsonout := dumpCommand.String("outfile", "-", "file writing output to") + dumpCommand.Parse(os.Args[2:]) + state := fetchData(*tokenpath) + DumpJSON(state, *jsonout) + case "moveShutters": + cmd := flag.NewFlagSet("moveShutter", flag.ExitOnError) + tokenpath := cmd.String("tokenfile", "/openhab/conf/token.json", "file with access token") + position := cmd.Int("pos", 0, "move shutter to position") + var shutters arrayFlags + cmd.Var(&shutters, "shutters", "which sutters to control") + cmd.Parse(os.Args[2:]) + + state := fetchData(*tokenpath) + Move(state, shutters, int32(*position)) + default: + fmt.Printf("%q is not valid command.\n", os.Args[1]) + os.Exit(2) + } +} diff --git a/move.go b/move.go new file mode 100644 index 0000000..5ae25dd --- /dev/null +++ b/move.go @@ -0,0 +1,49 @@ +package main + +import "fmt" +import "encoding/json" +import sw "./go-client" + +func Move(state *State, shutters []string, position int32) { + fmt.Printf("Moving shutters: %+v to %+v\n", shutters, position) + if len(shutters) == 0 { + return + } + + var updates []sw.ModulePercentage + for _, x := range shutters { + m := sw.ModulePercentage{ + Bridge: state.BridgeId, + Id: state.ModuleForName[x], + TargetPosition: position, + } + updates = append(updates, m) + } + + param := sw.SetState{ + Home: &sw.SetStateHome{ + Id: state.HomeId, + Modules: updates, + }, + } + + fmt.Printf("> request: %+v\n", param) + fmt.Printf("> request: %+v\n", param.Home) + fmt.Printf("> request: %+v\n", param.Home.Modules) + j, err := json.Marshal(param) + if err != nil { + panic(err) + } + fmt.Printf("> request: %+v\n", string(j)) + + response, _, err := state.Api.SetState(state.Auth, param) + if err != nil { + panic(err) + } + + j, err = json.Marshal(response) + if err != nil { + panic(err) + } + fmt.Printf("> response: %+v\n", string(j)) +} diff --git a/print.go b/print.go new file mode 100644 index 0000000..97920a9 --- /dev/null +++ b/print.go @@ -0,0 +1,23 @@ +package main + +import "fmt" + +func PrintStatus(state *State) { + for _, r := range state.RoomStatus { + if r.Temperature != 0 { + fmt.Printf( + "%s (air quality: %d / CO2: %d / Temperature: %d / Humidity: %d / Lux: %d)\n", + state.NameForRoom[r.Id], r.AirQuality, r.Co2, r.Temperature/10.0, r.Humidity, r.Lux) + } else { + fmt.Printf("%s\n", state.NameForRoom[r.Id]) + } + + for _, m := range state.ModulesForRoom[r.Id] { + if state.ModuleStatus[m].Type_ == "NXO" { + fmt.Printf(" - %d %s\n", state.ModuleStatus[m].CurrentPosition, state.NameForModule[m]) + } else { + fmt.Printf(" - %s: battery: %d%% rf strength: %d\n", state.NameForModule[m], state.ModuleStatus[m].BatteryPercent, state.ModuleStatus[m].RfStrength) + } + } + } +} diff --git a/token.go b/token.go new file mode 100644 index 0000000..3a729f2 --- /dev/null +++ b/token.go @@ -0,0 +1,109 @@ +package main + +import "encoding/json" +import "fmt" +import "log" +import "io/ioutil" +import "net/http" +import "net/url" +import "os" +import "strings" +import "time" + +var myClient = &http.Client{Timeout: 10 * time.Second} + +type Token struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + Scope []string `json:"scope"` + ExpiresIn int `json:"expires_in"` + ExpireIn int `json:"expire_in"` +} + +type TokenFile struct { + Token *Token `json:"token"` + Refreshed time.Time `json:"refreshed"` +} + +func readCacheToken(tokenFilePath string) *TokenFile { + jsonFile, err := os.Open(tokenFilePath) + if err != nil { + log.Panic(err) + } + defer jsonFile.Close() + + byteValue, _ := ioutil.ReadAll(jsonFile) + var tokenFile TokenFile + err = json.Unmarshal(byteValue, &tokenFile) + if err != nil { + log.Panic(err) + } + + return &tokenFile +} + +func writeCacheToken(tokenFilePath string, r *Token) { + file, err := json.MarshalIndent(TokenFile{ + Token: r, + Refreshed: time.Now(), + }, "", " ") + if err != nil { + log.Panic(err) + } + + err = ioutil.WriteFile(tokenFilePath, file, 0600) + if err != nil { + log.Panic(err) + } +} + +func doRefresh(refreshToken string) *Token { + reqBody := fmt.Sprintf( + "grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s", url.QueryEscape(refreshToken), clientId, clientSecret) + + url := "https://app.velux-active.com/oauth2/token" + req, err := http.NewRequest("POST", url, strings.NewReader(reqBody)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + log.Printf("token refresh: %+v", req) + + resp, err := myClient.Do(req) + if err != nil { + log.Panic(err) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Panic(err) + } + + r := new(Token) + err = json.Unmarshal(body, &r) + if err != nil { + log.Panic(err) + } + + if r.AccessToken == "" { + log.Panicf("invalid response: %s", body) + } + + return r +} + +func refreshToken(tokenFilePath string) *Token { + tokenFile := readCacheToken(tokenFilePath) + + var resultToken *Token + + expireTime := tokenFile.Refreshed.Add(time.Second * time.Duration(tokenFile.Token.ExpireIn)) + if expireTime.Before(time.Now()) { + log.Println("refreshing token") + resultToken = doRefresh(tokenFile.Token.RefreshToken) + writeCacheToken(tokenFilePath, resultToken) + } else { + log.Println("skip refreshing token") + resultToken = tokenFile.Token + } + + return resultToken +}