Files
2026-07-18 22:40:40 +08:00

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)
}