Add library

This commit is contained in:
joris 2024-11-28 22:44:31 +01:00
commit 836a60480d
8 changed files with 469 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
vendor/

58
auth.go Normal file
View File

@ -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
}

122
client.go Normal file
View File

@ -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
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.kerbertools.xyz/joris/tiko
go 1.23

0
go.sum Normal file
View File

56
history.go Normal file
View File

@ -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)
}

57
property.go Normal file
View File

@ -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
}

172
room.go Normal file
View File

@ -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)
}