install go2rtc on bob
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
package wyze
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
const (
|
||||
baseURLAuth = "https://auth-prod.api.wyze.com"
|
||||
baseURLAPI = "https://api.wyzecam.com"
|
||||
appName = "com.hualai.WyzeCam"
|
||||
appVersion = "2.50.0"
|
||||
)
|
||||
|
||||
type Cloud struct {
|
||||
client *http.Client
|
||||
apiKey string
|
||||
keyID string
|
||||
accessToken string
|
||||
phoneID string
|
||||
cameras []*Camera
|
||||
}
|
||||
|
||||
type Camera struct {
|
||||
MAC string `json:"mac"`
|
||||
P2PID string `json:"p2p_id"`
|
||||
ENR string `json:"enr"`
|
||||
IP string `json:"ip"`
|
||||
Nickname string `json:"nickname"`
|
||||
ProductModel string `json:"product_model"`
|
||||
ProductType string `json:"product_type"`
|
||||
DTLS int `json:"dtls"`
|
||||
FirmwareVer string `json:"firmware_ver"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
}
|
||||
|
||||
type deviceListResponse struct {
|
||||
Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
DeviceList []deviceInfo `json:"device_list"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type deviceInfo struct {
|
||||
MAC string `json:"mac"`
|
||||
ENR string `json:"enr"`
|
||||
Nickname string `json:"nickname"`
|
||||
ProductModel string `json:"product_model"`
|
||||
ProductType string `json:"product_type"`
|
||||
FirmwareVer string `json:"firmware_ver"`
|
||||
ConnState int `json:"conn_state"`
|
||||
DeviceParams deviceParams `json:"device_params"`
|
||||
}
|
||||
|
||||
type deviceParams struct {
|
||||
P2PID string `json:"p2p_id"`
|
||||
P2PType int `json:"p2p_type"`
|
||||
IP string `json:"ip"`
|
||||
DTLS int `json:"dtls"`
|
||||
}
|
||||
|
||||
type p2pInfoResponse struct {
|
||||
Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
|
||||
type loginResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
UserID string `json:"user_id"`
|
||||
MFAOptions []string `json:"mfa_options"`
|
||||
SMSSessionID string `json:"sms_session_id"`
|
||||
EmailSessionID string `json:"email_session_id"`
|
||||
}
|
||||
|
||||
func NewCloud(apiKey, keyID string) *Cloud {
|
||||
return &Cloud{
|
||||
client: &http.Client{Timeout: 30 * time.Second},
|
||||
phoneID: generatePhoneID(),
|
||||
apiKey: apiKey,
|
||||
keyID: keyID,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cloud) Login(email, password string) error {
|
||||
payload := map[string]string{
|
||||
"email": strings.TrimSpace(email),
|
||||
"password": hashPassword(password),
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(payload)
|
||||
|
||||
req, err := http.NewRequest("POST", baseURLAuth+"/api/user/login", strings.NewReader(string(jsonData)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Apikey", c.apiKey)
|
||||
req.Header.Set("Keyid", c.keyID)
|
||||
req.Header.Set("User-Agent", "go2rtc")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var errResp apiError
|
||||
_ = json.Unmarshal(body, &errResp)
|
||||
if errResp.hasError() {
|
||||
return fmt.Errorf("wyze: login failed (code %s): %s", errResp.code(), errResp.message())
|
||||
}
|
||||
|
||||
var result loginResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return fmt.Errorf("wyze: failed to parse login response: %w", err)
|
||||
}
|
||||
|
||||
if len(result.MFAOptions) > 0 {
|
||||
return &AuthError{
|
||||
Message: "MFA required",
|
||||
NeedsMFA: true,
|
||||
MFAType: strings.Join(result.MFAOptions, ","),
|
||||
}
|
||||
}
|
||||
|
||||
if result.AccessToken == "" {
|
||||
return errors.New("wyze: no access token in response")
|
||||
}
|
||||
|
||||
c.accessToken = result.AccessToken
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cloud) GetCameraList() ([]*Camera, error) {
|
||||
payload := map[string]any{
|
||||
"access_token": c.accessToken,
|
||||
"phone_id": c.phoneID,
|
||||
"app_name": appName,
|
||||
"app_ver": appName + "___" + appVersion,
|
||||
"app_version": appVersion,
|
||||
"phone_system_type": 1,
|
||||
"sc": "9f275790cab94a72bd206c8876429f3c",
|
||||
"sv": "9d74946e652647e9b6c9d59326aef104",
|
||||
"ts": time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(payload)
|
||||
|
||||
req, err := http.NewRequest("POST", baseURLAPI+"/app/v2/home_page/get_object_list", strings.NewReader(string(jsonData)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result deviceListResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("wyze: failed to parse device list: %w", err)
|
||||
}
|
||||
|
||||
if result.Code != "1" {
|
||||
return nil, fmt.Errorf("wyze: API error: %s - %s", result.Code, result.Msg)
|
||||
}
|
||||
|
||||
c.cameras = nil
|
||||
for _, dev := range result.Data.DeviceList {
|
||||
if dev.ProductType != "Camera" {
|
||||
continue
|
||||
}
|
||||
if dev.DeviceParams.IP == "" {
|
||||
continue // skip cameras without IP (gwell protocol)
|
||||
}
|
||||
|
||||
c.cameras = append(c.cameras, &Camera{
|
||||
MAC: dev.MAC,
|
||||
P2PID: dev.DeviceParams.P2PID,
|
||||
ENR: dev.ENR,
|
||||
IP: dev.DeviceParams.IP,
|
||||
Nickname: dev.Nickname,
|
||||
ProductModel: dev.ProductModel,
|
||||
ProductType: dev.ProductType,
|
||||
DTLS: dev.DeviceParams.DTLS,
|
||||
FirmwareVer: dev.FirmwareVer,
|
||||
IsOnline: dev.ConnState == 1,
|
||||
})
|
||||
}
|
||||
|
||||
return c.cameras, nil
|
||||
}
|
||||
|
||||
func (c *Cloud) GetCamera(id string) (*Camera, error) {
|
||||
if c.cameras == nil {
|
||||
if _, err := c.GetCameraList(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
id = strings.ToUpper(id)
|
||||
for _, cam := range c.cameras {
|
||||
if strings.ToUpper(cam.MAC) == id || strings.EqualFold(cam.Nickname, id) {
|
||||
return cam, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("wyze: camera not found: %s", id)
|
||||
}
|
||||
|
||||
func (c *Cloud) GetP2PInfo(mac string) (map[string]any, error) {
|
||||
payload := map[string]any{
|
||||
"access_token": c.accessToken,
|
||||
"phone_id": c.phoneID,
|
||||
"device_mac": mac,
|
||||
"app_name": appName,
|
||||
"app_ver": appName + "___" + appVersion,
|
||||
"app_version": appVersion,
|
||||
"phone_system_type": 1,
|
||||
"sc": "9f275790cab94a72bd206c8876429f3c",
|
||||
"sv": "9d74946e652647e9b6c9d59326aef104",
|
||||
"ts": time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(payload)
|
||||
|
||||
req, err := http.NewRequest("POST", baseURLAPI+"/app/v2/device/get_iotc_info", strings.NewReader(string(jsonData)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result p2pInfoResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if result.Code != "1" {
|
||||
return nil, fmt.Errorf("wyze: API error: %s - %s", result.Code, result.Msg)
|
||||
}
|
||||
|
||||
return result.Data, nil
|
||||
}
|
||||
|
||||
type apiError struct {
|
||||
Code string `json:"code"`
|
||||
ErrorCode int `json:"errorCode"`
|
||||
Msg string `json:"msg"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (e *apiError) hasError() bool {
|
||||
if e.Code == "1" || e.Code == "0" {
|
||||
return false
|
||||
}
|
||||
if e.Code == "" && e.ErrorCode == 0 {
|
||||
return false
|
||||
}
|
||||
return e.Code != "" || e.ErrorCode != 0
|
||||
}
|
||||
|
||||
func (e *apiError) message() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return e.Description
|
||||
}
|
||||
|
||||
func (e *apiError) code() string {
|
||||
if e.Code != "" {
|
||||
return e.Code
|
||||
}
|
||||
return fmt.Sprintf("%d", e.ErrorCode)
|
||||
}
|
||||
|
||||
type AuthError struct {
|
||||
Message string `json:"message"`
|
||||
NeedsMFA bool `json:"needs_mfa,omitempty"`
|
||||
MFAType string `json:"mfa_type,omitempty"`
|
||||
}
|
||||
|
||||
func (e *AuthError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func generatePhoneID() string {
|
||||
return core.RandString(16, 16) // 16 hex chars
|
||||
}
|
||||
|
||||
func hashPassword(password string) string {
|
||||
encoded := strings.TrimSpace(password)
|
||||
if strings.HasPrefix(strings.ToLower(encoded), "md5:") {
|
||||
return encoded[4:]
|
||||
}
|
||||
for range 3 {
|
||||
hash := md5.Sum([]byte(encoded))
|
||||
encoded = hex.EncodeToString(hash[:])
|
||||
}
|
||||
return encoded
|
||||
}
|
||||
Reference in New Issue
Block a user