From 836a60480d7d6aef2b589e3e5b1ab5ebbb3ea83f Mon Sep 17 00:00:00 2001 From: joris Date: Thu, 28 Nov 2024 22:44:31 +0100 Subject: [PATCH] Add library --- .gitignore | 1 + auth.go | 58 ++++++++++++++++++ client.go | 122 +++++++++++++++++++++++++++++++++++++ go.mod | 3 + go.sum | 0 history.go | 56 +++++++++++++++++ property.go | 57 +++++++++++++++++ room.go | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 469 insertions(+) create mode 100644 .gitignore create mode 100644 auth.go create mode 100644 client.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 history.go create mode 100644 property.go create mode 100644 room.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48b8bf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..6de49e5 --- /dev/null +++ b/auth.go @@ -0,0 +1,58 @@ +package tiko + +import ( + "bytes" + "encoding/json" + "net/http" +) + +const ( + baseURL = "https://particuliers-tiko.fr/api/v3" + graphqlAPIURL = baseURL + "/graphql/" +) + +type User struct { + ID int64 `json:"id"` + Properties []Property `json:"properties"` +} + +type AuthResp struct { + LogIn struct { + User User `json:"user"` + Token string `json:"token"` + } `json:"logIn"` +} + +type ApiResp[T any] struct { + Data T `json:"data"` +} + +func (c *Client) Authenticate(username, password string) (*User, string, error) { + requestData := map[string]interface{}{ + "operationName": "LogIn", + "variables": map[string]string{ + "email": username, + "password": password, + }, + "query": "mutation LogIn($email: String!, $password: String!, $langCode: String, $retainSession: Boolean) { logIn( input: { email: $email password: $password langCode: $langCode retainSession: $retainSession}) { user { id properties { id } } token }}", + } + + // Convertir les données en JSON + requestDataJSON, err := json.Marshal(requestData) + if err != nil { + return nil, "", err + } + + // Créer la requête HTTP + req, err := http.NewRequest(http.MethodPost, graphqlAPIURL, bytes.NewBuffer(requestDataJSON)) + if err != nil { + return nil, "", err + } + + resp, err := Do[ApiResp[AuthResp]](c, req) + if err != nil { + return nil, "", err + } + + return &resp.Data.LogIn.User, resp.Data.LogIn.Token, nil +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..9edeb83 --- /dev/null +++ b/client.go @@ -0,0 +1,122 @@ +package tiko + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/cookiejar" + "net/http/httputil" +) + +var ( + ErrMissingConfiguration = errors.New("missing configuration") +) + +type Config struct { + Debug bool `json:"debug,omitempty"` + + Username string `json:"username"` + Password string `json:"password"` +} + +type Client struct { + config *Config + httpClient *http.Client + + user *User + token string +} + +func NewClient(cfg *Config) *Client { + jar, _ := cookiejar.New(&cookiejar.Options{}) + return &Client{ + config: cfg, + httpClient: &http.Client{ + Jar: jar, + }, + } +} + +func (c *Client) Debug() bool { + if c.config != nil { + return c.config.Debug + } + + return false +} + +func (c *Client) Init() error { + if c.user != nil { + return nil + } + + if c.config == nil { + return ErrMissingConfiguration + } + + var err error + + c.user, c.token, err = c.Authenticate(c.config.Username, c.config.Password) + if err != nil { + return err + } + + return nil +} + +func (c *Client) GetUser() (*User, error) { + if c.user != nil { + return c.user, nil + } + + if err := c.Init(); err != nil { + return nil, err + } + + return c.user, nil +} + +func Do[T any](c *Client, req *http.Request) (*T, error) { + // Ajouter les en-têtes à la requête + if c.token != "" { + req.Header.Set("Authorization", "token "+c.token) + } + req.Header.Set("Content-Type", "application/json") + + // Effectuer la requête HTTP + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusBadRequest { + if c.Debug() { + d, _ := httputil.DumpRequest(req, true) + fmt.Println("Request\n", string(d)) + + d, _ = httputil.DumpResponse(resp, true) + fmt.Println("Response\n", string(d)) + } + + return nil, errors.New("bad http status") + } + + // Lire le corps de la réponse + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // Décoder la réponse JSON + // var historyResponse HistoryResponse + var obj T + err = json.Unmarshal(body, &obj) + if err != nil { + return nil, err + } + + return &obj, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c98bc4c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.kerbertools.xyz/joris/tiko + +go 1.23 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/history.go b/history.go new file mode 100644 index 0000000..e945603 --- /dev/null +++ b/history.go @@ -0,0 +1,56 @@ +package tiko + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + "time" +) + +type HistoryResponse struct { + Status string `json:"status"` + Response struct { + Timestamps []float64 `json:"timestamps"` + Values []float64 `json:"values"` + Kbox string `json:"kbox"` + Version string `json:"version"` + MissingData []int `json:"missing_data"` + Control []int `json:"control"` + EcoMode []int `json:"eco_mode"` + Production []int `json:"production"` + Resolution string `json:"resolution"` + ValuesTargetTemperature []float64 `json:"values_target_temperature"` + ValuesMeasuredTemperature []float64 `json:"values_measured_temperature"` + Start int64 `json:"start"` + End int64 `json:"end"` + ValuesList []int `json:"values_list"` + } `json:"response"` +} + +func (c *Client) GetRoomHistory(propertyID, roomID int64, startDate, endDate time.Time, resolution string) (*HistoryResponse, error) { + if err := c.Init(); err != nil { + return nil, err + } + + // Convertir les dates en millisecondes UNIX + start := startDate.UnixNano() / int64(time.Millisecond) + end := endDate.UnixNano() / int64(time.Millisecond) + + // Construire l'URL avec les paramètres + path := fmt.Sprintf("%s/properties/%d/rooms/%d/history/", baseURL, propertyID, roomID) + + params := url.Values{} + params.Set("start", strconv.FormatInt(start, 10)) + params.Set("end", strconv.FormatInt(end, 10)) + params.Set("resolution", resolution) + url := fmt.Sprintf("%s?%s", path, params.Encode()) + + // Créer la requête HTTP + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, err + } + + return Do[HistoryResponse](c, req) +} diff --git a/property.go b/property.go new file mode 100644 index 0000000..3e62a65 --- /dev/null +++ b/property.go @@ -0,0 +1,57 @@ +package tiko + +import ( + "bytes" + "encoding/json" + "net/http" +) + +type Property struct { + ID int64 `json:"id"` + Mode struct { + Boost bool `json:"boost"` + Frost bool `json:"frost"` + Absence bool `json:"absence"` + DisableHeating bool `json:"disableHeating"` + } `json:"mode"` + TypeName string `json:"__typename"` + Rooms []Room +} + +type PropertyResp struct { + Property Property `json:"property"` +} + +func (c *Client) GetProperty(propertyID int64) (*Property, error) { + if err := c.Init(); err != nil { + return nil, err + } + + // Créer les données de la requête + requestData := map[string]interface{}{ + "operationName": "GET_PROPERTY_OVERVIEW_DECENTRALISED", + "variables": map[string]int64{ + "id": propertyID, + }, + "query": "query GET_PROPERTY_OVERVIEW_DECENTRALISED($id: Int!, $excludeRooms: [Int]) { property(id: $id) { id mode rooms(excludeRooms: $excludeRooms) { id name type color heaters currentTemperatureDegrees targetTemperatureDegrees humidity sensors mode { boost absence frost disableHeating __typename } status { disconnected heaterDisconnected heatingOperating sensorBatteryLow sensorDisconnected temporaryAdjustment heatersRegulated heaterCalibrationState __typename } __typename } __typename } }", + } + + // Convertir les données en JSON + requestDataJSON, err := json.Marshal(requestData) + if err != nil { + return nil, err + } + + // Créer la requête HTTP + req, err := http.NewRequest(http.MethodPost, graphqlAPIURL, bytes.NewBuffer(requestDataJSON)) + if err != nil { + return nil, err + } + + resp, err := Do[ApiResp[PropertyResp]](c, req) + if err != nil { + return nil, err + } + + return &resp.Data.Property, nil +} diff --git a/room.go b/room.go new file mode 100644 index 0000000..7cfbad6 --- /dev/null +++ b/room.go @@ -0,0 +1,172 @@ +package tiko + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "net/http" +) + +type RoomMode string + +const ( + RoomModeBoost = RoomMode("boost") + RoomModeFalse = RoomMode("false") + RoomModeAway = RoomMode("absence") + RoomModeFrost = RoomMode("frost") + RoomModeDisable = RoomMode("disableHeating") +) + +type Room struct { + ID int64 `json:"id"` + Name string `json:"name"` + Type int `json:"type"` + Color string `json:"color"` + Heaters int `json:"heaters"` + CurrentTemperature float64 `json:"currentTemperatureDegrees"` + TargetTemperature float64 `json:"targetTemperatureDegrees"` + Humidity *int `json:"humidity"` + Sensors int `json:"sensors"` + Mode struct { + Boost bool `json:"boost"` + Absence bool `json:"absence"` + Frost bool `json:"frost"` + DisableHeating bool `json:"disableHeating"` + TypeName string `json:"__typename"` + } `json:"mode"` + Status struct { + Disconnected bool `json:"disconnected"` + // HeaterDisconnected bool `json:"heaterDisconnected"` + HeatingOperating bool `json:"heatingOperating"` + SensorBatteryLow bool `json:"sensorBatteryLow"` + SensorDisconnected bool `json:"sensorDisconnected"` + // TemporaryAdjustment bool `json:"temporaryAdjustment"` + HeatersRegulated bool `json:"heatersRegulated"` + HeaterCalibrationState [][2]interface{} `json:"heaterCalibrationState"` + TypeName string `json:"__typename"` + } `json:"status"` + TypeName string `json:"__typename"` +} + +type PropertyRoom struct { + Room Room `json:"room"` +} + +type PropertyRoomResp struct { + Property PropertyRoom `json:"property"` +} + +func (c *Client) GetRoom(propertyID, roomID int64) (*Room, error) { + if err := c.Init(); err != nil { + return nil, err + } + + // Créer les données de la requête + requestData := map[string]interface{}{ + "operationName": "GET_PROPERTY_MODE_AND_ROOM", + "variables": map[string]int64{ + "propertyId": propertyID, + "roomId": roomID, + }, + "query": "query GET_PROPERTY_MODE_AND_ROOM($propertyId: Int!, $roomId: Int!) { property(id: $propertyId) { id mode room(id: $roomId) { id name type color heaters currentTemperatureDegrees targetTemperatureDegrees humidity sensors mode { boost absence frost disableHeating __typename } status { disconnected heaterDisconnected heatingOperating sensorBatteryLow sensorDisconnected temporaryAdjustment heatersRegulated heaterCalibrationState __typename } __typename } __typename } }", + } + + // Convertir les données en JSON + requestDataJSON, err := json.Marshal(requestData) + if err != nil { + return nil, err + } + + // Créer la requête HTTP + req, err := http.NewRequest(http.MethodPost, graphqlAPIURL, bytes.NewBuffer(requestDataJSON)) + if err != nil { + return nil, err + } + + resp, err := Do[ApiResp[PropertyRoomResp]](c, req) + if err != nil { + return nil, err + } + + return &resp.Data.Property.Room, nil +} + +func (c *Client) SetRoomMode(propertyID, roomID int64, mode RoomMode) (*Room, error) { + if err := c.Init(); err != nil { + return nil, err + } + + // Créer les données de la requête + requestData := map[string]interface{}{ + "operationName": "SET_ROOM_MODE", + "variables": map[string]interface{}{ + "propertyId": propertyID, + "roomId": roomID, + "mode": mode, + }, + "query": "mutation SET_ROOM_MODE($propertyId: Int!, $roomId: Int!, $mode: String!) { setRoomMode(input: {propertyId: $propertyId, roomId: $roomId, mode: $mode}) { id mode { boost absence frost disableHeating __typename } __typename } }", + } + + // Convertir les données en JSON + requestDataJSON, err := json.Marshal(requestData) + if err != nil { + return nil, err + } + + // Créer la requête HTTP + req, err := http.NewRequest(http.MethodPost, graphqlAPIURL, bytes.NewBuffer(requestDataJSON)) + if err != nil { + return nil, err + } + + resp, err := Do[map[string]interface{}](c, req) + if err != nil { + return nil, err + } + fmt.Println("SetRoomMode Resp", resp) + + return c.GetRoom(propertyID, roomID) +} + +func (c *Client) SetRoomTemperature(propertyID, roomID int64, temp float32) (*Room, error) { + if err := c.Init(); err != nil { + return nil, err + } + + if temp <= 7 || temp >= 25 { + return nil, errors.New("temp is out of bounds") + } + + // Créer les données de la requête + requestData := map[string]interface{}{ + "operationName": "SET_PROPERTY_ROOM_ADJUST_TEMPERATURE", + "variables": map[string]interface{}{ + "propertyId": propertyID, + "roomId": roomID, + "temperature": temp, + }, + "query": "mutation SET_PROPERTY_ROOM_ADJUST_TEMPERATURE($propertyId: Int!, $roomId: Int!, $temperature: Float!) { setRoomAdjustTemperature(input: {propertyId: $propertyId, roomId: $roomId, temperature: $temperature}) { id adjustTemperature { active endDateTime temperature __typename } __typename } }", + } + + // Convertir les données en JSON + requestDataJSON, err := json.Marshal(requestData) + if err != nil { + return nil, err + } + + // Créer la requête HTTP + req, err := http.NewRequest(http.MethodPost, graphqlAPIURL, bytes.NewBuffer(requestDataJSON)) + if err != nil { + return nil, err + } + + resp, err := Do[map[string]interface{}](c, req) + if err != nil { + return nil, err + } + + fmt.Println("SetRoomTemperature Resp", resp) + + return c.GetRoom(propertyID, roomID) +}