339 lines
11 KiB
Go
339 lines
11 KiB
Go
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)
|
|
}
|