Files
go2rtc/pkg/tuya/api.go
T
2025-07-10 16:09:18 +02:00

285 lines
7.2 KiB
Go

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
}