package tuya import ( "bytes" "crypto/md5" "encoding/json" "fmt" "io" "net/http" "strconv" "time" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" "github.com/google/uuid" pionWebrtc "github.com/pion/webrtc/v4" ) type TuyaClient struct { httpClient *http.Client mqtt *TuyaMQTT apiURL string sessionID string clientID string deviceID string accessToken string refreshToken string secret string expireTime int64 uid string motoID string auth string iceServers []pionWebrtc.ICEServer } type Token struct { UID string `json:"uid"` AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpireTime int64 `json:"expire_time"` } type AudioAttributes struct { CallMode []int `json:"call_mode"` HardwareCapability []int `json:"hardware_capability"` } type OpenApiICE struct { Urls string `json:"urls"` Username string `json:"username"` Credential string `json:"credential"` TTL int `json:"ttl"` } type WebICE struct { Urls string `json:"urls"` Username string `json:"username,omitempty"` Credential string `json:"credential,omitempty"` } type P2PConfig struct { Ices []OpenApiICE `json:"ices"` } type WebRTConfig struct { AudioAttributes AudioAttributes `json:"audio_attributes"` Auth string `json:"auth"` ID string `json:"id"` MotoID string `json:"moto_id"` P2PConfig P2PConfig `json:"p2p_config"` Skill string `json:"skill"` SupportsWebRTC bool `json:"supports_webrtc"` VideoClaritiy int `json:"video_clarity"` } type TokenResponse struct { Result Token `json:"result"` } type WebRTCConfigResponse struct { Result WebRTConfig `json:"result"` } type OpenIoTHubConfigRequest struct { UID string `json:"uid"` UniqueID string `json:"unique_id"` LinkType string `json:"link_type"` Topics string `json:"topics"` } type OpenIoTHubConfigResponse struct { Success bool `json:"success"` Result OpenIoTHubConfig `json:"result"` } type OpenIoTHubConfig struct { Url string `json:"url"` ClientID string `json:"client_id"` Username string `json:"username"` Password string `json:"password"` SinkTopic struct { IPC string `json:"ipc"` } `json:"sink_topic"` SourceSink struct { IPC string `json:"ipc"` } `json:"source_topic"` ExpireTime int `json:"expire_time"` } const ( defaultTimeout = 5 * time.Second ) func NewTuyaClient(openAPIURL string, deviceID string, uid string, clientID string, secret string) (*TuyaClient, error) { client := &TuyaClient{ httpClient: &http.Client{Timeout: defaultTimeout}, mqtt: &TuyaMQTT{waiter: core.Waiter{}}, apiURL: openAPIURL, sessionID: core.RandString(6, 62), clientID: clientID, deviceID: deviceID, secret: secret, uid: uid, } if err := client.InitToken(); err != nil { return nil, fmt.Errorf("failed to initialize token: %w", err) } if err := client.InitDevice(); err != nil { return nil, fmt.Errorf("failed to initialize device: %w", err) } if err := client.StartMQTT(); err != nil { return nil, fmt.Errorf("failed to start MQTT: %w", err) } return client, nil } func (c *TuyaClient) Close() { c.StopMQTT() c.httpClient.CloseIdleConnections() } func(c *TuyaClient) Request(method string, url string, body any) ([]byte, error) { var bodyReader io.Reader if body != nil { jsonBody, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("failed to marshal request body: %w", err) } bodyReader = bytes.NewReader(jsonBody) } req, err := http.NewRequest(method, url, bodyReader) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } ts := time.Now().UnixNano() / 1000000 sign := c.calBusinessSign(ts) req.Header.Set("Accept", "*") req.Header.Set("Content-Type", "application/json") req.Header.Set("Access-Control-Allow-Origin", "*") req.Header.Set("Access-Control-Allow-Methods", "*") req.Header.Set("Access-Control-Allow-Headers", "*") req.Header.Set("mode", "no-cors") req.Header.Set("client_id", c.clientID) req.Header.Set("access_token", c.accessToken) req.Header.Set("sign", sign) req.Header.Set("t", strconv.FormatInt(ts, 10)) response, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } defer response.Body.Close() res, err := io.ReadAll(response.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } if response.StatusCode != http.StatusOK { return nil, fmt.Errorf("request failed with status code %d: %s", response.StatusCode, string(res)) } return res, nil } func(c *TuyaClient) InitToken() (err error) { url := fmt.Sprintf("https://%s/v1.0/token?grant_type=1", c.apiURL) c.accessToken = "" c.refreshToken = "" body, err := c.Request("GET", url, nil) if err != nil { return fmt.Errorf("failed to get token: %w", err) } var tokenResponse TokenResponse err = json.Unmarshal(body, &tokenResponse) if err != nil { return fmt.Errorf("failed to unmarshal token response: %w", err) } c.accessToken = tokenResponse.Result.AccessToken c.refreshToken = tokenResponse.Result.RefreshToken c.expireTime = tokenResponse.Result.ExpireTime return nil } func(c *TuyaClient) InitDevice() (err error) { url := fmt.Sprintf("https://%s/v1.0/users/%s/devices/%s/webrtc-configs", c.apiURL, c.uid, c.deviceID) body, err := c.Request("GET", url, nil) if err != nil { return fmt.Errorf("failed to get webrtc-configs: %w", err) } var webRTCConfigResponse WebRTCConfigResponse err = json.Unmarshal(body, &webRTCConfigResponse) if err != nil { return fmt.Errorf("failed to unmarshal webrtc-configs response: %w", err) } c.motoID = webRTCConfigResponse.Result.MotoID c.auth = webRTCConfigResponse.Result.Auth iceServersBytes, err := json.Marshal(&webRTCConfigResponse.Result.P2PConfig.Ices) if err != nil { return fmt.Errorf("failed to marshal ICE servers: %w", err) } c.iceServers, err = webrtc.UnmarshalICEServers([]byte(iceServersBytes)) if err != nil { return fmt.Errorf("failed to unmarshal ICE servers: %w", err) } return nil } func(c *TuyaClient) LoadHubConfig() (config *OpenIoTHubConfig, err error) { url := fmt.Sprintf("https://%s/v2.0/open-iot-hub/access/config", c.apiURL) request := &OpenIoTHubConfigRequest{ UID: c.uid, UniqueID: uuid.New().String(), LinkType: "mqtt", Topics: "ipc", } var openIoTHubConfigResponse OpenIoTHubConfigResponse body, err := c.Request("POST", url, request) if err != nil { return nil, fmt.Errorf("failed to get OpenIoTHub config: %w", err) } err = json.Unmarshal(body, &openIoTHubConfigResponse) if err != nil { return nil, fmt.Errorf("failed to unmarshal OpenIoTHub config response: %w", err) } if !openIoTHubConfigResponse.Success { return nil, fmt.Errorf("failed to get OpenIoTHub config: %s", string(body)) } return &openIoTHubConfigResponse.Result, nil } func(c *TuyaClient) calBusinessSign(ts int64) string { data := fmt.Sprintf("%s%s%s%d", c.clientID, c.accessToken, c.secret, ts) val := md5.Sum([]byte(data)) res := fmt.Sprintf("%X", val) return res }