feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// E2 — Cloud device management example.
|
||||
// Demonstrates: get user info → home list → device list → read/write prop → do action.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"miot"
|
||||
"miot/examples/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. Load config
|
||||
cfg, err := config.LoadConfig(config.DefaultPath())
|
||||
if err != nil {
|
||||
fmt.Printf("Load config error: %v\n", err)
|
||||
return
|
||||
}
|
||||
if cfg.AuthInfo.AccessToken == "" {
|
||||
fmt.Println("请先运行 E1 cloud_oauth 获取 token")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 2. Create HTTP client
|
||||
http, err := miot.NewMIoTHttpClient(cfg.CloudServer, miot.OAUTH2_CLIENT_ID, cfg.AuthInfo.AccessToken)
|
||||
if err != nil {
|
||||
fmt.Printf("Create HTTP client error: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer http.Close()
|
||||
|
||||
// 3. Get user info
|
||||
user, err := http.GetUserInfo()
|
||||
if err != nil {
|
||||
fmt.Printf("GetUserInfo error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("=== 用户信息 ===")
|
||||
printMap(user)
|
||||
}
|
||||
|
||||
// 4. Get home list
|
||||
homes, err := http.GetHomeInfos()
|
||||
if err != nil {
|
||||
fmt.Printf("GetHomeInfos error: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("\n=== 家庭列表 ===")
|
||||
if uid, ok := homes["uid"].(string); ok {
|
||||
fmt.Printf("UID: %s\n", uid)
|
||||
}
|
||||
if homeList, ok := homes["home_list"].(map[string]interface{}); ok {
|
||||
for hid, hi := range homeList {
|
||||
if h, ok := hi.(map[string]interface{}); ok {
|
||||
fmt.Printf(" Home: %s, Name: %v\n", hid, h["home_name"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Get device list
|
||||
devices, err := http.GetDevices(nil)
|
||||
if err != nil {
|
||||
fmt.Printf("GetDevices error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
devMap, _ := devices["devices"].(map[string]map[string]interface{})
|
||||
if len(devMap) == 0 {
|
||||
fmt.Println("没有找到设备")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("\n=== 设备列表 ===")
|
||||
var firstDID string
|
||||
for did, d := range devMap {
|
||||
fmt.Printf(" DID: %s, Name: %v, Model: %v, Online: %v\n",
|
||||
did, d["name"], d["model"], d["online"])
|
||||
if firstDID == "" {
|
||||
firstDID = did
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Read a property (use hardcoded siid=2, piid=1 — power for many devices)
|
||||
siid := 2
|
||||
piid := 1
|
||||
fmt.Printf("\n=== 读取属性 (did=%s, siid=%d, piid=%d) ===\n", firstDID, siid, piid)
|
||||
prop, err := http.GetProp(firstDID, siid, piid)
|
||||
if err != nil {
|
||||
fmt.Printf("GetProp error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("属性值: %v\n", prop["value"])
|
||||
}
|
||||
|
||||
// 7. Set a property (toggle power: 0→1)
|
||||
fmt.Printf("\n=== 设置属性 (did=%s, siid=%d, piid=%d, value=false) ===\n", firstDID, siid, piid)
|
||||
setResult, err := http.SetProp([]miot.PropParam{
|
||||
{DID: firstDID, Siid: siid, Piid: piid, Value: false},
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("SetProp error: %v\n", err)
|
||||
} else {
|
||||
for _, r := range setResult {
|
||||
fmt.Printf(" DID: %s, Code: %d, Value: %v\n", r.DID, r.Code, r.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Do an action (toggle: siid=2, aiid=1)
|
||||
aiid := 1
|
||||
fmt.Printf("\n=== 执行动作 (did=%s, siid=%d, aiid=%d) ===\n", firstDID, siid, aiid)
|
||||
actionResult, err := http.DoAction(firstDID, siid, aiid, []interface{}{
|
||||
map[string]interface{}{"value": true},
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("DoAction error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("动作结果: %v\n", actionResult)
|
||||
}
|
||||
}
|
||||
|
||||
func printMap(m map[string]interface{}) {
|
||||
for k, v := range m {
|
||||
fmt.Printf(" %s: %v\n", k, v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// E1 — OAuth authorization flow example.
|
||||
// Demonstrates: generate auth URL → get code → exchange for token → refresh token.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"miot"
|
||||
"miot/examples/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. Load config (or use defaults)
|
||||
cfg, err := config.LoadConfig(config.DefaultPath())
|
||||
if err != nil {
|
||||
fmt.Printf("Load config error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure UUID is set (may be empty from old-format config)
|
||||
if cfg.UUID == "" {
|
||||
cfg.UUID = config.NewUUID()
|
||||
}
|
||||
|
||||
// 2. Create OAuth client (Go constructor appends "ha." internally)
|
||||
oauth := miot.NewMIoTOauthClient(
|
||||
miot.OAUTH2_CLIENT_ID,
|
||||
cfg.RedirectURL,
|
||||
cfg.CloudServer,
|
||||
cfg.UUID,
|
||||
)
|
||||
|
||||
// 3. Generate authorization URL
|
||||
authURL := oauth.GenAuthURL("", "", nil, false)
|
||||
fmt.Println("=== OAuth 授权 ===")
|
||||
fmt.Println("请打开以下URL完成授权:")
|
||||
fmt.Println(authURL)
|
||||
|
||||
// 4. Read authorization code from stdin
|
||||
fmt.Print("\n输入回调URL中的 code: ")
|
||||
var code string
|
||||
fmt.Scanln(&code)
|
||||
if code == "" {
|
||||
fmt.Println("未输入 code,退出")
|
||||
return
|
||||
}
|
||||
|
||||
// 5. Exchange code for access token
|
||||
result, err := oauth.GetAccessToken(code)
|
||||
if err != nil {
|
||||
fmt.Printf("获取 token 失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 6. Save tokens to config (aligns with Python auth_info structure)
|
||||
cfg.AuthInfo = config.AuthInfo{
|
||||
AccessToken: safeStr(result, "access_token"),
|
||||
RefreshToken: safeStr(result, "refresh_token"),
|
||||
MacKey: safeStr(result, "mac_key"),
|
||||
ExpiresIn: safeInt(result, "expires_in"),
|
||||
ExpiresTS: config.CalcExpiresTS(safeInt(result, "expires_in")),
|
||||
}
|
||||
|
||||
// 7. Get UID from home list (uid is in GetHomeInfos response, not token response)
|
||||
http, err := miot.NewMIoTHttpClient(cfg.CloudServer, miot.OAUTH2_CLIENT_ID, cfg.AuthInfo.AccessToken)
|
||||
if err == nil {
|
||||
defer http.Close()
|
||||
if homes, e := http.GetHomeInfos(); e == nil {
|
||||
cfg.UID = safeStr(homes, "uid")
|
||||
fmt.Printf("UID: %s\n", cfg.UID)
|
||||
}
|
||||
}
|
||||
|
||||
if err := config.SaveConfig(config.DefaultPath(), cfg); err != nil {
|
||||
fmt.Printf("保存 config 失败: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("\n=== Token 已保存 ===")
|
||||
t := cfg.AuthInfo.AccessToken
|
||||
fmt.Printf("access_token: %s...\n", t[:min(20, len(t))])
|
||||
t = cfg.AuthInfo.RefreshToken
|
||||
fmt.Printf("refresh_token: %s...\n", t[:min(20, len(t))])
|
||||
fmt.Printf("uid: %s\n", cfg.UID)
|
||||
fmt.Printf("uuid: %s\n", cfg.UUID)
|
||||
}
|
||||
|
||||
// 8. Demonstrate token refresh
|
||||
fmt.Println("\n=== 刷新 Token ===")
|
||||
newResult, err := oauth.RefreshAccessToken(cfg.AuthInfo.RefreshToken)
|
||||
if err != nil {
|
||||
fmt.Printf("刷新 token 失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
cfg.AuthInfo.AccessToken = safeStr(newResult, "access_token")
|
||||
cfg.AuthInfo.RefreshToken = safeStr(newResult, "refresh_token")
|
||||
cfg.AuthInfo.ExpiresTS = config.CalcExpiresTS(safeInt(newResult, "expires_in"))
|
||||
config.SaveConfig(config.DefaultPath(), cfg)
|
||||
fmt.Println("Token 已刷新并保存")
|
||||
}
|
||||
|
||||
func safeStr(m map[string]interface{}, key string) string {
|
||||
if v, ok := m[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return fmt.Sprintf("%.0f", n)
|
||||
case json.Number:
|
||||
return n.String()
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func safeInt(m map[string]interface{}, key string) int {
|
||||
if v, ok := m[key]; ok {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return int(n)
|
||||
case int:
|
||||
return n
|
||||
case json.Number:
|
||||
i, _ := n.Int64()
|
||||
return int(i)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"auth_info": {
|
||||
"access_token": "V3_bmQhfIrkI-AxA2Tic4l7-t6bDURwgsZnwqTGLFGwWpk94q_8eUqkx0GONdP31teHuKekfTH9SzYnFQk1aShA6_t6peafUG11897cV8wPgPnUh1aww_PvLaRLagRGyNMjj01gqWp3_jf10D4wLctL8IPI8ZkGEA_n9HgKyxH-aMKuJf9BLbicmogQHBru2bVZ",
|
||||
"refresh_token": "R3_dRDvElS486JEgbmmTOuhzLvGFMM89Chalyb9Q8p-S88EGwzC8SFdMfbNGTLrmggkJ-EPkJrNywQBWMpLGcve1MqknnhPA6S1df0R9V5xzoNC_sQoEYH4ltR_j3ymj9LSlaHJZSsC_bTfEKmY5nlQbmSTwdR0pIraQVS92IOwIUpmO4hiFOBc01RcndZNCwfpkmltHclDYj7rgUenzRMY2g",
|
||||
"mac_key": "cLY53QhwiAtvlIGeZ6_Q_FgHyaQ",
|
||||
"expires_in": 259200,
|
||||
"expires_ts": 1782811831
|
||||
},
|
||||
"uid": "2897121707",
|
||||
"cloud_server": "cn",
|
||||
"uuid": "0684e27d49b7a234ff1a4d82f8fca234",
|
||||
"redirect_url": "http://homeassistant.local:8123",
|
||||
"language": "en",
|
||||
"ctrl_mode": "auto",
|
||||
"storage_path": "",
|
||||
"nick_name": "Xiaomi"
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// Package config provides shared configuration for all MIoT examples.
|
||||
// Aligns with Python miot_client.py: entry_data + miot_storage.py: auth_info.
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"miot"
|
||||
)
|
||||
|
||||
// AuthInfo mirrors the OAuth token stored in Python miot_config/{uid}_{cloud_server}.dict.
|
||||
// Python saves this via update_user_config -> {auth_info: {...}}.
|
||||
type AuthInfo struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
MacKey string `json:"mac_key"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
ExpiresTS int64 `json:"expires_ts"`
|
||||
}
|
||||
|
||||
// Config mirrors Python entry_data (HA config_entry.data) + auth_info from storage.
|
||||
// Python splits these: entry_data is in HA config, auth_info is in miot_config/*.dict.
|
||||
// For standalone examples, we combine both into one file.
|
||||
type Config struct {
|
||||
// === auth_info (from OAuth flow, saved by E1) ===
|
||||
AuthInfo AuthInfo `json:"auth_info"`
|
||||
|
||||
// === entry_data required fields (Python MIoTClient.__init__ validates these) ===
|
||||
UID string `json:"uid"` // Xiaomi account user ID
|
||||
CloudServer string `json:"cloud_server"` // Region: cn/de/i2/ru/sg/us
|
||||
UUID string `json:"uuid"` // MQTT client ID seed (Python: SHA256 of HA instance UUID)
|
||||
RedirectURL string `json:"redirect_url"` // OAuth callback URL
|
||||
|
||||
// === entry_data optional fields ===
|
||||
Language string `json:"language"` // integration_language: en/zh-Hans/de etc.
|
||||
CtrlMode string `json:"ctrl_mode"` // auto/cloud/lan
|
||||
StoragePath string `json:"storage_path"` // data directory for miot_storage
|
||||
NickName string `json:"nick_name"` // display name in Xiaomi Home
|
||||
|
||||
// internal
|
||||
configPath string // path to this config file
|
||||
}
|
||||
|
||||
// EntryData returns the map[string]interface{} representation for NewMIoTClient.
|
||||
// Aligns with Python entry_data dict passed to MIoTClient.__init__.
|
||||
func (c *Config) EntryData() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"access_token": c.AuthInfo.AccessToken,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCtrlMode converts CtrlMode string to Go CtrlMode type.
|
||||
func (c *Config) GetCtrlMode() miot.CtrlMode {
|
||||
switch c.CtrlMode {
|
||||
case "gateway":
|
||||
return miot.CtrlModeGateway
|
||||
case "lan":
|
||||
return miot.CtrlModeLAN
|
||||
default:
|
||||
return miot.CtrlModeCloud
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfig reads a JSON config file at path.
|
||||
func LoadConfig(path string) (*Config, error) {
|
||||
if path == "" {
|
||||
return SampleConfig(), nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return SampleConfig(), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &Config{}
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.configPath = path
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// SaveConfig writes the config as JSON to path.
|
||||
func SaveConfig(path string, cfg *Config) error {
|
||||
cfg.configPath = path
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
// DefaultPath returns the shared config file path (examples/config.json).
|
||||
func DefaultPath() string {
|
||||
return "../config.json"
|
||||
}
|
||||
|
||||
// SampleConfig returns a Config with defaults, matching Python const.py defaults.
|
||||
func SampleConfig() *Config {
|
||||
return &Config{
|
||||
CloudServer: miot.DEFAULT_CLOUD_SERVER,
|
||||
RedirectURL: miot.OAUTH_REDIRECT_URL,
|
||||
Language: miot.DEFAULT_INTEGRATION_LANGUAGE,
|
||||
CtrlMode: miot.DEFAULT_CTRL_MODE,
|
||||
NickName: miot.DEFAULT_NICK_NAME,
|
||||
UUID: newUUID(),
|
||||
}
|
||||
}
|
||||
|
||||
// newUUID generates a random UUID string (Python uses SHA256 of HA instance UUID).
|
||||
func NewUUID() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
func newUUID() string {
|
||||
return NewUUID()
|
||||
}
|
||||
|
||||
// TokenValid returns true if the access token exists and hasn't expired.
|
||||
// Python: checks expires_ts > time.time().
|
||||
func (a *AuthInfo) TokenValid() bool {
|
||||
if a.AccessToken == "" {
|
||||
return false
|
||||
}
|
||||
if a.ExpiresTS > 0 {
|
||||
return time.Now().Unix() < a.ExpiresTS
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// TOKEN_EXPIRES_TS_RATIO is the safety margin applied to token expiry (Python: 0.7).
|
||||
const TOKEN_EXPIRES_TS_RATIO = 0.7
|
||||
|
||||
// CalcExpiresTS calculates expires_ts from expires_in (seconds from now, with ratio).
|
||||
// Python: int(time.time() + expires_in * TOKEN_EXPIRES_TS_RATIO).
|
||||
func CalcExpiresTS(expiresIn int) int64 {
|
||||
return time.Now().Unix() + int64(float64(expiresIn)*TOKEN_EXPIRES_TS_RATIO)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBazCCAQ+gAwIBAgIEA/UKYDAMBggqhkjOPQQDAgUAMCIxEzARBgNVBAoTCk1p
|
||||
amlhIFJvb3QxCzAJBgNVBAYTAkNOMCAXDTE2MTEyMzAxMzk0NVoYDzIwNjYxMTEx
|
||||
MDEzOTQ1WjAiMRMwEQYDVQQKEwpNaWppYSBSb290MQswCQYDVQQGEwJDTjBZMBMG
|
||||
ByqGSM49AgEGCCqGSM49AwEHA0IABL71iwLa4//4VBqgRI+6xE23xpovqPCxtv96
|
||||
2VHbZij61/Ag6jmi7oZ/3Xg/3C+whglcwoUEE6KALGJ9vccV9PmjLzAtMAwGA1Ud
|
||||
EwQFMAMBAf8wHQYDVR0OBBYEFJa3onw5sblmM6n40QmyAGDI5sURMAwGCCqGSM49
|
||||
BAMCBQADSAAwRQIgchciK9h6tZmfrP8Ka6KziQ4Lv3hKfrHtAZXMHPda4IYCIQCG
|
||||
az93ggFcbrG9u2wixjx1HKW4DUA5NXZG0wWQTpJTbQ==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBjzCCATWgAwIBAgIBATAKBggqhkjOPQQDAjAiMRMwEQYDVQQKEwpNaWppYSBS
|
||||
b290MQswCQYDVQQGEwJDTjAgFw0yMjA2MDkxNDE0MThaGA8yMDcyMDUyNzE0MTQx
|
||||
OFowLDELMAkGA1UEBhMCQ04xHTAbBgNVBAoMFE1JT1QgQ0VOVFJBTCBHQVRFV0FZ
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEdYrzbnp/0x/cZLZnuEDXTFf8mhj4
|
||||
CVpZPwgj9e9Ve5r3K7zvu8Jjj7JF1JjQYvEC6yhp1SzBgglnK4L8xQzdiqNQME4w
|
||||
HQYDVR0OBBYEFCf9+YBU7pXDs6K6CAQPRhlGJ+cuMB8GA1UdIwQYMBaAFJa3onw5
|
||||
sblmM6n40QmyAGDI5sURMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIh
|
||||
AKUv+c8v98vypkGMTzMwckGjjVqTef8xodsy6PhcSCq+AiA/n9mDs62hAo5zXyJy
|
||||
Bs1s7mqXPf1XgieoxIvs1MqyiA==
|
||||
-----END CERTIFICATE-----
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,125 @@
|
||||
// E9 — Full MIoT client integration example.
|
||||
// Demonstrates: create client → set storage/http/mips → init → refresh devices → subscribe → monitor.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"miot"
|
||||
"miot/examples/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. Load config
|
||||
cfg, err := config.LoadConfig(config.DefaultPath())
|
||||
if err != nil {
|
||||
fmt.Printf("Load config error: %v\n", err)
|
||||
return
|
||||
}
|
||||
if cfg.AuthInfo.AccessToken == "" {
|
||||
fmt.Println("请先运行 E1 cloud_oauth 获取 token")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 2. Create entry data (Python: entry_data dict)
|
||||
entryData := cfg.EntryData()
|
||||
|
||||
// 3. Create the full client
|
||||
client := miot.NewMIoTClient("demo", entryData, cfg.UID, cfg.CloudServer, cfg.GetCtrlMode())
|
||||
|
||||
// 4. Set up storage
|
||||
storage := miot.NewMIoTStorage("./data")
|
||||
client.SetStorage(storage)
|
||||
fmt.Println("Storage 已配置")
|
||||
|
||||
// 5. Set up HTTP client
|
||||
http, err := miot.NewMIoTHttpClient(cfg.CloudServer, miot.OAUTH2_CLIENT_ID, cfg.AuthInfo.AccessToken)
|
||||
if err != nil {
|
||||
fmt.Printf("Create HTTP client error: %v\n", err)
|
||||
return
|
||||
}
|
||||
client.SetHTTPClient(http)
|
||||
fmt.Println("HTTP Client 已配置")
|
||||
|
||||
// 6. Set up MIPS cloud client
|
||||
mips := miot.NewMipsCloudClient(
|
||||
"ssl://cn-ha.mqtt.io.mi.com:8883",
|
||||
"ha."+cfg.UUID,
|
||||
miot.OAUTH2_CLIENT_ID,
|
||||
cfg.AuthInfo.AccessToken,
|
||||
)
|
||||
client.SetMipsCloud(mips)
|
||||
fmt.Println("MIPS Cloud 已配置")
|
||||
|
||||
// 7. Initialize client
|
||||
if err := client.Init(); err != nil {
|
||||
fmt.Printf("Init error: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Client 已初始化")
|
||||
|
||||
// 8. Start background services
|
||||
network := miot.NewMIoTNetwork(nil, nil, 30*time.Second)
|
||||
network.Init()
|
||||
defer network.Deinit()
|
||||
|
||||
svc := miot.NewMipsService()
|
||||
svc.Init()
|
||||
defer svc.Deinit()
|
||||
|
||||
lan, err := miot.NewMIoTLan(nil, network, svc, true, 0)
|
||||
if err != nil {
|
||||
fmt.Printf("LAN init warning: %v\n", err)
|
||||
} else {
|
||||
defer lan.Deinit()
|
||||
fmt.Println("LAN 控制器已启动")
|
||||
}
|
||||
|
||||
// 9. Connect MIPS
|
||||
fmt.Println("正在连接 MIPS...")
|
||||
if err := mips.Connect(); err != nil {
|
||||
fmt.Printf("MIPS connect warning: %v\n", err)
|
||||
} else {
|
||||
defer mips.Disconnect()
|
||||
fmt.Println("MIPS 已连接")
|
||||
}
|
||||
|
||||
// 10. Refresh device list
|
||||
fmt.Println("\n刷新设备列表...")
|
||||
if err := client.RefreshDevices(); err != nil {
|
||||
fmt.Printf("RefreshDevices warning: %v\n", err)
|
||||
} else {
|
||||
devices := client.DeviceList()
|
||||
fmt.Printf("设备数量: %d\n", len(devices))
|
||||
for did, info := range devices {
|
||||
fmt.Printf(" %s: %v (online=%v)\n", did, info["name"], info["online"])
|
||||
}
|
||||
|
||||
// Subscribe to first device's state
|
||||
for did := range devices {
|
||||
client.SubDeviceState(did, func(did string, state miot.MIoTDeviceState, ctx interface{}) {
|
||||
fmt.Printf("[设备状态] DID=%s, State=%s\n", did, state)
|
||||
})
|
||||
// Subscribe to all properties for the first device
|
||||
client.SubProp(did, func(params map[string]interface{}, ctx interface{}) {
|
||||
fmt.Printf("[属性] did=%v, siid=%v, piid=%v, value=%v\n",
|
||||
params["did"], params["siid"], params["piid"], params["value"])
|
||||
}, 0, 0)
|
||||
break // only first device for demo
|
||||
}
|
||||
}
|
||||
|
||||
// 11. Wait for Ctrl+C
|
||||
fmt.Println("\n等待 Ctrl+C 退出...")
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, os.Interrupt)
|
||||
<-sigCh
|
||||
|
||||
// 12. Cleanup
|
||||
fmt.Println("\n正在清理...")
|
||||
client.Deinit()
|
||||
fmt.Println("示例结束")
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// E4 — LAN device control example.
|
||||
// Demonstrates: init network → create LAN controller → register device → wait for online → subscribe.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"miot"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. Initialize network monitor
|
||||
network := miot.NewMIoTNetwork(nil, nil, 30*time.Second)
|
||||
network.Init()
|
||||
defer network.Deinit()
|
||||
|
||||
// 3. Create MIPS service (required by MIoTLan)
|
||||
svc := miot.NewMipsService()
|
||||
svc.Init()
|
||||
defer svc.Deinit()
|
||||
|
||||
// 4. Create LAN controller
|
||||
lan, err := miot.NewMIoTLan(nil, network, svc, true, 0)
|
||||
if err != nil {
|
||||
fmt.Printf("Create LAN error: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer lan.Deinit()
|
||||
|
||||
// 5. Register devices with hardcoded values
|
||||
// Replace with your actual device DID and token (32 hex chars)
|
||||
devices := map[string]map[string]interface{}{
|
||||
"12345678": {
|
||||
"did": "12345678",
|
||||
"token": "00000000000000000000000000000000",
|
||||
},
|
||||
}
|
||||
lan.UpdateDevices(devices)
|
||||
fmt.Println("已注册设备,等待上线...")
|
||||
|
||||
// 6. Subscribe to device state
|
||||
lan.SubDeviceState("demo", func(did string, state map[string]interface{}, ctx interface{}) {
|
||||
fmt.Printf("[设备状态] DID=%s, State=%v\n", did, state)
|
||||
}, nil)
|
||||
|
||||
// 7. Subscribe to property changes (all properties)
|
||||
did := "12345678"
|
||||
lan.SubProp(did, func(params map[string]interface{}, ctx interface{}) {
|
||||
fmt.Printf("[属性变化] DID=%s, siid=%v, piid=%v, value=%v\n",
|
||||
params["did"], params["siid"], params["piid"], params["value"])
|
||||
}, 0, 0, nil)
|
||||
|
||||
// 8. Wait for device to come online
|
||||
fmt.Println("等待设备上线 (30秒)...")
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
// Print current online devices
|
||||
devList := lan.GetDevList()
|
||||
fmt.Println("\n=== 在线设备 ===")
|
||||
for did, state := range devList {
|
||||
fmt.Printf(" DID: %s, State: %v\n", did, state)
|
||||
}
|
||||
fmt.Println("示例结束")
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// E5 — mDNS service discovery example.
|
||||
// Demonstrates: create MipsService → init → discover _miot-central._tcp.local. → print results.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"miot"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. Create MIPS service browser
|
||||
svc := miot.NewMipsService()
|
||||
|
||||
// 2. Initialize mDNS browser
|
||||
svc.Init()
|
||||
defer svc.Deinit()
|
||||
fmt.Println("mDNS 服务浏览器已启动")
|
||||
|
||||
// 3. Subscribe to service changes
|
||||
svc.SubServiceChange("demo", "*", func(groupID string, state miot.MipsServiceState, data map[string]interface{}) {
|
||||
fmt.Printf("\n[服务变化] groupID=%s, state=%s\n", groupID, state)
|
||||
for k, v := range data {
|
||||
fmt.Printf(" %s: %v\n", k, v)
|
||||
}
|
||||
})
|
||||
|
||||
// 4. Wait a moment for initial discovery
|
||||
fmt.Println("正在搜索 MIoT 中央网关 (5秒)...")
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// 5. Get and print all discovered services
|
||||
services := svc.GetServices("")
|
||||
fmt.Println("\n=== 已发现的 MIoT 服务 ===")
|
||||
if len(services) == 0 {
|
||||
fmt.Println(" 未发现任何服务 (可能需要等待更长时间)")
|
||||
} else {
|
||||
for groupID, s := range services {
|
||||
fmt.Printf(" GroupID: %s\n", groupID)
|
||||
fmt.Printf(" Name: %s\n", s.Name)
|
||||
fmt.Printf(" Addresses: %v\n", s.Addresses)
|
||||
fmt.Printf(" Port: %d\n", s.Port)
|
||||
fmt.Printf(" DID: %s\n", s.DID)
|
||||
fmt.Printf(" Server: %s\n", s.Server)
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Keep browser running for more discoveries
|
||||
fmt.Println("继续监听服务变化 (30秒)...")
|
||||
time.Sleep(30 * time.Second)
|
||||
fmt.Println("示例结束")
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// E3 — MIPS cloud subscription example.
|
||||
// Demonstrates: connect to MQTT cloud broker → subscribe to device prop/event → wait for messages.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"miot"
|
||||
"miot/examples/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. Load config
|
||||
cfg, err := config.LoadConfig(config.DefaultPath())
|
||||
if err != nil {
|
||||
fmt.Printf("Load config error: %v\n", err)
|
||||
return
|
||||
}
|
||||
if cfg.AuthInfo.AccessToken == "" {
|
||||
fmt.Println("请先运行 E1 cloud_oauth 获取 token")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Need a device DID — hardcode or get from config
|
||||
did := "2053225728" // use UID as placeholder, user should replace
|
||||
|
||||
fmt.Printf("使用 DID: %s (如无效请修改)\n", did)
|
||||
|
||||
// 2. Create MIPS cloud client
|
||||
mips := miot.NewMipsCloudClient(
|
||||
"ssl://cn-ha.mqtt.io.mi.com:8883",
|
||||
"ha."+cfg.UUID,
|
||||
miot.OAUTH2_CLIENT_ID,
|
||||
cfg.AuthInfo.AccessToken,
|
||||
)
|
||||
fmt.Println("正在连接 MIPS Cloud...")
|
||||
if err := mips.Connect(); err != nil {
|
||||
fmt.Printf("MIPS Connect error: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer mips.Disconnect()
|
||||
fmt.Println("MIPS Cloud 已连接")
|
||||
|
||||
// 4. Subscribe to property changes (wildcard: all properties)
|
||||
propTopic := fmt.Sprintf("device/%s/up/properties_changed/#", did)
|
||||
fmt.Printf("订阅属性: %s\n", propTopic)
|
||||
_ = mips.Subscribe(propTopic, func(topic string, payload []byte) {
|
||||
fmt.Printf("[属性变化] topic=%s, payload=%s\n", topic, string(payload))
|
||||
})
|
||||
|
||||
// 5. Subscribe to events
|
||||
eventTopic := fmt.Sprintf("device/%s/up/event_occured/#", did)
|
||||
fmt.Printf("订阅事件: %s\n", eventTopic)
|
||||
_ = mips.Subscribe(eventTopic, func(topic string, payload []byte) {
|
||||
fmt.Printf("[事件] topic=%s, payload=%s\n", topic, string(payload))
|
||||
})
|
||||
|
||||
// 6. Subscribe to device state
|
||||
stateTopic := fmt.Sprintf("device/%s/state/#", did)
|
||||
fmt.Printf("订阅状态: %s\n", stateTopic)
|
||||
_ = mips.Subscribe(stateTopic, func(topic string, payload []byte) {
|
||||
fmt.Printf("[状态变化] topic=%s, payload=%s\n", topic, string(payload))
|
||||
})
|
||||
|
||||
// 7. Wait for messages
|
||||
fmt.Println("\n等待设备消息 (60秒)...")
|
||||
time.Sleep(60 * time.Second)
|
||||
fmt.Println("示例结束")
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// E6 — Network status monitor example.
|
||||
// Demonstrates: init network → print status/interface info → subscribe to changes.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"miot"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. Create network monitor (use defaults)
|
||||
network := miot.NewMIoTNetwork(nil, nil, 30*time.Second)
|
||||
network.Init()
|
||||
defer network.Deinit()
|
||||
|
||||
// 2. Print current network status
|
||||
fmt.Println("=== 网络状态 ===")
|
||||
if network.GetNetworkStatus() {
|
||||
fmt.Println("状态: 已连接")
|
||||
} else {
|
||||
fmt.Println("状态: 未连接")
|
||||
}
|
||||
|
||||
// 3. Print each network interface
|
||||
fmt.Println("\n=== 网络接口 ===")
|
||||
for name, info := range network.GetNetworkInfo() {
|
||||
fmt.Printf(" 接口: %s\n", name)
|
||||
fmt.Printf(" IP: %s\n", info.IP)
|
||||
fmt.Printf(" Netmask: %s\n", info.Netmask)
|
||||
fmt.Printf(" NetSeg: %s\n", info.NetSeg)
|
||||
}
|
||||
|
||||
// 4. Subscribe to network status changes
|
||||
network.SubNetworkStatus("demo", func(status bool) {
|
||||
if status {
|
||||
fmt.Println("[网络状态] 已连接")
|
||||
} else {
|
||||
fmt.Println("[网络状态] 已断开")
|
||||
}
|
||||
})
|
||||
|
||||
// 5. Subscribe to interface changes
|
||||
network.SubNetworkInfo("demo", func(status miot.InterfaceStatus, info *miot.NetworkInfo) {
|
||||
fmt.Printf("[接口变化] %s: %s (%s/%s)\n", status, info.Name, info.IP, info.Netmask)
|
||||
})
|
||||
|
||||
// 6. Wait for network events
|
||||
fmt.Println("\n监听网络变化 (30秒)...")
|
||||
time.Sleep(30 * time.Second)
|
||||
fmt.Println("示例结束")
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,107 @@
|
||||
// E8 — MIoT device spec parser example.
|
||||
// Demonstrates: parse a device URN → iterate services/properties/actions → query value lists.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"miot"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. Create storage (needed for spec caching)
|
||||
storage := miot.NewMIoTStorage("./data")
|
||||
|
||||
// 2. Create spec parser
|
||||
parser := miot.NewMIoTSpecParser("zh-Hans", storage, "specs")
|
||||
fmt.Println("Spec 解析器已创建")
|
||||
|
||||
// 3. Parse a sample device URN
|
||||
urn := "urn:miot-spec-v2:device:light:0000A001:yeelink-light1:1"
|
||||
fmt.Printf("\n解析 URN: %s\n", urn)
|
||||
|
||||
instance, err := parser.Parse(urn, "")
|
||||
if err != nil {
|
||||
fmt.Printf("Parse error: %v\n", err)
|
||||
fmt.Println("\n提示: 请先在 ./specs/ 目录下放置对应的 JSON 规格文件")
|
||||
return
|
||||
}
|
||||
|
||||
// 4. Print instance info
|
||||
fmt.Printf("\n=== 设备规格 ===\n")
|
||||
fmt.Printf("URN: %s\n", instance.URN)
|
||||
fmt.Printf("Type: %s\n", instance.Type)
|
||||
fmt.Printf("Description: %s\n", instance.DescriptionTrans)
|
||||
fmt.Printf("Services: %d\n", len(instance.Services))
|
||||
|
||||
// 5. Iterate each service
|
||||
for _, svc := range instance.Services {
|
||||
fmt.Printf("\n--- Service: %s ---\n", svc.DescriptionTrans)
|
||||
fmt.Printf(" Type: %s\n", svc.Type)
|
||||
fmt.Printf(" IID: %d\n", svc.IID)
|
||||
fmt.Printf(" Description: %s\n", svc.Description)
|
||||
|
||||
// Properties
|
||||
if len(svc.Properties) > 0 {
|
||||
fmt.Printf(" Properties (%d):\n", len(svc.Properties))
|
||||
for _, prop := range svc.Properties {
|
||||
fmt.Printf(" [%d.%d] %s\n", prop.SIID, prop.PIID, prop.DescriptionTrans)
|
||||
fmt.Printf(" Format: %s, Access: %v", prop.Format, prop.Access)
|
||||
if prop.Writable() {
|
||||
fmt.Printf(" [可写]")
|
||||
}
|
||||
if prop.Readable() {
|
||||
fmt.Printf(" [可读]")
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
if prop.ValueRange != nil {
|
||||
fmt.Printf(" Range: [%v, %v], Step: %v\n",
|
||||
prop.ValueRange.Min, prop.ValueRange.Max, prop.ValueRange.Step)
|
||||
}
|
||||
|
||||
if len(prop.ValueList) > 0 {
|
||||
fmt.Printf(" ValueList (%d items):\n", len(prop.ValueList))
|
||||
names := prop.ValueList.Names()
|
||||
for i, item := range prop.ValueList {
|
||||
fmt.Printf(" - %s: %v (%s)\n", names[i], item.Value, item.Desc)
|
||||
}
|
||||
|
||||
// Demonstrate GetValueByDescription
|
||||
if firstDesc := prop.ValueList[0].Desc; firstDesc != "" {
|
||||
v := prop.ValueList.GetValueByDescription(firstDesc)
|
||||
fmt.Printf(" GetValueByDescription(%q) = %v\n", firstDesc, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
if len(svc.Actions) > 0 {
|
||||
fmt.Printf(" Actions (%d):\n", len(svc.Actions))
|
||||
for _, act := range svc.Actions {
|
||||
fmt.Printf(" [%d.%d] %s\n", act.SIID, act.AIID, act.DescriptionTrans)
|
||||
if len(act.In) > 0 {
|
||||
fmt.Printf(" In: %v\n", act.In)
|
||||
}
|
||||
if len(act.Out) > 0 {
|
||||
fmt.Printf(" Out: %v\n", act.Out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Demonstrate ValuePrecision / ValueFormat
|
||||
if len(instance.Services) > 0 && len(instance.Services[0].Properties) > 0 {
|
||||
prop := instance.Services[0].Properties[0]
|
||||
fmt.Printf("\n=== 值转换演示 (property: %s) ===\n", prop.DescriptionTrans)
|
||||
raw := prop.ValueFormat("1")
|
||||
fmt.Printf(" ValueFormat(\"1\") = %v (type: %T)\n", raw, raw)
|
||||
prec := prop.ValuePrecision(3.14159)
|
||||
fmt.Printf(" ValuePrecision(3.14159) = %v\n", prec)
|
||||
expr := prop.EvalExpr(100.0)
|
||||
fmt.Printf(" EvalExpr(100.0) = %v\n", expr)
|
||||
}
|
||||
|
||||
fmt.Println("\n示例结束")
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// E7 — Storage and certificate management example.
|
||||
// Demonstrates: save/load data → user config → certificate operations → manufacturer lookup.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"miot"
|
||||
"miot/examples/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. Load config
|
||||
cfg, err := config.LoadConfig(config.DefaultPath())
|
||||
if err != nil {
|
||||
fmt.Printf("Load config error: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Create storage
|
||||
storage := miot.NewMIoTStorage("./data")
|
||||
fmt.Println("Storage 已创建 (root=./data)")
|
||||
|
||||
// 3. Save and load data
|
||||
fmt.Println("\n=== 读写操作 ===")
|
||||
if err := storage.Save("test", "hello", "world"); err != nil {
|
||||
fmt.Printf("Save error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Save: test/hello → world (OK)")
|
||||
}
|
||||
|
||||
val, err := storage.Load("test", "hello", "str")
|
||||
if err != nil {
|
||||
fmt.Printf("Load error: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("Load: test/hello = %v\n", val)
|
||||
}
|
||||
|
||||
// 4. User config operations
|
||||
fmt.Println("\n=== 用户配置 ===")
|
||||
configData := map[string]interface{}{"language": "zh-Hans", "theme": "dark"}
|
||||
if uid := cfg.UID; uid != "" {
|
||||
err := storage.UpdateUserConfig(uid, cfg.CloudServer, configData, false)
|
||||
if err != nil {
|
||||
fmt.Printf("UpdateUserConfig error: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("UpdateUserConfig: OK")
|
||||
}
|
||||
|
||||
loaded := storage.LoadUserConfig(uid, cfg.CloudServer, nil)
|
||||
fmt.Printf("LoadUserConfig: %v\n", loaded)
|
||||
} else {
|
||||
fmt.Println("跳过 (UID 为空)")
|
||||
}
|
||||
|
||||
// 5. Certificate operations
|
||||
fmt.Println("\n=== 证书操作 ===")
|
||||
cert, err := miot.NewMIoTCert(storage, cfg.UID, cfg.CloudServer)
|
||||
if err != nil {
|
||||
fmt.Printf("NewMIoTCert error: %v\n", err)
|
||||
} else {
|
||||
ok, err := cert.VerifyCACert()
|
||||
if err != nil {
|
||||
fmt.Printf("VerifyCACert error: %v\n", err)
|
||||
} else if ok {
|
||||
fmt.Println("CA 证书验证: 通过")
|
||||
} else {
|
||||
fmt.Println("CA 证书验证: 失败 (hash 不匹配)")
|
||||
}
|
||||
|
||||
fmt.Printf("CA 文件: %s\n", cert.CAFile())
|
||||
fmt.Printf("Key 文件: %s\n", cert.KeyFile())
|
||||
fmt.Printf("Cert 文件: %s\n", cert.CertFile())
|
||||
}
|
||||
|
||||
// 6. Device manufacturer lookup
|
||||
fmt.Println("\n=== 厂商信息 ===")
|
||||
mfg := miot.NewDeviceManufacturer(storage)
|
||||
mfg.Init()
|
||||
|
||||
manufacturers := []string{"xiaomi", "yeelink", "lumi", "chuangmi"}
|
||||
for _, m := range manufacturers {
|
||||
name := mfg.GetName(m)
|
||||
if name != m {
|
||||
fmt.Printf(" %s → %s\n", m, name)
|
||||
} else {
|
||||
fmt.Printf(" %s → (未缓存)\n", m)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\n示例结束")
|
||||
}
|
||||
Reference in New Issue
Block a user