feat: add Go API library
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
package mijia
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
checkNewMessagesURI = "/v2/message/v2/check_new_msg"
|
||||
getHomesURI = "/v2/homeroom/gethome_merged"
|
||||
getDevicesURI = "/home/home_device_list"
|
||||
getSharedDevicesURI = "/v2/home/device_list_page"
|
||||
getScenesURI = "/appgateway/miot/appsceneservice/AppSceneService/GetSimpleSceneList"
|
||||
getConsumablesURI = "/v2/home/standard_consumable_items"
|
||||
getPropertiesURI = "/miotspec/prop/get"
|
||||
setPropertiesURI = "/miotspec/prop/set"
|
||||
runActionURI = "/miotspec/action"
|
||||
)
|
||||
|
||||
var getHomesData = map[string]any{
|
||||
"fg": true, "fetch_share": true, "fetch_share_dev": true, "fetch_cariot": true,
|
||||
"limit": 300, "app_ver": 7, "plat_form": 0,
|
||||
}
|
||||
|
||||
func (client *Client) CheckNewMessages(ctx context.Context, beginAt time.Time) (json.RawMessage, error) {
|
||||
result, err := client.request(ctx, checkNewMessagesURI, map[string]int64{"begin_at": beginAt.Unix()}, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("check new messages: %w", err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (client *Client) GetHomes(ctx context.Context) ([]Home, error) {
|
||||
result, err := client.request(ctx, getHomesURI, getHomesData, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get homes: %w", err)
|
||||
}
|
||||
var response struct {
|
||||
Homes []Home `json:"homelist"`
|
||||
}
|
||||
if err := decodeJSON(result, &response); err != nil {
|
||||
return nil, fmt.Errorf("decode %s response: %w", getHomesURI, err)
|
||||
}
|
||||
return response.Homes, nil
|
||||
}
|
||||
|
||||
func (client *Client) GetDevices(ctx context.Context, homeID string) ([]Device, error) {
|
||||
homes, err := client.GetHomes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if homeID != "" {
|
||||
owner, err := homeOwner(homes, homeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.getDevices(ctx, homeID, owner)
|
||||
}
|
||||
|
||||
var devices []Device
|
||||
for _, home := range homes {
|
||||
homeDevices, err := client.getDevices(ctx, home.ID, home.UID)
|
||||
devices = append(devices, homeDevices...)
|
||||
if err != nil {
|
||||
return devices, fmt.Errorf("get devices for home %s: %w", home.ID, err)
|
||||
}
|
||||
}
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (client *Client) getDevices(ctx context.Context, homeID string, owner int64) ([]Device, error) {
|
||||
numericHomeID, err := strconv.ParseInt(homeID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("home id %q must be numeric: %w", homeID, err)
|
||||
}
|
||||
var devices []Device
|
||||
startDID := ""
|
||||
seenCursors := map[string]struct{}{startDID: {}}
|
||||
for {
|
||||
data := map[string]any{
|
||||
"home_owner": owner, "home_id": numericHomeID, "limit": 200, "start_did": startDID,
|
||||
"get_split_device": true, "support_smart_home": true, "get_cariot_device": true, "get_third_device": true,
|
||||
}
|
||||
result, err := client.request(ctx, getDevicesURI, data, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request devices for home %s: %w", homeID, err)
|
||||
}
|
||||
var page struct {
|
||||
Devices []Device `json:"device_info"`
|
||||
MaxDID string `json:"max_did"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
if err := decodeJSON(result, &page); err != nil {
|
||||
return devices, fmt.Errorf("decode %s response for home %s: %w", getDevicesURI, homeID, err)
|
||||
}
|
||||
for index := range page.Devices {
|
||||
page.Devices[index].HomeID = homeID
|
||||
}
|
||||
devices = append(devices, page.Devices...)
|
||||
if !page.HasMore {
|
||||
return devices, nil
|
||||
}
|
||||
if page.MaxDID == "" {
|
||||
return devices, fmt.Errorf("paginate %s response for home %s: has_more is true but max_did is empty", getDevicesURI, homeID)
|
||||
}
|
||||
if _, seen := seenCursors[page.MaxDID]; seen {
|
||||
return devices, fmt.Errorf("paginate %s response for home %s: max_did %q repeats or forms a cursor cycle", getDevicesURI, homeID, page.MaxDID)
|
||||
}
|
||||
seenCursors[page.MaxDID] = struct{}{}
|
||||
startDID = page.MaxDID
|
||||
}
|
||||
}
|
||||
|
||||
func (client *Client) GetSharedDevices(ctx context.Context) ([]Device, error) {
|
||||
data := map[string]any{
|
||||
"ssid": "<unknown ssid>", "bssid": "02:00:00:00:00:00", "getVirtualModel": true, "getHuamiDevices": 1,
|
||||
"get_split_device": true, "support_smart_home": true, "get_cariot_device": true, "get_third_device": true,
|
||||
"get_phone_device": true, "get_miwear_device": true,
|
||||
}
|
||||
result, err := client.request(ctx, getSharedDevicesURI, data, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get shared devices: %w", err)
|
||||
}
|
||||
var response struct {
|
||||
Devices []Device `json:"list"`
|
||||
}
|
||||
if err := decodeJSON(result, &response); err != nil {
|
||||
return nil, fmt.Errorf("decode %s response: %w", getSharedDevicesURI, err)
|
||||
}
|
||||
devices := make([]Device, 0, len(response.Devices))
|
||||
for _, device := range response.Devices {
|
||||
if device.Owner {
|
||||
device.HomeID = "shared"
|
||||
devices = append(devices, device)
|
||||
}
|
||||
}
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (client *Client) GetScenes(ctx context.Context, homeID string) ([]Scene, error) {
|
||||
homes, err := client.GetHomes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if homeID != "" {
|
||||
owner, err := homeOwner(homes, homeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.getScenes(ctx, homeID, owner)
|
||||
}
|
||||
var scenes []Scene
|
||||
for _, home := range homes {
|
||||
homeScenes, err := client.getScenes(ctx, home.ID, home.UID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get scenes for home %s: %w", home.ID, err)
|
||||
}
|
||||
scenes = append(scenes, homeScenes...)
|
||||
}
|
||||
return scenes, nil
|
||||
}
|
||||
|
||||
func (client *Client) getScenes(ctx context.Context, homeID string, owner int64) ([]Scene, error) {
|
||||
data := map[string]any{"app_version": 12, "get_type": 2, "home_id": homeID, "owner_uid": owner}
|
||||
result, err := client.request(ctx, getScenesURI, data, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request scenes for home %s: %w", homeID, err)
|
||||
}
|
||||
var response struct {
|
||||
Scenes []Scene `json:"manual_scene_info_list"`
|
||||
}
|
||||
if err := decodeJSON(result, &response); err != nil {
|
||||
return nil, fmt.Errorf("decode %s response for home %s: %w", getScenesURI, homeID, err)
|
||||
}
|
||||
for index := range response.Scenes {
|
||||
response.Scenes[index].HomeID = homeID
|
||||
}
|
||||
return response.Scenes, nil
|
||||
}
|
||||
|
||||
func (client *Client) RunScene(ctx context.Context, sceneID, homeID string) (json.RawMessage, error) {
|
||||
homes, err := client.GetHomes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
owner, err := homeOwner(homes, homeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := map[string]any{"scene_id": sceneID, "scene_type": 2, "phone_id": "null", "home_id": homeID, "owner_uid": owner}
|
||||
result, err := client.request(ctx, "/appgateway/miot/appsceneservice/AppSceneService/NewRunScene", data, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("run scene %s in home %s: %w", sceneID, homeID, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (client *Client) GetConsumables(ctx context.Context, homeID string) ([]Consumable, error) {
|
||||
homes, err := client.GetHomes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if homeID != "" {
|
||||
owner, err := homeOwner(homes, homeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.getConsumables(ctx, homeID, owner)
|
||||
}
|
||||
var consumables []Consumable
|
||||
for _, home := range homes {
|
||||
homeConsumables, err := client.getConsumables(ctx, home.ID, home.UID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get consumables for home %s: %w", home.ID, err)
|
||||
}
|
||||
consumables = append(consumables, homeConsumables...)
|
||||
}
|
||||
return consumables, nil
|
||||
}
|
||||
|
||||
func (client *Client) getConsumables(ctx context.Context, homeID string, owner int64) ([]Consumable, error) {
|
||||
numericHomeID, err := strconv.ParseInt(homeID, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("home id %q must be numeric: %w", homeID, err)
|
||||
}
|
||||
data := map[string]any{"home_id": numericHomeID, "owner_id": owner, "filter_ignore": true}
|
||||
result, err := client.request(ctx, getConsumablesURI, data, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request consumables for home %s: %w", homeID, err)
|
||||
}
|
||||
var response struct {
|
||||
Items []struct {
|
||||
Consumables []Consumable `json:"consumes_data"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := decodeJSON(result, &response); err != nil {
|
||||
return nil, fmt.Errorf("decode %s response for home %s: %w", getConsumablesURI, homeID, err)
|
||||
}
|
||||
if len(response.Items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
consumables := response.Items[0].Consumables
|
||||
for index := range consumables {
|
||||
consumables[index].HomeID = homeID
|
||||
var details []json.RawMessage
|
||||
if json.Unmarshal(consumables[index].Details, &details) == nil && len(details) == 1 {
|
||||
consumables[index].Details = details[0]
|
||||
}
|
||||
}
|
||||
return consumables, nil
|
||||
}
|
||||
|
||||
func (client *Client) GetProperties(ctx context.Context, requests []PropertyRequest) ([]PropertyResult, error) {
|
||||
if len(requests) == 0 {
|
||||
return []PropertyResult{}, nil
|
||||
}
|
||||
result, err := client.request(ctx, getPropertiesURI, map[string]any{"params": requests, "datasource": 1}, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get properties: %w", err)
|
||||
}
|
||||
var properties []PropertyResult
|
||||
if err := decodeJSON(result, &properties); err != nil {
|
||||
return nil, fmt.Errorf("decode %s response: %w", getPropertiesURI, err)
|
||||
}
|
||||
return properties, nil
|
||||
}
|
||||
|
||||
func (client *Client) SetProperties(ctx context.Context, requests []PropertySetRequest) ([]PropertyResult, error) {
|
||||
if len(requests) == 0 {
|
||||
return []PropertyResult{}, nil
|
||||
}
|
||||
result, err := client.request(ctx, setPropertiesURI, map[string]any{"params": requests}, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("set properties: %w", err)
|
||||
}
|
||||
var properties []PropertyResult
|
||||
if err := decodeJSON(result, &properties); err != nil {
|
||||
return nil, fmt.Errorf("decode %s response: %w", setPropertiesURI, err)
|
||||
}
|
||||
for index := range properties {
|
||||
properties[index].Message = resultMessage(properties[index].Code, properties[index].Message)
|
||||
}
|
||||
return properties, nil
|
||||
}
|
||||
|
||||
func (client *Client) RunActions(ctx context.Context, requests []ActionRequest) ([]ActionResult, error) {
|
||||
results := make([]ActionResult, 0, len(requests))
|
||||
for index, action := range requests {
|
||||
result, err := client.request(ctx, runActionURI, map[string]any{"params": action}, true)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("run action %d: %w", index, err)
|
||||
}
|
||||
var actionResult ActionResult
|
||||
if err := decodeJSON(result, &actionResult); err != nil {
|
||||
return results, fmt.Errorf("decode %s response for action %d: %w", runActionURI, index, err)
|
||||
}
|
||||
actionResult.Message = resultMessage(actionResult.Code, actionResult.Message)
|
||||
results = append(results, actionResult)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (client *Client) GetStatistics(ctx context.Context, requests []StatisticsRequest) ([]json.RawMessage, error) {
|
||||
results := make([]json.RawMessage, 0, len(requests))
|
||||
for index, statisticsRequest := range requests {
|
||||
result, err := client.request(ctx, "/v2/user/statistics", statisticsRequest, true)
|
||||
if err != nil {
|
||||
return results, fmt.Errorf("get statistics %d: %w", index, err)
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func homeOwner(homes []Home, homeID string) (int64, error) {
|
||||
for _, home := range homes {
|
||||
if home.ID == homeID {
|
||||
return home.UID, nil
|
||||
}
|
||||
}
|
||||
return 0, &APIError{Code: -1, Message: fmt.Sprintf("未找到 home_id=%s 的家庭信息", homeID)}
|
||||
}
|
||||
|
||||
func resultMessage(code int, message string) string {
|
||||
if code == 0 || code == 1 {
|
||||
return "成功"
|
||||
}
|
||||
if knownMessage, ok := errorCodeMessages[code]; ok {
|
||||
return knownMessage
|
||||
}
|
||||
if message != "" {
|
||||
return message
|
||||
}
|
||||
return ErrorMessage(code)
|
||||
}
|
||||
Reference in New Issue
Block a user