commit 836a60480d7d6aef2b589e3e5b1ab5ebbb3ea83f Author: joris Date: Thu Nov 28 22:44:31 2024 +0100 Add library 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) +}