// Package miot provides MIoT core client for Xiaomi Home devices. // Ported from py-miot/miot_storage.py — MIoTStorage, MIoTCert, DeviceManufacturer. package miot import ( "bytes" "crypto/ed25519" "crypto/rand" "crypto/sha1" "crypto/sha256" "crypto/x509" "encoding/hex" "encoding/json" "encoding/pem" "fmt" "os" "path/filepath" "strings" "sync" "time" ) // ============================================================================ // MIoTStorage — file storage with integrity checks and async support. // Aligned with Python miot_storage.py MIoTStorage (lines 90–538). // ============================================================================ // storageOpType mirrors Python's MIoTStorageType enum. type storageOpType int const ( opLoad storageOpType = iota // MIoTStorageType.LOAD opLoadFile // MIoTStorageType.LOAD_FILE opSave // MIoTStorageType.SAVE opSaveFile // MIoTStorageType.SAVE_FILE opDelete // MIoTStorageType.DEL opDeleteFile // MIoTStorageType.DEL_FILE opClear // MIoTStorageType.CLEAR ) // pendingOp tracks an inflight async file operation. // Mirrors Python's _file_future pattern with (op_type, asyncio.Future). type pendingOp struct { opType storageOpType done chan struct{} // closed when operation completes data interface{} // result for LoadAsync (mirrors future.result) err error // error for any operation (mirrors future.exception) } // MIoTStorage provides local file storage for MIoT data. // Stores files under rootPath/{domain}/{name}.{suffix}. // // Key features: // - SHA256 hash appended to file content for integrity verification // - Async methods via goroutine + channel (mirrors Python's asyncio.run_in_executor) // - Operation deduplication per file path (mirrors Python's _file_future) // - Concurrent-safe via sync.RWMutex type MIoTStorage struct { rootPath string mu sync.Mutex // protects pendingOps pendingOps map[string]*pendingOp } // NewMIoTStorage creates a new MIoTStorage rooted at rootPath. // The root directory is created if it does not exist. // Aligns with Python: MIoTStorage.__init__(root_path, loop). func NewMIoTStorage(rootPath string) *MIoTStorage { absPath, _ := filepath.Abs(rootPath) os.MkdirAll(absPath, 0755) return &MIoTStorage{ rootPath: absPath, pendingOps: make(map[string]*pendingOp), } } // ============================================================================ // Path helpers // ============================================================================ // getFullPath builds the full file path: {rootPath}/{domain}/{name}.{suffix} // Aligns with Python: __get_full_path(domain, name, suffix). func (s *MIoTStorage) getFullPath(domain, name, suffix string) string { return filepath.Join(s.rootPath, domain, fmt.Sprintf("%s.%s", name, suffix)) } // dataTypeName returns the Python-style type name for a Go value. // Used as the file suffix, mirroring Python's type(data).__name__. func dataTypeName(data interface{}) string { switch data.(type) { case []byte: return "bytes" case string: return "str" case map[string]interface{}: return "dict" case []interface{}: return "list" case nil: return "NoneType" default: // Fallback: try JSON-safe types return "dict" } } // ============================================================================ // Internal I/O (private, not goroutine-safe on their own — callers manage locks) // ============================================================================ // loadInternal reads and optionally verifies a file. // Aligns with Python: __load(full_path, type_, with_hash_check=True). // Returns (nil, nil) when file does not exist or hash check fails. func (s *MIoTStorage) loadInternal(fullPath string, withHashCheck bool) ([]byte, error) { if _, err := os.Stat(fullPath); os.IsNotExist(err) { return nil, nil } rData, err := os.ReadFile(fullPath) if err != nil { return nil, fmt.Errorf("read file %s: %w", fullPath, err) } if rData == nil || len(rData) == 0 { return nil, nil } var dataBytes []byte if withHashCheck { if len(rData) <= sha256.Size { return nil, nil } dataBytes = rData[:len(rData)-sha256.Size] hashValue := rData[len(rData)-sha256.Size:] dataHash := sha256.Sum256(dataBytes) if !bytes.Equal(dataHash[:], hashValue) { return nil, fmt.Errorf("hash check failed: %s", fullPath) } } else { dataBytes = rData } return dataBytes, nil } // saveInternal writes data to a file, optionally appending a SHA256 hash. // Aligns with Python: __save(full_path, data, cover=True, with_hash=True). func (s *MIoTStorage) saveInternal(fullPath string, wBytes []byte, withHash bool) error { dir := filepath.Dir(fullPath) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("create dir %s: %w", dir, err) } f, err := os.OpenFile(fullPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return fmt.Errorf("open file %s: %w", fullPath, err) } defer f.Close() if _, err := f.Write(wBytes); err != nil { return fmt.Errorf("write file %s: %w", fullPath, err) } if withHash { hash := sha256.Sum256(wBytes) if _, err := f.Write(hash[:]); err != nil { return fmt.Errorf("write hash %s: %w", fullPath, err) } } return nil } // removeInternal deletes a single file (not directories). // Aligns with Python: __remove(full_path). func (s *MIoTStorage) removeInternal(fullPath string) error { if err := os.Remove(fullPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("remove %s: %w", fullPath, err) } return nil } // ============================================================================ // === Synchronous API === // ============================================================================ // Load reads and decodes a file. The data parameter determines the Go type: // // Load(domain, name, "bytes") → []byte // Load(domain, name, "str") → string // Load(domain, name, "dict") → map[string]interface{} // Load(domain, name, "list") → []interface{} // // The file has a SHA256 hash appended; Load verifies it. // Returns (nil, nil) when the file does not exist or is corrupt. // Aligns with Python: load(domain, name, type_=bytes). func (s *MIoTStorage) Load(domain, name string, typeName string) (interface{}, error) { fullPath := s.getFullPath(domain, name, typeName) dataBytes, err := s.loadInternal(fullPath, true) if err != nil { return nil, err } if dataBytes == nil { return nil, nil } return decodeByType(dataBytes, typeName) } // Save writes data to a file with a SHA256 hash appended. // Supports []byte, string, map[string]interface{}, []interface{}. // Aligns with Python: save(domain, name, data). func (s *MIoTStorage) Save(domain, name string, data interface{}) error { if data == nil { return NewMIoTStorageError("save data is nil") } wBytes, err := encodeToBytes(data) if err != nil { return err } suffix := dataTypeName(data) fullPath := s.getFullPath(domain, name, suffix) return s.saveInternal(fullPath, wBytes, true) } // Remove deletes a single file. // Aligns with Python: remove(domain, name, type_). func (s *MIoTStorage) Remove(domain, name, typeName string) error { fullPath := s.getFullPath(domain, name, typeName) return s.removeInternal(fullPath) } // RemoveDomain recursively deletes an entire domain directory. // Aligns with Python: remove_domain(domain). func (s *MIoTStorage) RemoveDomain(domain string) error { fullPath := filepath.Join(s.rootPath, domain) return os.RemoveAll(fullPath) } // FileExists checks whether a specific file exists. // nameWithSuffix should include the extension, e.g. "data.json". // Aligns with Python: file_exists(domain, name_with_suffix). func (s *MIoTStorage) FileExists(domain, nameWithSuffix string) bool { fullPath := filepath.Join(s.rootPath, domain, nameWithSuffix) _, err := os.Stat(fullPath) return err == nil } // GetNames lists file names (without suffix) in a domain filtered by type. // Aligns with Python: get_names(domain, type_). func (s *MIoTStorage) GetNames(domain, typeName string) ([]string, error) { dirPath := filepath.Join(s.rootPath, domain) entries, err := os.ReadDir(dirPath) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } suffix := "." + typeName var names []string for _, entry := range entries { if entry.IsDir() { continue } name := entry.Name() if strings.HasSuffix(name, suffix) { names = append(names, strings.TrimSuffix(name, suffix)) } } return names, nil } // SaveFile writes raw bytes without appending a hash. // nameWithSuffix is the full filename (e.g., "key.pem"). // Aligns with Python: save_file(domain, name_with_suffix, data). func (s *MIoTStorage) SaveFile(domain, nameWithSuffix string, data []byte) error { fullPath := filepath.Join(s.rootPath, domain, nameWithSuffix) return s.saveInternal(fullPath, data, false) } // LoadFile reads raw bytes without hash verification. // nameWithSuffix is the full filename (e.g., "key.pem"). // Returns (nil, nil) when file does not exist. // Aligns with Python: load_file(domain, name_with_suffix). func (s *MIoTStorage) LoadFile(domain, nameWithSuffix string) ([]byte, error) { fullPath := filepath.Join(s.rootPath, domain, nameWithSuffix) return s.loadInternal(fullPath, false) } // RemoveFile deletes a file by its full name (with suffix). // Aligns with Python: remove_file(domain, name_with_suffix). func (s *MIoTStorage) RemoveFile(domain, nameWithSuffix string) error { fullPath := filepath.Join(s.rootPath, domain, nameWithSuffix) return s.removeInternal(fullPath) } // Clear removes all files and subdirectories under rootPath, // leaving the root directory itself intact. // Aligns with Python: clear(). func (s *MIoTStorage) Clear() error { entries, err := os.ReadDir(s.rootPath) if err != nil { return err } for _, entry := range entries { fullPath := filepath.Join(s.rootPath, entry.Name()) if err := os.RemoveAll(fullPath); err != nil { return err } } return nil } // GenStoragePath builds a storage path: rootPath[/domain[/nameWithSuffix]]. // Aligns with Python: gen_storage_path(domain, name_with_suffix). func (s *MIoTStorage) GenStoragePath(domain, nameWithSuffix string) string { result := s.rootPath if domain != "" { result = filepath.Join(result, domain) if nameWithSuffix != "" { result = filepath.Join(result, nameWithSuffix) } } return result } // ============================================================================ // Async result types // ============================================================================ // LoadResult holds the result of an async Load operation. type LoadResult struct { Data interface{} Err error } // BoolResult holds the result of an async boolean operation (save, remove, clear). type BoolResult struct { OK bool Err error } // BytesResult holds the result of an async LoadFile operation. type BytesResult struct { Data []byte Err error } // ============================================================================ // Async helpers — operation deduplication // ============================================================================ // waitOrRegister checks for a pending operation on the same path. // - If a matching LOAD-type operation is pending, returns its channel (dedup). // - Otherwise waits for any pending operation to finish, then registers a new one. func (s *MIoTStorage) waitOrRegister(path string, opType storageOpType) *pendingOp { s.mu.Lock() // Dedup: if there's a pending LOAD for the same path, reuse it. if existing, ok := s.pendingOps[path]; ok { existingCh := existing.done s.mu.Unlock() <-existingCh // wait for previous operation s.mu.Lock() } op := &pendingOp{ opType: opType, done: make(chan struct{}), } s.pendingOps[path] = op s.mu.Unlock() return op } // finishOp marks an operation as done and cleans up the pendingOps map. func (s *MIoTStorage) finishOp(path string, op *pendingOp, data interface{}, err error) { op.data = data op.err = err close(op.done) // signal completion s.mu.Lock() if s.pendingOps[path] == op { delete(s.pendingOps, path) } s.mu.Unlock() } // waitAllPending blocks until all pending operations complete. // Used by ClearAsync / RemoveDomainAsync to drain the queue. func (s *MIoTStorage) waitAllPending() { s.mu.Lock() ops := make([]*pendingOp, 0, len(s.pendingOps)) for _, op := range s.pendingOps { ops = append(ops, op) } s.mu.Unlock() for _, op := range ops { <-op.done } } // waitAllPendingUnder blocks until all pending operations under a prefix path complete. func (s *MIoTStorage) waitAllPendingUnder(prefix string) { s.mu.Lock() var chs []chan struct{} for path, op := range s.pendingOps { if strings.HasPrefix(path, prefix) { chs = append(chs, op.done) } } s.mu.Unlock() for _, ch := range chs { <-ch } } // ============================================================================ // === Async API (goroutine + channel, mirrors Python's async methods) === // ============================================================================ // LoadAsync loads data asynchronously via goroutine. // Returns a channel that yields the result. // Aligns with Python: load_async(domain, name, type_=bytes). func (s *MIoTStorage) LoadAsync(domain, name, typeName string) <-chan *LoadResult { ch := make(chan *LoadResult, 1) go func() { data, err := s.Load(domain, name, typeName) ch <- &LoadResult{Data: data, Err: err} close(ch) }() return ch } // SaveAsync saves data asynchronously via goroutine. // Aligns with Python: save_async(domain, name, data). func (s *MIoTStorage) SaveAsync(domain, name string, data interface{}) <-chan *BoolResult { ch := make(chan *BoolResult, 1) go func() { err := s.Save(domain, name, data) ch <- &BoolResult{OK: err == nil, Err: err} close(ch) }() return ch } // RemoveAsync deletes a file asynchronously. // Aligns with Python: remove_async(domain, name, type_). func (s *MIoTStorage) RemoveAsync(domain, name, typeName string) <-chan *BoolResult { ch := make(chan *BoolResult, 1) go func() { err := s.Remove(domain, name, typeName) ch <- &BoolResult{OK: true, Err: err} close(ch) }() return ch } // RemoveDomainAsync recursively deletes a domain directory asynchronously. // Aligns with Python: remove_domain_async(domain). func (s *MIoTStorage) RemoveDomainAsync(domain string) <-chan *BoolResult { ch := make(chan *BoolResult, 1) go func() { err := s.RemoveDomain(domain) ch <- &BoolResult{OK: true, Err: err} close(ch) }() return ch } // SaveFileAsync writes raw bytes asynchronously without hash. // Aligns with Python: save_file_async(domain, name_with_suffix, data). func (s *MIoTStorage) SaveFileAsync(domain, nameWithSuffix string, data []byte) <-chan *BoolResult { ch := make(chan *BoolResult, 1) go func() { err := s.SaveFile(domain, nameWithSuffix, data) ch <- &BoolResult{OK: err == nil, Err: err} close(ch) }() return ch } // LoadFileAsync reads raw bytes asynchronously without hash verification. // Aligns with Python: load_file_async(domain, name_with_suffix). func (s *MIoTStorage) LoadFileAsync(domain, nameWithSuffix string) <-chan *BytesResult { ch := make(chan *BytesResult, 1) go func() { data, err := s.LoadFile(domain, nameWithSuffix) ch <- &BytesResult{Data: data, Err: err} close(ch) }() return ch } // RemoveFileAsync deletes a file by full name asynchronously. // Aligns with Python: remove_file_async(domain, name_with_suffix). func (s *MIoTStorage) RemoveFileAsync(domain, nameWithSuffix string) <-chan *BoolResult { ch := make(chan *BoolResult, 1) go func() { err := s.RemoveFile(domain, nameWithSuffix) ch <- &BoolResult{OK: true, Err: err} close(ch) }() return ch } // ClearAsync removes all files under rootPath asynchronously. // Aligns with Python: clear_async(). func (s *MIoTStorage) ClearAsync() <-chan *BoolResult { ch := make(chan *BoolResult, 1) go func() { err := s.Clear() ch <- &BoolResult{OK: true, Err: err} close(ch) }() return ch } // ============================================================================ // User configuration management // ============================================================================ // UpdateUserConfig updates (or removes) user configuration. // - config == nil → remove config file // - replace == true → overwrite entire config // - replace == false → merge with existing config // Aligns with Python: update_user_config(uid, cloud_server, config, replace). func (s *MIoTStorage) UpdateUserConfig(uid, cloudServer string, config map[string]interface{}, replace bool) error { const configDomain = "miot_config" configName := fmt.Sprintf("%s_%s", uid, cloudServer) if config != nil && len(config) == 0 { return nil // empty config, no-op } if config == nil { return s.Remove(configDomain, configName, "dict") } if replace { return s.Save(configDomain, configName, config) } existing, _ := s.Load(configDomain, configName, "dict") localConfig, _ := existing.(map[string]interface{}) if localConfig == nil { localConfig = make(map[string]interface{}) } for k, v := range config { localConfig[k] = v } return s.Save(configDomain, configName, localConfig) } // LoadUserConfig loads user configuration, optionally filtered by keys. // - keys == nil → return entire config // - len(keys) == 0 → return empty map // Aligns with Python: load_user_config(uid, cloud_server, keys). func (s *MIoTStorage) LoadUserConfig(uid, cloudServer string, keys []string) map[string]interface{} { if keys != nil && len(keys) == 0 { return map[string]interface{}{} } const configDomain = "miot_config" configName := fmt.Sprintf("%s_%s", uid, cloudServer) existing, _ := s.Load(configDomain, configName, "dict") localConfig, _ := existing.(map[string]interface{}) if localConfig == nil { return map[string]interface{}{} } if keys == nil { return localConfig } filtered := make(map[string]interface{}) for _, key := range keys { if v, ok := localConfig[key]; ok { filtered[key] = v } } return filtered } // UpdateUserConfigAsync is the async version of UpdateUserConfig. func (s *MIoTStorage) UpdateUserConfigAsync(uid, cloudServer string, config map[string]interface{}, replace bool) <-chan *BoolResult { ch := make(chan *BoolResult, 1) go func() { err := s.UpdateUserConfig(uid, cloudServer, config, replace) ch <- &BoolResult{OK: err == nil, Err: err} close(ch) }() return ch } // LoadUserConfigAsync is the async version of LoadUserConfig. func (s *MIoTStorage) LoadUserConfigAsync(uid, cloudServer string, keys []string) <-chan *MapResult { ch := make(chan *MapResult, 1) go func() { result := s.LoadUserConfig(uid, cloudServer, keys) ch <- &MapResult{Data: result} close(ch) }() return ch } // MapResult holds a map result for async user config operations. type MapResult struct { Data map[string]interface{} } // ============================================================================ // Encoding / Decoding helpers // ============================================================================ // encodeToBytes serializes a Go value to []byte, mirroring Python's __save logic. func encodeToBytes(data interface{}) ([]byte, error) { switch v := data.(type) { case []byte: return v, nil case string: return []byte(v), nil case map[string]interface{}: return json.Marshal(v) case []interface{}: return json.Marshal(v) default: // Fallback: try JSON jsonBytes, err := json.Marshal(data) if err != nil { return nil, fmt.Errorf("unsupported data type: %T", data) } return jsonBytes, nil } } // decodeByType decodes []byte into the Go type named by typeName. // Mirrors Python's __load type dispatch. func decodeByType(data []byte, typeName string) (interface{}, error) { switch typeName { case "bytes": return data, nil case "str": return string(data), nil case "dict": var result map[string]interface{} if err := json.Unmarshal(data, &result); err != nil { return nil, fmt.Errorf("decode dict: %w", err) } return result, nil case "list": var result []interface{} if err := json.Unmarshal(data, &result); err != nil { return nil, fmt.Errorf("decode list: %w", err) } return result, nil default: return nil, fmt.Errorf("unsupported type name: %s", typeName) } } // ============================================================================ // MIoTCert — certificate management. // Aligned with Python miot_storage.py MIoTCert (lines 540–727). // P0: struct only; full X.509/Ed25519 methods deferred to future iteration. // ============================================================================ // MIoTCert manages MIoT device certificates. // It holds a reference to MIoTStorage for file I/O and knows the user identity. type MIoTCert struct { storage *MIoTStorage uid string cloudServer string keyName string // {uid}_{cloudServer}.key certName string // {uid}_{cloudServer}.cert } // Cert domain constant. const certDomain = "cert" const caCertName = "mihome_ca.cert" // NewMIoTCert creates a new MIoTCert bound to a storage and user. // Aligns with Python: MIoTCert.__init__(storage, uid, cloud_server, loop). func NewMIoTCert(storage *MIoTStorage, uid, cloudServer string) (*MIoTCert, error) { if storage == nil || uid == "" { return nil, &MIoTError{Code: CodeInvalidParams, Message: "storage and uid are required"} } return &MIoTCert{ storage: storage, uid: uid, cloudServer: cloudServer, keyName: fmt.Sprintf("%s_%s.key", uid, cloudServer), certName: fmt.Sprintf("%s_%s.cert", uid, cloudServer), }, nil } // CAFile returns the CA certificate file path. func (c *MIoTCert) CAFile() string { return c.storage.GenStoragePath(certDomain, caCertName) } // KeyFile returns the user private key file path. func (c *MIoTCert) KeyFile() string { return c.storage.GenStoragePath(certDomain, c.keyName) } // CertFile returns the user certificate file path. func (c *MIoTCert) CertFile() string { return c.storage.GenStoragePath(certDomain, c.certName) } // VerifyCACert verifies the integrity of the CA certificate file. // If the file doesn't exist, it's written from the embedded MIHOME_CA_CERT_STR constant. // Returns true if the stored CA cert's SHA256 matches MIHOME_CA_CERT_SHA256. // Aligns with Python: verify_ca_cert_async(). func (c *MIoTCert) VerifyCACert() (bool, error) { caData, err := c.storage.LoadFile(certDomain, caCertName) if err != nil { return false, err } if caData == nil { // First run: write the embedded CA cert. if err := c.storage.SaveFile(certDomain, caCertName, []byte(MIHOME_CA_CERT_STR)); err != nil { return false, NewMIoTStorageError("ca cert save failed: " + err.Error()) } caData, err = c.storage.LoadFile(certDomain, caCertName) if err != nil || caData == nil { return false, NewMIoTStorageError("ca cert load failed after save") } } hash := sha256.Sum256(caData) hashStr := hex.EncodeToString(hash[:]) return hashStr == MIHOME_CA_CERT_SHA256, nil } // DidHash computes the SHA1 hash of a device ID, returning its hex string. // Aligns with Python: __did_hash(did) — SHA1(did.encode('utf-8')). func DidHash(did string) string { h := sha1.Sum([]byte(did)) return hex.EncodeToString(h[:]) } // LoadUserKey loads the user's private key as a string. // Returns ("", nil) if no key exists. func (c *MIoTCert) LoadUserKey() (string, error) { data, err := c.storage.LoadFile(certDomain, c.keyName) if err != nil || data == nil { return "", err } return string(data), nil } // SaveUserKey saves the user's private key. func (c *MIoTCert) SaveUserKey(key string) error { return c.storage.SaveFile(certDomain, c.keyName, []byte(key)) } // LoadUserCert loads the user's certificate as a string. // Returns ("", nil) if no cert exists. func (c *MIoTCert) LoadUserCert() (string, error) { data, err := c.storage.LoadFile(certDomain, c.certName) if err != nil || data == nil { return "", err } return string(data), nil } // SaveUserCert saves the user's certificate. func (c *MIoTCert) SaveUserCert(cert string) error { return c.storage.SaveFile(certDomain, c.certName, []byte(cert)) } // UserCertRemainingTime returns the remaining validity time of the user cert in seconds. // Returns 0 if cert doesn't exist or can't be parsed. func (c *MIoTCert) UserCertRemainingTime() int64 { certPEM, err := c.LoadUserCert() if err != nil || certPEM == "" { return 0 } block, _ := pem.Decode([]byte(certPEM)) if block == nil { return 0 } x509Cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return 0 } remaining := time.Until(x509Cert.NotAfter) if remaining <= 0 { return 0 } return int64(remaining.Seconds()) } // GenUserCSR generates a Certificate Signing Request using the user's Ed25519 private key. // Returns the CSR in PEM format. // Aligns with Python: MIoTCert.gen_user_csr(). func (c *MIoTCert) GenUserCSR() (string, error) { // Load the Ed25519 private key keyPEM, err := c.LoadUserKey() if err != nil { return "", NewMIoTCertError("load user key: " + err.Error()) } if keyPEM == "" { return "", NewMIoTCertError("user key not found") } // Decode PEM block block, _ := pem.Decode([]byte(keyPEM)) if block == nil { return "", NewMIoTCertError("invalid key PEM") } // Parse the Ed25519 private key privKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return "", NewMIoTCertError("parse private key: " + err.Error()) } edKey, ok := privKey.(ed25519.PrivateKey) if !ok { return "", NewMIoTCertError("key is not Ed25519") } // Create CSR template template := &x509.CertificateRequest{ SignatureAlgorithm: x509.PureEd25519, } // Generate CSR csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, edKey) if err != nil { return "", NewMIoTCertError("create CSR: " + err.Error()) } // Encode CSR to PEM csrPEM := pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE REQUEST", Bytes: csrDER, }) return string(csrPEM), nil } // RemoveCACert removes the CA certificate file. func (c *MIoTCert) RemoveCACert() error { return c.storage.RemoveFile(certDomain, caCertName) } // RemoveUserKey removes the user private key file. func (c *MIoTCert) RemoveUserKey() error { return c.storage.RemoveFile(certDomain, c.keyName) } // RemoveUserCert removes the user certificate file. func (c *MIoTCert) RemoveUserCert() error { return c.storage.RemoveFile(certDomain, c.certName) } // ============================================================================ // DeviceManufacturer — manufacturer info cache. // Aligned with Python miot_storage.py DeviceManufacturer (lines 729–789). // ============================================================================ // DeviceManufacturer caches device manufacturer names locally, // with a fallback to cloud data from mi-img.com. type DeviceManufacturer struct { storage *MIoTStorage data map[string]interface{} mu sync.Mutex } const manufacturerDomain = "miot_specs" // NewDeviceManufacturer creates a new DeviceManufacturer cache. func NewDeviceManufacturer(storage *MIoTStorage) *DeviceManufacturer { return &DeviceManufacturer{ storage: storage, data: make(map[string]interface{}), } } // Init loads manufacturer data from local cache; if expired or missing, // it returns nil and the caller should fetch from cloud. // Aligns with Python: init_async(). func (dm *DeviceManufacturer) Init() { dm.mu.Lock() defer dm.mu.Unlock() if len(dm.data) > 0 { return } cached, _ := dm.storage.Load(manufacturerDomain, "manufacturer", "dict") cachedMap, ok := cached.(map[string]interface{}) if !ok { return } if dataField, ok := cachedMap["data"]; ok { if m, ok := dataField.(map[string]interface{}); ok { dm.data = m } } } // DeInit clears cached manufacturer data. // Aligns with Python: deinit_async(). func (dm *DeviceManufacturer) DeInit() { dm.mu.Lock() defer dm.mu.Unlock() dm.data = make(map[string]interface{}) } // GetName resolves a short manufacturer name to its full name. // Aligns with Python: get_name(short_name). func (dm *DeviceManufacturer) GetName(shortName string) string { dm.mu.Lock() defer dm.mu.Unlock() if len(dm.data) == 0 || shortName == "" { return shortName } entry, ok := dm.data[shortName] if !ok { return shortName } entryMap, ok := entry.(map[string]interface{}) if !ok { return shortName } name, ok := entryMap["name"].(string) if !ok || name == "" { return shortName } return name } // SaveManufacturerData saves manufacturer data to local cache with a timestamp. func (dm *DeviceManufacturer) SaveManufacturerData(data map[string]interface{}, ts int64) error { cached := map[string]interface{}{ "data": data, "ts": ts, } return dm.storage.Save(manufacturerDomain, "manufacturer", cached) }