- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
525 lines
15 KiB
Go
525 lines
15 KiB
Go
// 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 }
|