// 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 }