feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
// Package miot provides MIoT core client for Xiaomi Home devices.
|
||||
// It's a Golang port of MIoTClient from xiaomihome-py/miot/.
|
||||
package miot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// MIoTDevice (aligned with Python miot_device.py MIoTDevice)
|
||||
// ============================================================================
|
||||
|
||||
// MIoTDevice represents a Xiaomi device.
|
||||
// Aligns with Python miot_device.py MIoTDevice.
|
||||
// Created by the upper layer, not managed by MIoTClient.
|
||||
// Python MIoTDevice receives device_info: dict, doesn't manage property cache itself.
|
||||
type MIoTDevice struct {
|
||||
miotClient *MIoTClient
|
||||
specInstance *MIoTSpecInstance
|
||||
|
||||
// Device metadata (parsed from device_info dict, aligned with Python __init__)
|
||||
online bool
|
||||
did string
|
||||
name string
|
||||
model string
|
||||
modelStrs []string // model split by "." (aligned with Python _model_strs)
|
||||
manufacturer string
|
||||
fwVersion string
|
||||
icon string
|
||||
homeID string
|
||||
homeName string
|
||||
roomID string
|
||||
roomName string
|
||||
groupID string
|
||||
|
||||
// Network-related fields (aligned with Python cloud device_info)
|
||||
token string
|
||||
ip string
|
||||
connectType int // pid, -1 means not connected
|
||||
rssi int
|
||||
ssid string
|
||||
bssid string
|
||||
|
||||
// Sub devices (aligned with Python sub_devices)
|
||||
subDevices map[int]*MIoTDevice
|
||||
|
||||
// SPEC service instances (if loaded)
|
||||
serviceList []*MIoTSpecService
|
||||
|
||||
// Subscription lists (aligned with Python _device_state_sub_list / _value_sub_list)
|
||||
deviceStateSubList map[string]map[string]func(string, interface{})
|
||||
valueSubList map[string]map[string]func(map[string]interface{}, interface{})
|
||||
subID int
|
||||
subIDMu sync.Mutex
|
||||
}
|
||||
|
||||
// NewMIoTDevice creates a new MIoTDevice.
|
||||
// Aligns with Python MIoTDevice.__init__.
|
||||
// miotClient: reference to MIoTClient
|
||||
// deviceInfo: device metadata (map[string]interface{})
|
||||
// specInstance: SPEC instance (can be nil, lazy loaded)
|
||||
func NewMIoTDevice(miotClient *MIoTClient, deviceInfo map[string]interface{}, specInstance *MIoTSpecInstance) *MIoTDevice {
|
||||
dev := &MIoTDevice{
|
||||
miotClient: miotClient,
|
||||
specInstance: specInstance,
|
||||
deviceStateSubList: make(map[string]map[string]func(string, interface{})),
|
||||
valueSubList: make(map[string]map[string]func(map[string]interface{}, interface{})),
|
||||
subDevices: make(map[int]*MIoTDevice),
|
||||
}
|
||||
|
||||
// Parse device metadata from deviceInfo
|
||||
if did, ok := deviceInfo["did"].(string); ok {
|
||||
dev.did = did
|
||||
}
|
||||
if name, ok := deviceInfo["name"].(string); ok {
|
||||
dev.name = name
|
||||
}
|
||||
if model, ok := deviceInfo["model"].(string); ok {
|
||||
dev.model = model
|
||||
dev.modelStrs = strings.Split(model, ".")
|
||||
}
|
||||
if online, ok := deviceInfo["online"].(bool); ok {
|
||||
dev.online = online
|
||||
}
|
||||
if homeID, ok := deviceInfo["home_id"].(string); ok {
|
||||
dev.homeID = homeID
|
||||
}
|
||||
if homeName, ok := deviceInfo["home_name"].(string); ok {
|
||||
dev.homeName = homeName
|
||||
}
|
||||
if roomID, ok := deviceInfo["room_id"].(string); ok {
|
||||
dev.roomID = roomID
|
||||
}
|
||||
if roomName, ok := deviceInfo["room_name"].(string); ok {
|
||||
dev.roomName = roomName
|
||||
}
|
||||
if groupID, ok := deviceInfo["group_id"].(string); ok {
|
||||
dev.groupID = groupID
|
||||
}
|
||||
if fwVersion, ok := deviceInfo["fw_version"].(string); ok {
|
||||
dev.fwVersion = fwVersion
|
||||
}
|
||||
if manufacturer, ok := deviceInfo["manufacturer"].(string); ok {
|
||||
dev.manufacturer = manufacturer
|
||||
}
|
||||
if icon, ok := deviceInfo["icon"].(string); ok {
|
||||
dev.icon = icon
|
||||
}
|
||||
if token, ok := deviceInfo["token"].(string); ok {
|
||||
dev.token = token
|
||||
}
|
||||
if ip, ok := deviceInfo["local_ip"].(string); ok {
|
||||
dev.ip = ip
|
||||
}
|
||||
if ct, ok := deviceInfo["connect_type"].(float64); ok {
|
||||
dev.connectType = int(ct)
|
||||
}
|
||||
if rssi, ok := deviceInfo["rssi"].(float64); ok {
|
||||
dev.rssi = int(rssi)
|
||||
}
|
||||
if ssid, ok := deviceInfo["ssid"].(string); ok {
|
||||
dev.ssid = ssid
|
||||
}
|
||||
if bssid, ok := deviceInfo["bssid"].(string); ok {
|
||||
dev.bssid = bssid
|
||||
}
|
||||
|
||||
return dev
|
||||
}
|
||||
|
||||
// LoadFromCloud creates a MIoTDevice from cloud device data.
|
||||
// Aligns with Python: populating device_info from cloud response.
|
||||
func LoadFromCloud(miotClient *MIoTClient, cloudData map[string]interface{}) *MIoTDevice {
|
||||
return NewMIoTDevice(miotClient, cloudData, nil)
|
||||
}
|
||||
|
||||
// LoadSPEC loads SPEC definitions for this device.
|
||||
// Aligns with Python: MIoTDevice loads SPEC via miot_spec.
|
||||
func (d *MIoTDevice) LoadSPEC(specInstance *MIoTSpecInstance) error {
|
||||
if specInstance == nil {
|
||||
return fmt.Errorf("spec instance is nil")
|
||||
}
|
||||
d.specInstance = specInstance
|
||||
d.serviceList = specInstance.GetServices()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DID returns the device ID.
|
||||
func (d *MIoTDevice) DID() string {
|
||||
return d.did
|
||||
}
|
||||
|
||||
// Name returns the device name.
|
||||
func (d *MIoTDevice) Name() string {
|
||||
return d.name
|
||||
}
|
||||
|
||||
// Model returns the device model.
|
||||
func (d *MIoTDevice) Model() string {
|
||||
return d.model
|
||||
}
|
||||
|
||||
// Online returns the online status.
|
||||
func (d *MIoTDevice) Online() bool {
|
||||
return d.online
|
||||
}
|
||||
|
||||
// SetOnline sets the online status.
|
||||
func (d *MIoTDevice) SetOnline(online bool) {
|
||||
d.online = online
|
||||
}
|
||||
|
||||
// SpecInstance returns the SPEC instance.
|
||||
func (d *MIoTDevice) SpecInstance() *MIoTSpecInstance {
|
||||
return d.specInstance
|
||||
}
|
||||
|
||||
// SetSpecInstance sets the SPEC instance (called after lazy loading).
|
||||
func (d *MIoTDevice) SetSpecInstance(specInstance *MIoTSpecInstance) {
|
||||
d.specInstance = specInstance
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Subscription methods (aligned with Python MIoTDevice)
|
||||
// ============================================================================
|
||||
|
||||
// SubProp subscribes to property changes.
|
||||
// Aligns with Python MIoTDevice.sub_prop.
|
||||
// siid, piid: property identifiers
|
||||
// handler: callback function called when property changes
|
||||
// Returns: subscription ID for unsubscribing.
|
||||
func (d *MIoTDevice) SubProp(siid, piid int, handler func(map[string]interface{}, interface{})) string {
|
||||
if d.miotClient == nil {
|
||||
fmt.Printf("[MIoTDevice] Warning: miotClient is nil, cannot subscribe to property\n")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Call MIoTClient.SubProp
|
||||
subID := d.miotClient.SubProp(d.did, handler, siid, piid)
|
||||
|
||||
// Store subscription ID
|
||||
d.subIDMu.Lock()
|
||||
defer d.subIDMu.Unlock()
|
||||
|
||||
if d.valueSubList["prop"] == nil {
|
||||
d.valueSubList["prop"] = make(map[string]func(map[string]interface{}, interface{}))
|
||||
}
|
||||
d.valueSubList["prop"][subID] = handler
|
||||
|
||||
return subID
|
||||
}
|
||||
|
||||
// UnsubProp unsubscribes from property changes.
|
||||
// Aligns with Python MIoTDevice.unsub_prop.
|
||||
func (d *MIoTDevice) UnsubProp(subID string) {
|
||||
if d.miotClient == nil {
|
||||
fmt.Printf("[MIoTDevice] Warning: miotClient is nil, cannot unsubscribe from property\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Call MIoTClient.UnsubProp
|
||||
d.miotClient.UnsubProp(d.did, subID)
|
||||
|
||||
// Remove subscription ID
|
||||
d.subIDMu.Lock()
|
||||
defer d.subIDMu.Unlock()
|
||||
|
||||
if subs, ok := d.valueSubList["prop"]; ok {
|
||||
delete(subs, subID)
|
||||
if len(subs) == 0 {
|
||||
delete(d.valueSubList, "prop")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SubEvent subscribes to device events.
|
||||
// Aligns with Python MIoTDevice.sub_event.
|
||||
// siid, eiid: event identifiers
|
||||
// handler: callback function called when event occurs
|
||||
// Returns: subscription ID for unsubscribing.
|
||||
func (d *MIoTDevice) SubEvent(siid, eiid int, handler func(map[string]interface{}, interface{})) string {
|
||||
if d.miotClient == nil {
|
||||
fmt.Printf("[MIoTDevice] Warning: miotClient is nil, cannot subscribe to event\n")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Call MIoTClient.SubEvent
|
||||
subID := d.miotClient.SubEvent(d.did, handler, siid, eiid)
|
||||
|
||||
// Store subscription ID
|
||||
d.subIDMu.Lock()
|
||||
defer d.subIDMu.Unlock()
|
||||
|
||||
if d.valueSubList["event"] == nil {
|
||||
d.valueSubList["event"] = make(map[string]func(map[string]interface{}, interface{}))
|
||||
}
|
||||
d.valueSubList["event"][subID] = handler
|
||||
|
||||
return subID
|
||||
}
|
||||
|
||||
// UnsubEvent unsubscribes from device events.
|
||||
// Aligns with Python MIoTDevice.unsub_event.
|
||||
func (d *MIoTDevice) UnsubEvent(subID string) {
|
||||
if d.miotClient == nil {
|
||||
fmt.Printf("[MIoTDevice] Warning: miotClient is nil, cannot unsubscribe from event\n")
|
||||
return
|
||||
}
|
||||
|
||||
// Call MIoTClient.UnsubEvent
|
||||
d.miotClient.UnsubEvent(d.did, subID)
|
||||
|
||||
// Remove subscription ID
|
||||
d.subIDMu.Lock()
|
||||
defer d.subIDMu.Unlock()
|
||||
|
||||
if subs, ok := d.valueSubList["event"]; ok {
|
||||
delete(subs, subID)
|
||||
if len(subs) == 0 {
|
||||
delete(d.valueSubList, "event")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SubscribeState subscribes to device state changes (online/offline).
|
||||
// Aligns with Python MIoTDevice._device_state_sub_list.
|
||||
func (d *MIoTDevice) SubscribeState(key string, handler func(string, interface{})) string {
|
||||
d.subIDMu.Lock()
|
||||
defer d.subIDMu.Unlock()
|
||||
|
||||
subID := fmt.Sprintf("%p", handler)
|
||||
|
||||
if d.deviceStateSubList[key] == nil {
|
||||
d.deviceStateSubList[key] = make(map[string]func(string, interface{}))
|
||||
}
|
||||
d.deviceStateSubList[key][subID] = handler
|
||||
|
||||
return subID
|
||||
}
|
||||
|
||||
// UnsubscribeState unsubscribes from device state changes.
|
||||
func (d *MIoTDevice) UnsubscribeState(key string, subID string) {
|
||||
d.subIDMu.Lock()
|
||||
defer d.subIDMu.Unlock()
|
||||
|
||||
if subs, ok := d.deviceStateSubList[key]; ok {
|
||||
delete(subs, subID)
|
||||
if len(subs) == 0 {
|
||||
delete(d.deviceStateSubList, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyStateChange notifies all state subscribers.
|
||||
// Called when device online/offline status changes.
|
||||
func (d *MIoTDevice) NotifyStateChange(state interface{}) {
|
||||
d.subIDMu.Lock()
|
||||
defer d.subIDMu.Unlock()
|
||||
|
||||
for _, subs := range d.deviceStateSubList {
|
||||
for _, handler := range subs {
|
||||
go handler(d.did, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Property read/write (aligned with Python miot_device.py)
|
||||
// ============================================================================
|
||||
|
||||
// GetPropertyAsync reads a property value from cloud by siid/piid.
|
||||
// Aligns with Python: MIoTServiceEntity.get_property_async(prop).
|
||||
func (d *MIoTDevice) GetPropertyAsync(siid, piid int) (interface{}, error) {
|
||||
if d.miotClient == nil || d.miotClient.http == nil {
|
||||
return nil, fmt.Errorf("HTTP client not available")
|
||||
}
|
||||
|
||||
params := []PropParam{
|
||||
{DID: d.did, Siid: siid, Piid: piid},
|
||||
}
|
||||
|
||||
results, err := d.miotClient.http.GetProps(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
return nil, fmt.Errorf("no result for property %d.%d", siid, piid)
|
||||
}
|
||||
|
||||
if results[0].Code != 0 {
|
||||
return nil, fmt.Errorf("get property %d.%d failed, code=%d", siid, piid, results[0].Code)
|
||||
}
|
||||
|
||||
return results[0].Value, nil
|
||||
}
|
||||
|
||||
// SetPropertyAsync writes a property value to cloud by siid/piid.
|
||||
// Aligns with Python: MIoTServiceEntity.set_property_async(prop, value).
|
||||
func (d *MIoTDevice) SetPropertyAsync(siid, piid int, value interface{}) error {
|
||||
if d.miotClient == nil || d.miotClient.http == nil {
|
||||
return fmt.Errorf("HTTP client not available")
|
||||
}
|
||||
|
||||
params := []PropParam{
|
||||
{DID: d.did, Siid: siid, Piid: piid, Value: value},
|
||||
}
|
||||
|
||||
results, err := d.miotClient.http.SetProp(params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(results) > 0 && results[0].Code != 0 {
|
||||
return fmt.Errorf("set property %d.%d failed, code=%d", siid, piid, results[0].Code)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CallActionAsync calls an action by siid/aiid.
|
||||
// Aligns with Python: MIoTServiceEntity.action_async(...) / MIoTActionEntity.action_async(...).
|
||||
func (d *MIoTDevice) CallActionAsync(siid, aiid int, inList []interface{}) (map[string]interface{}, error) {
|
||||
if d.miotClient == nil || d.miotClient.http == nil {
|
||||
return nil, fmt.Errorf("HTTP client not available")
|
||||
}
|
||||
|
||||
return d.miotClient.http.DoAction(d.did, siid, aiid, inList)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Device status helpers
|
||||
// ============================================================================
|
||||
|
||||
// HasWiFi returns true if the device has a WiFi connection (connectType != -1).
|
||||
// Aligns with Python logic: connect_type != -1.
|
||||
func (d *MIoTDevice) HasWiFi() bool {
|
||||
return d.connectType != -1
|
||||
}
|
||||
|
||||
// IsOnline returns the online status.
|
||||
func (d *MIoTDevice) IsOnline() bool {
|
||||
return d.online
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Serialization (aligned with Python to_dict / from_dict)
|
||||
// ============================================================================
|
||||
|
||||
// ToDict serializes the device to a map for storage.
|
||||
// Aligns with Python: to_dict() -> device serialized for storage.
|
||||
func (d *MIoTDevice) ToDict() map[string]interface{} {
|
||||
result := map[string]interface{}{
|
||||
"online": d.online,
|
||||
"did": d.did,
|
||||
"name": d.name,
|
||||
"model": d.model,
|
||||
"manufacturer": d.manufacturer,
|
||||
"fw_version": d.fwVersion,
|
||||
"icon": d.icon,
|
||||
"home_id": d.homeID,
|
||||
"home_name": d.homeName,
|
||||
"room_id": d.roomID,
|
||||
"room_name": d.roomName,
|
||||
"group_id": d.groupID,
|
||||
"connect_type": d.connectType,
|
||||
"rssi": d.rssi,
|
||||
"ssid": d.ssid,
|
||||
"bssid": d.bssid,
|
||||
"token": d.token,
|
||||
"local_ip": d.ip,
|
||||
}
|
||||
|
||||
// Serialize sub devices
|
||||
if len(d.subDevices) > 0 {
|
||||
subDevicesMap := make(map[string]interface{})
|
||||
for siid, sub := range d.subDevices {
|
||||
subDevicesMap[fmt.Sprintf("s%d", siid)] = sub.ToDict()
|
||||
}
|
||||
result["sub_devices"] = subDevicesMap
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FromDict deserializes a device from a stored map.
|
||||
// Aligns with Python: from_dict(data) classmethod pattern.
|
||||
func FromDict(miotClient *MIoTClient, data map[string]interface{}) *MIoTDevice {
|
||||
return NewMIoTDevice(miotClient, data, nil)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sub-devices
|
||||
// ============================================================================
|
||||
|
||||
// AddSubDevice adds a sub-device by SIID.
|
||||
func (d *MIoTDevice) AddSubDevice(siid int, sub *MIoTDevice) {
|
||||
if d.subDevices == nil {
|
||||
d.subDevices = make(map[int]*MIoTDevice)
|
||||
}
|
||||
d.subDevices[siid] = sub
|
||||
}
|
||||
|
||||
// GetSubDevice returns a sub-device by SIID.
|
||||
func (d *MIoTDevice) GetSubDevice(siid int) *MIoTDevice {
|
||||
if d.subDevices == nil {
|
||||
return nil
|
||||
}
|
||||
return d.subDevices[siid]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Accessors for Device fields
|
||||
// ============================================================================
|
||||
|
||||
// Manufacturer returns the device manufacturer.
|
||||
func (d *MIoTDevice) Manufacturer() string { return d.manufacturer }
|
||||
|
||||
// FWVersion returns the firmware version.
|
||||
func (d *MIoTDevice) FWVersion() string { return d.fwVersion }
|
||||
|
||||
// Icon returns the device icon URL.
|
||||
func (d *MIoTDevice) Icon() string { return d.icon }
|
||||
|
||||
// HomeID returns the home ID.
|
||||
func (d *MIoTDevice) HomeID() string { return d.homeID }
|
||||
|
||||
// HomeName returns the home name.
|
||||
func (d *MIoTDevice) HomeName() string { return d.homeName }
|
||||
|
||||
// RoomID returns the room ID.
|
||||
func (d *MIoTDevice) RoomID() string { return d.roomID }
|
||||
|
||||
// RoomName returns the room name.
|
||||
func (d *MIoTDevice) RoomName() string { return d.roomName }
|
||||
|
||||
// GroupID returns the group ID.
|
||||
func (d *MIoTDevice) GroupID() string { return d.groupID }
|
||||
|
||||
// Token returns the device token.
|
||||
func (d *MIoTDevice) Token() string { return d.token }
|
||||
|
||||
// IP returns the local IP address.
|
||||
func (d *MIoTDevice) IP() string { return d.ip }
|
||||
|
||||
// ConnectType returns the connection type (pid).
|
||||
func (d *MIoTDevice) ConnectType() int { return d.connectType }
|
||||
|
||||
// RSSI returns the WiFi signal strength.
|
||||
func (d *MIoTDevice) RSSI() int { return d.rssi }
|
||||
|
||||
// SSID returns the WiFi SSID.
|
||||
func (d *MIoTDevice) SSID() string { return d.ssid }
|
||||
|
||||
// BSSID returns the WiFi BSSID.
|
||||
func (d *MIoTDevice) BSSID() string { return d.bssid }
|
||||
|
||||
// SubDevices returns the sub-device map.
|
||||
func (d *MIoTDevice) SubDevices() map[int]*MIoTDevice { return d.subDevices }
|
||||
|
||||
// ServiceList returns the SPEC service instances.
|
||||
func (d *MIoTDevice) ServiceList() []*MIoTSpecService { return d.serviceList }
|
||||
Reference in New Issue
Block a user