Files
xiaomihome/miot/miot_device.go
T
4566704 2b7de69403 refactor(logger): 日志改用 logrus 全局包级调用,移除 Logger 注入接口
为什么:并入宿主项目后须与宿主日志体系一致——宿主统一用 pkg/logger 初始化
logrus 全局实例并直接 logrus.Xxx 包级调用;包一层 Logger 接口会让 logrus 的
caller 定位(报告调用文件:行号)失准。

改动:
- 删除 logger 包(Logger 接口 / Default / SetDefault,默认本就是 logrus.StandardLogger)
- 11 个结构体移除 lgr logger.Logger 字段、SetLogger 方法及构造中的 lgr 初始化
- 192 处 lgr.Xxxf 调用改为 logrus.Xxxf,日志消息文本保持不变
- xiaomi.Client 移除 Logger() 访问器,air_conditioner 回读日志去掉判空调用直连 logrus
- miot_i18n.go 两处标准库 log.Printf 改为 logrus.Errorf,统一日志出口
- go.mod:logrus 从 indirect 提升为直接依赖
- goimports/goformat 全树规范化:此前部分文件未 gofmt(单行 if、对齐),本次顺带
  格式化,纯空白/换行变化,无逻辑改动

验证:go build / go vet / go test ./... 全部通过(bridge、miot、xiaomi、devices、specs)
2026-09-15 20:16:26 +08:00

531 lines
16 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"
"github.com/sirupsen/logrus"
)
// ============================================================================
// 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),
}
if specInstance != nil {
dev.serviceList = specInstance.GetServices()
}
// 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 {
logrus.Warnf("[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 {
logrus.Warnf("[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 {
logrus.Warnf("[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 {
logrus.Warnf("[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 }