feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user