// Package miot provides MIoT core client for Xiaomi Home devices. // spec_parser.go — full SPEC parser ported from py-miot/miot_spec.py. package miot import ( "encoding/json" "fmt" "hash/crc32" "log" "math" "net/http" "os" "path/filepath" "runtime" "strconv" "strings" "sync" "time" ) // ============================================================================ // MIoTSpecInstance and related types (aligned with Python miot_spec.py) // ============================================================================ // MIoTSpecInstance represents a parsed SPEC instance (device). type MIoTSpecInstance struct { URN string Type string Description string DescriptionTrans string Expires int64 Services []*MIoTSpecService } // GetServices returns the service list (convenience method for device.go). func (i *MIoTSpecInstance) GetServices() []*MIoTSpecService { return i.Services } // MIoTSpecService represents a service in the SPEC. type MIoTSpecService struct { IID int Type string Description string DescriptionTrans string NeedFilter bool Proprietary bool Properties []*MIoTSpecProperty Actions []*MIoTSpecAction Events []*MIoTSpecEvent } // MIoTSpecProperty represents a property in the SPEC. type MIoTSpecProperty struct { SIID int PIID int Type string Description string DescriptionTrans string Format string Access []string Unit string Expr string // simple expression e.g. "src_value * 2" Precision int // decimal precision for floats ValueRange *MIoTSpecValueRange ValueList MIoTSpecValueList NeedFilter bool Proprietary bool } // MIoTSpecValueRange represents a value range in the SPEC. type MIoTSpecValueRange struct { Min interface{} Max interface{} Step interface{} } // MIoTSpecValueItem represents a single value list entry in the SPEC. type MIoTSpecValueItem struct { Value interface{} Desc string Name string } // MIoTSpecValueList represents a collection of value list entries. type MIoTSpecValueList []*MIoTSpecValueItem // MIoTSpecAction represents an action in the SPEC. type MIoTSpecAction struct { SIID int AIID int Type string Description string DescriptionTrans string In []string Out []string NeedFilter bool Proprietary bool } // MIoTSpecEvent represents an event in the SPEC. type MIoTSpecEvent struct { SIID int EIID int Type string Description string DescriptionTrans string Arguments []string NeedFilter bool Proprietary bool } // ============================================================================ // MIoTSpecParser — the main parser // ============================================================================ // MIoTSpecParser wraps SPEC parsing logic. type MIoTSpecParser struct { lang string storage *MIoTStorage specDir string stdLib *SpecStdLib initDone bool initMu sync.Mutex } // NewMIoTSpecParser creates a new MIoTSpecParser. func NewMIoTSpecParser(lang string, storage *MIoTStorage, specDir string) *MIoTSpecParser { if lang == "" { lang = DEFAULT_INTEGRATION_LANGUAGE } return &MIoTSpecParser{ lang: lang, storage: storage, specDir: specDir, stdLib: NewSpecStdLib(lang), } } // Init initializes the parser, loading or refreshing the standard library. func (p *MIoTSpecParser) Init() error { p.initMu.Lock() defer p.initMu.Unlock() if p.initDone { return nil } // Try loading cached std lib stdLibCache, _ := p.storage.Load("miot_specs", "spec_std_lib", "dict") if cacheMap, ok := stdLibCache.(map[string]interface{}); ok { if dataField, ok := cacheMap["data"]; ok { if dataMap, ok := dataField.(map[string]interface{}); ok { if tsField, ok := cacheMap["ts"]; ok { var ts int64 switch v := tsField.(type) { case float64: ts = int64(v) case json.Number: ts, _ = v.Int64() } if ts > 0 && time.Now().Unix()-ts < SPEC_STD_LIB_EFFECTIVE_TIME { log.Printf("[spec] using cached spec std lib, ts=%d", ts) p.stdLib.Load(dataMap) p.initDone = true return nil } } } } } // Refresh from cloud if err := p.stdLib.Refresh(); err != nil { log.Printf("[spec] refresh std lib failed: %v", err) // Try stale cache as fallback if cacheMap, ok := stdLibCache.(map[string]interface{}); ok { if dataField, ok := cacheMap["data"]; ok { if dataMap, ok := dataField.(map[string]interface{}); ok { log.Printf("[spec] using stale cached spec std lib") p.stdLib.Load(dataMap) p.initDone = true return nil } } } return fmt.Errorf("load spec std lib failed") } // Save the refreshed std lib _ = p.storage.Save("miot_specs", "spec_std_lib", map[string]interface{}{ "data": p.stdLib.Dump(), "ts": time.Now().Unix(), }) p.initDone = true return nil } // Parse parses a URN and returns a MIoTSpecInstance. // The model parameter is the device model string (e.g., "lumi.plug.maeu01"). func (p *MIoTSpecParser) Parse(urn string, model string) (*MIoTSpecInstance, error) { if !p.initDone { if err := p.Init(); err != nil { return nil, fmt.Errorf("spec parser init: %w", err) } } // Read local JSON spec file specData, err := p.readSpecFile(urn) if err != nil { return nil, fmt.Errorf("read spec file: %w", err) } return p.parseSpec(urn, model, specData) } // readSpecFile reads and parses a local JSON spec file from specDir/{urn}.json. func (p *MIoTSpecParser) readSpecFile(urn string) (map[string]interface{}, error) { // On Windows, colons are not allowed in filenames fileName := urn if runtime.GOOS == "windows" { fileName = strings.ReplaceAll(urn, ":", "_") } filePath := filepath.Join(p.specDir, fileName+".json") data, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("read %s: %w", filePath, err) } var result map[string]interface{} if err := json.Unmarshal(data, &result); err != nil { return nil, fmt.Errorf("parse %s: %w", filePath, err) } return result, nil } // parseSpec parses a raw spec dict into a MIoTSpecInstance tree. func (p *MIoTSpecParser) parseSpec(urn string, model string, specData map[string]interface{}) (*MIoTSpecInstance, error) { // Get instance description from std lib urnParts := strings.Split(urn, ":") urnKey := strings.Join(urnParts[:min(6, len(urnParts))], ":") deviceTypeKey := strings.Join(urnParts[:min(5, len(urnParts))], ":") description := "" if desc, ok := specData["description"].(string); ok { description = desc } descriptionTrans := p.stdLib.DeviceTranslate(deviceTypeKey) if descriptionTrans == "" { descriptionTrans = description } if descriptionTrans == "" && len(urnParts) > 3 { descriptionTrans = urnParts[3] } instance := &MIoTSpecInstance{ URN: urn, Type: urnKey, Description: description, DescriptionTrans: descriptionTrans, } servicesRaw, _ := specData["services"].([]interface{}) if servicesRaw == nil { return instance, nil } for _, svcRaw := range servicesRaw { service, ok := svcRaw.(map[string]interface{}) if !ok { continue } if p.skipProprietary(service) { continue } specSvc := p.parseService(service) if specSvc == nil { continue } instance.Services = append(instance.Services, specSvc) } return instance, nil } // parseService parses a single service from the spec. func (p *MIoTSpecParser) parseService(service map[string]interface{}) *MIoTSpecService { iid := intVal(service, "iid") typeStr := strVal(service, "type") description := strVal(service, "description") if typeStr == "" || description == "" { return nil } // Skip device-information service typeParts := strings.Split(typeStr, ":") if len(typeParts) > 3 && typeParts[3] == "device-information" { return nil } svc := &MIoTSpecService{ IID: iid, Type: typeStr, Description: description, Proprietary: len(typeParts) > 1 && typeParts[1] != "miot-spec-v2", } svc.NeedFilter = boolVal(service, "need_filter") // Translate description typeKey := strings.Join(typeParts[:min(5, len(typeParts))], ":") svc.DescriptionTrans = p.stdLib.ServiceTranslate(typeKey) if svc.DescriptionTrans == "" { svc.DescriptionTrans = description } // Parse properties propertiesRaw, _ := service["properties"].([]interface{}) for _, propRaw := range propertiesRaw { prop, ok := propRaw.(map[string]interface{}) if !ok { continue } if p.skipProprietary(prop) { continue } specProp := p.parseProperty(prop, svc, typeParts) if specProp != nil && !specProp.NeedFilter { svc.Properties = append(svc.Properties, specProp) } } // Parse events eventsRaw, _ := service["events"].([]interface{}) for _, eventRaw := range eventsRaw { event, ok := eventRaw.(map[string]interface{}) if !ok { continue } if p.skipProprietary(event) { continue } specEvent := p.parseEvent(event, svc) if specEvent != nil && !specEvent.NeedFilter { svc.Events = append(svc.Events, specEvent) } } // Parse actions actionsRaw, _ := service["actions"].([]interface{}) for _, actionRaw := range actionsRaw { action, ok := actionRaw.(map[string]interface{}) if !ok { continue } if p.skipProprietary(action) { continue } specAction := p.parseAction(action, svc) if specAction != nil && !specAction.NeedFilter { svc.Actions = append(svc.Actions, specAction) } } return svc } // parseProperty parses a property from the spec. func (p *MIoTSpecParser) parseProperty(prop map[string]interface{}, svc *MIoTSpecService, svcTypeParts []string) *MIoTSpecProperty { piid := intVal(prop, "iid") propType := strVal(prop, "type") description := strVal(prop, "description") formatStr := strVal(prop, "format") accessRaw := prop["access"] if propType == "" || description == "" || formatStr == "" || accessRaw == nil { return nil } propTypeParts := strings.Split(propType, ":") // Parse access access := parseAccess(accessRaw) // Parse unit unit := strVal(prop, "unit") if unit == "none" { unit = "" } specProp := &MIoTSpecProperty{ SIID: svc.IID, PIID: piid, Type: propType, Description: description, Format: formatStr, Access: access, Unit: unit, NeedFilter: boolVal(prop, "need_filter"), Proprietary: len(propTypeParts) > 1 && propTypeParts[1] != "miot-spec-v2", } // Translate description typeKey := strings.Join(propTypeParts[:min(5, len(propTypeParts))], ":") specProp.DescriptionTrans = p.stdLib.PropertyTranslate(typeKey) if specProp.DescriptionTrans == "" { specProp.DescriptionTrans = description } // Parse value-range if vrRaw, ok := prop["value-range"]; ok { specProp.ValueRange = parseValueRange(vrRaw) } // Parse value-list if vlRaw, ok := prop["value-list"]; ok { specProp.ValueList = parseValueList(vlRaw, svcTypeParts, propTypeParts) } return specProp } // parseEvent parses an event from the spec. func (p *MIoTSpecParser) parseEvent(event map[string]interface{}, svc *MIoTSpecService) *MIoTSpecEvent { eiid := intVal(event, "iid") eventType := strVal(event, "type") description := strVal(event, "description") if eventType == "" || description == "" { return nil } eventTypeParts := strings.Split(eventType, ":") specEvent := &MIoTSpecEvent{ SIID: svc.IID, EIID: eiid, Type: eventType, Description: description, NeedFilter: boolVal(event, "need_filter"), Proprietary: len(eventTypeParts) > 1 && eventTypeParts[1] != "miot-spec-v2", } // Translate description typeKey := strings.Join(eventTypeParts[:min(5, len(eventTypeParts))], ":") specEvent.DescriptionTrans = p.stdLib.EventTranslate(typeKey) if specEvent.DescriptionTrans == "" { specEvent.DescriptionTrans = description } // Parse arguments if args, ok := event["arguments"].([]interface{}); ok { for _, arg := range args { specEvent.Arguments = append(specEvent.Arguments, fmt.Sprintf("%v", arg)) } } return specEvent } // parseAction parses an action from the spec. func (p *MIoTSpecParser) parseAction(action map[string]interface{}, svc *MIoTSpecService) *MIoTSpecAction { aiid := intVal(action, "iid") actionType := strVal(action, "type") description := strVal(action, "description") if actionType == "" || description == "" { return nil } actionTypeParts := strings.Split(actionType, ":") specAction := &MIoTSpecAction{ SIID: svc.IID, AIID: aiid, Type: actionType, Description: description, NeedFilter: boolVal(action, "need_filter"), Proprietary: len(actionTypeParts) > 1 && actionTypeParts[1] != "miot-spec-v2", } // Translate description typeKey := strings.Join(actionTypeParts[:min(5, len(actionTypeParts))], ":") specAction.DescriptionTrans = p.stdLib.ActionTranslate(typeKey) if specAction.DescriptionTrans == "" { specAction.DescriptionTrans = description } // Parse in/out if inRaw, ok := action["in"].([]interface{}); ok { for _, in := range inRaw { specAction.In = append(specAction.In, fmt.Sprintf("%v", in)) } } if outRaw, ok := action["out"].([]interface{}); ok { for _, out := range outRaw { specAction.Out = append(specAction.Out, fmt.Sprintf("%v", out)) } } return specAction } // skipProprietary returns true if the item is marked proprietary. func (p *MIoTSpecParser) skipProprietary(item map[string]interface{}) bool { if proprietary, ok := item["proprietary"]; ok { switch v := proprietary.(type) { case bool: return v case string: return v == "true" || v == "1" } } return false } // ============================================================================ // SpecStdLib — standard library for multi-language translation // ============================================================================ // SpecStdLib is the MIoT-Spec-V2 standard library for multi-language descriptions. type SpecStdLib struct { lang string devices map[string]map[string]string services map[string]map[string]string properties map[string]map[string]string events map[string]map[string]string actions map[string]map[string]string values map[string]map[string]string } // NewSpecStdLib creates a new SpecStdLib. func NewSpecStdLib(lang string) *SpecStdLib { if lang == "" { lang = DEFAULT_INTEGRATION_LANGUAGE } return &SpecStdLib{ lang: lang, devices: make(map[string]map[string]string), services: make(map[string]map[string]string), properties: make(map[string]map[string]string), events: make(map[string]map[string]string), actions: make(map[string]map[string]string), values: make(map[string]map[string]string), } } // Load loads standard library data from a parsed map. func (s *SpecStdLib) Load(data map[string]interface{}) { if data == nil { return } if d, ok := data["devices"].(map[string]interface{}); ok { s.devices = convertStringMapMap(d) } if sv, ok := data["services"].(map[string]interface{}); ok { s.services = convertStringMapMap(sv) } if p, ok := data["properties"].(map[string]interface{}); ok { s.properties = convertStringMapMap(p) } if e, ok := data["events"].(map[string]interface{}); ok { s.events = convertStringMapMap(e) } if a, ok := data["actions"].(map[string]interface{}); ok { s.actions = convertStringMapMap(a) } if v, ok := data["values"].(map[string]interface{}); ok { s.values = convertStringMapMap(v) } } // Dump exports the standard library as a map. func (s *SpecStdLib) Dump() map[string]interface{} { return map[string]interface{}{ "devices": s.devices, "services": s.services, "properties": s.properties, "events": s.events, "actions": s.actions, "values": s.values, } } // DeviceTranslate translates a device type key. func (s *SpecStdLib) DeviceTranslate(key string) string { return s.translate(s.devices, key) } // ServiceTranslate translates a service type key. func (s *SpecStdLib) ServiceTranslate(key string) string { return s.translate(s.services, key) } // PropertyTranslate translates a property type key. func (s *SpecStdLib) PropertyTranslate(key string) string { return s.translate(s.properties, key) } // EventTranslate translates an event type key. func (s *SpecStdLib) EventTranslate(key string) string { return s.translate(s.events, key) } // ActionTranslate translates an action type key. func (s *SpecStdLib) ActionTranslate(key string) string { return s.translate(s.actions, key) } // ValueTranslate translates a value key. func (s *SpecStdLib) ValueTranslate(key string) string { return s.translate(s.values, key) } // translate is the common translation logic. func (s *SpecStdLib) translate(m map[string]map[string]string, key string) string { if m == nil || key == "" { return "" } translations, ok := m[key] if !ok { return "" } if desc, ok := translations[s.lang]; ok { return desc } return translations[DEFAULT_INTEGRATION_LANGUAGE] } // Refresh fetches the latest standard library from cloud endpoints. func (s *SpecStdLib) Refresh() error { httpClient := &http.Client{Timeout: 30 * time.Second} stdLibs := map[string]map[string]map[string]string{ "devices": nil, "services": nil, "properties": nil, "events": nil, "actions": nil, "values": nil, } // Retry up to 3 times var lastErr error for attempt := 0; attempt < 3; attempt++ { if attempt > 0 { log.Printf("[spec] retry std lib refresh, attempt %d", attempt) time.Sleep(time.Duration(attempt) * time.Second) } var errs []error // Fetch templates from miot-spec.org types := []string{"device", "service", "property", "event", "action"} for i, name := range types { url := fmt.Sprintf("https://miot-spec.org/miot-spec-v2/template/list/%s", name) result, err := s.fetchTemplateList(httpClient, url) if err != nil { errs = append(errs, fmt.Errorf("%s: %w", name, err)) continue } stdLibs[types[i]] = result } // Fetch property values valResult, err := s.fetchPropertyValues(httpClient) if err != nil { errs = append(errs, fmt.Errorf("property_value: %w", err)) } else { stdLibs["values"] = valResult } if len(errs) == 0 { // Merge external std lib from CDN externalTypes := []string{"device", "service", "property", "event", "action", "property_value"} for i, name := range externalTypes { url := fmt.Sprintf("https://cdn.cnbj1.fds.api.mi-img.com/res-conf/xiaomi-home/std_ex_%s.json", name) extData, err := s.fetchJSONMapMap(httpClient, url) if err != nil { log.Printf("[spec] get external std lib failed, %s: %v", name, err) continue } if extData != nil { targetKey := types[min(i, len(types)-1)] if i == 5 { // property_value targetKey = "values" } if existing := stdLibs[targetKey]; existing != nil { for k, v := range extData { existing[k] = v } } } } // Apply to self if d, ok := stdLibs["devices"]; ok && d != nil { s.devices = d } if sv, ok := stdLibs["services"]; ok && sv != nil { s.services = sv } if p, ok := stdLibs["properties"]; ok && p != nil { s.properties = p } if e, ok := stdLibs["events"]; ok && e != nil { s.events = e } if a, ok := stdLibs["actions"]; ok && a != nil { s.actions = a } if v, ok := stdLibs["values"]; ok && v != nil { s.values = v } return nil } if len(errs) > 0 { lastErr = fmt.Errorf("std lib refresh errors: %v", errs) log.Printf("[spec] std lib refresh error: %v", lastErr) } } if lastErr != nil { return lastErr } return fmt.Errorf("std lib refresh failed after retries") } // fetchTemplateList fetches a template list from miot-spec.org. func (s *SpecStdLib) fetchTemplateList(client *http.Client, url string) (map[string]map[string]string, error) { data, err := s.fetchJSON(client, url) if err != nil { return nil, err } resultList, ok := data["result"].([]interface{}) if !ok { return nil, fmt.Errorf("invalid template list format: %s", url) } result := make(map[string]map[string]string) for _, itemRaw := range resultList { item, ok := itemRaw.(map[string]interface{}) if !ok { continue } itemType, ok := item["type"].(string) if !ok { continue } descRaw, ok := item["description"] if !ok { continue } descMap, ok := descRaw.(map[string]interface{}) if !ok { continue } // Normalize zh_cn -> zh-Hans, zh_hk/zh_tw -> zh-Hant translations := make(map[string]string) for k, v := range descMap { vs, ok := v.(string) if !ok { continue } key := k switch key { case "zh_cn": key = "zh-Hans" case "zh_hk", "zh_tw": key = "zh-Hant" } translations[key] = vs } result[itemType] = translations } return result, nil } // fetchPropertyValues fetches property value normalizations from miot-spec.org. func (s *SpecStdLib) fetchPropertyValues(client *http.Client) (map[string]map[string]string, error) { url := "https://miot-spec.org/miot-spec-v2/normalization/list/property_value" data, err := s.fetchJSON(client, url) if err != nil { return nil, err } resultList, ok := data["result"].([]interface{}) if !ok { return nil, fmt.Errorf("invalid property value format") } result := make(map[string]map[string]string) for _, itemRaw := range resultList { item, ok := itemRaw.(map[string]interface{}) if !ok { continue } urn, _ := item["urn"].(string) proName, _ := item["proName"].(string) normalization, _ := item["normalization"].(string) desc, _ := item["description"].(string) if urn == "" || proName == "" || normalization == "" { continue } key := fmt.Sprintf("%s|%s|%s", urn, proName, normalization) result[key] = map[string]string{ "zh-Hans": desc, "en": normalization, } } return result, nil } // fetchJSONMapMap fetches a JSON endpoint and returns map[string]map[string]string. func (s *SpecStdLib) fetchJSONMapMap(client *http.Client, url string) (map[string]map[string]string, error) { data, err := s.fetchJSON(client, url) if err != nil { return nil, err } result := make(map[string]map[string]string) for k, v := range data { vMap, ok := v.(map[string]interface{}) if !ok { continue } inner := make(map[string]string) for ik, iv := range vMap { if ivs, ok := iv.(string); ok { inner[ik] = ivs } } result[k] = inner } return result, nil } // fetchJSON is a helper to GET a URL and parse JSON. func (s *SpecStdLib) fetchJSON(client *http.Client, url string) (map[string]interface{}, error) { resp, err := client.Get(url) if err != nil { return nil, fmt.Errorf("GET %s: %w", url, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("GET %s: status %d", url, resp.StatusCode) } var result map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("parse JSON %s: %w", url, err) } return result, nil } // ============================================================================ // Helper functions // ============================================================================ // convertStringMapMap converts map[string]interface{} to map[string]map[string]string. func convertStringMapMap(input map[string]interface{}) map[string]map[string]string { result := make(map[string]map[string]string) for k, v := range input { inner, ok := v.(map[string]interface{}) if !ok { continue } innerMap := make(map[string]string) for ik, iv := range inner { if ivs, ok := iv.(string); ok { innerMap[ik] = ivs } } result[k] = innerMap } return result } // parseAccess parses the access field into a string slice. func parseAccess(access interface{}) []string { switch v := access.(type) { case []interface{}: result := make([]string, 0, len(v)) for _, item := range v { if s, ok := item.(string); ok { result = append(result, s) } } return result case string: return []string{v} } return nil } // parseValueRange parses a value-range from spec dict or list format. func parseValueRange(vr interface{}) *MIoTSpecValueRange { switch v := vr.(type) { case map[string]interface{}: return &MIoTSpecValueRange{ Min: v["min"], Max: v["max"], Step: v["step"], } case []interface{}: if len(v) >= 3 { return &MIoTSpecValueRange{ Min: v[0], Max: v[1], Step: v[2], } } } return nil } // parseValueList parses a value-list from spec format. func parseValueList(vl interface{}, svcTypeParts, propTypeParts []string) MIoTSpecValueList { items, ok := vl.([]interface{}) if !ok { return nil } // Deduplicate descriptions descCount := make(map[string]int) var result MIoTSpecValueList for _, itemRaw := range items { item, ok := itemRaw.(map[string]interface{}) if !ok { continue } value := item["value"] description := strVal(item, "description") name := strVal(item, "name") if description == "" { description = fmt.Sprintf("v_%v", value) } // Handle duplicate descriptions count := descCount[description] + 1 descCount[description] = count if count > 1 { name = fmt.Sprintf("%s_%d", name, count) description = fmt.Sprintf("%s_%d", description, count) } if name == "" { name = description } result = append(result, &MIoTSpecValueItem{ Value: value, Desc: description, Name: name, }) } return result } // strVal safely extracts a string value from a map. func strVal(m map[string]interface{}, key string) string { if v, ok := m[key]; ok { if s, ok := v.(string); ok { return s } return fmt.Sprintf("%v", v) } return "" } // intVal safely extracts an int value from a map. func intVal(m map[string]interface{}, key string) int { if v, ok := m[key]; ok { switch n := v.(type) { case float64: return int(n) case int: return n case json.Number: i, _ := n.Int64() return int(i) case string: i, _ := strconv.Atoi(n) return i } } return 0 } // boolVal safely extracts a bool value from a map. func boolVal(m map[string]interface{}, key string) bool { if v, ok := m[key]; ok { switch b := v.(type) { case bool: return b case string: return b == "true" || b == "1" } } return false } // ============================================================================ // MIoT Spec format mapping functions (aligned with Python miot_spec.py) // ============================================================================ // SpecFormatGoType maps a spec format string to a Go type indicator. // string→"str", bool→"bool", float→"float", default→"int". func SpecFormatGoType(format string) string { switch format { case "string", "str": return "str" case "bool": return "bool" case "float": return "float" default: return "int" } } // SpecPrecision calculates precision from a step value. // Aligns with Python: len(format(value[2], '.10f').rstrip('0').rstrip('.').split('.')[1]) if '.' in step_ else 0 func SpecPrecision(step interface{}) int { // Convert step to float64 string with high precision stepFloat := toFloat64(step) stepStr := formatFloat(stepFloat) if !strings.Contains(stepStr, ".") { return 0 } parts := strings.Split(stepStr, ".") return len(parts[1]) } // formatFloat formats a float with 10 decimal places and trims trailing zeros. func formatFloat(f float64) string { if f == 0 { return "0" } s := strconv.FormatFloat(f, 'f', 10, 64) s = strings.TrimRight(s, "0") s = strings.TrimRight(s, ".") return s } // ============================================================================ // SpecHash — simple spec ID hashing // ============================================================================ // SpecHash generates a hash from type and iid components. func SpecHash(typeStr string, iids ...int) uint32 { parts := []string{typeStr} for _, i := range iids { parts = append(parts, strconv.Itoa(i)) } return crc32.ChecksumIEEE([]byte(strings.Join(parts, "."))) } // min returns the minimum of two ints. func min(a, b int) int { if a < b { return a } return b } // ============================================================================ // MIoTSpecProperty methods — H1: eval_expr / value_format / value_precision // H3: writable / readable / notifiable // ============================================================================ // EvalExpr evaluates a simple expression in the Expr field. // If Expr is empty, returns srcValue unchanged. // Supports: "src_value * N", "src_value / N", "src_value + N", "src_value - N" func (p *MIoTSpecProperty) EvalExpr(srcValue interface{}) interface{} { if p.Expr == "" { return srcValue } expr := strings.TrimSpace(p.Expr) if expr == "src_value" || expr == "" { return srcValue } // Parse simple binary expression: src_value parts := strings.Fields(expr) if len(parts) != 3 || parts[0] != "src_value" { return srcValue } operand, err := strconv.ParseFloat(parts[2], 64) if err != nil { return srcValue } sv := toFloat64(srcValue) var result float64 switch parts[1] { case "*": result = sv * operand case "/": if operand == 0 { return srcValue } result = sv / operand case "+": result = sv + operand case "-": result = sv - operand default: return srcValue } return result } // ValueFormat converts a value to the target type based on the Format field. // string → int/float/bool as specified. func (p *MIoTSpecProperty) ValueFormat(value interface{}) interface{} { if value == nil { return nil } switch p.Format { case "int": switch v := value.(type) { case string: i, err := strconv.ParseFloat(v, 64) if err != nil { return int(toFloat64(v)) } return int(i) case float64: return int(v) case json.Number: f, _ := v.Float64() return int(f) } case "float": switch v := value.(type) { case string: f, _ := strconv.ParseFloat(v, 64) return f case int: return float64(v) case json.Number: f, _ := v.Float64() return f } case "bool": switch v := value.(type) { case bool: return v case string: return v == "true" || v == "True" || v == "1" case float64: return v != 0 case int: return v != 0 } } return value } // ValuePrecision applies precision rounding based on Format and Precision fields. // float → round to Precision decimal places. // int → align to ValueRange.Step. func (p *MIoTSpecProperty) ValuePrecision(value interface{}) interface{} { if value == nil { return nil } switch p.Format { case "float": f := toFloat64(value) return mathRound(f, p.Precision) case "int": v := toFloat64(value) if p.ValueRange != nil && p.ValueRange.Step != nil { step := toFloat64(p.ValueRange.Step) if step > 0 { v = math.Round(v/step) * step } } return int(math.Round(v)) } return value } // mathRound rounds a float64 to the specified number of decimal places. func mathRound(f float64, places int) float64 { if places <= 0 { return math.Round(f) } pow := math.Pow10(places) return math.Round(f*pow) / pow } // Writable returns true if the Access list contains "write". func (p *MIoTSpecProperty) Writable() bool { for _, a := range p.Access { if a == "write" { return true } } return false } // Readable returns true if the Access list contains "read". func (p *MIoTSpecProperty) Readable() bool { for _, a := range p.Access { if a == "read" { return true } } return false } // Notifiable returns true if the Access list contains "notify". func (p *MIoTSpecProperty) Notifiable() bool { for _, a := range p.Access { if a == "notify" { return true } } return false } // ============================================================================ // MIoTSpecValueList methods — H2: query helpers // ============================================================================ // Names returns all item names. func (vl MIoTSpecValueList) Names() []string { result := make([]string, 0, len(vl)) for _, item := range vl { result = append(result, item.Name) } return result } // Values returns all item values. func (vl MIoTSpecValueList) Values() []interface{} { result := make([]interface{}, 0, len(vl)) for _, item := range vl { result = append(result, item.Value) } return result } // Descriptions returns all item descriptions. func (vl MIoTSpecValueList) Descriptions() []string { result := make([]string, 0, len(vl)) for _, item := range vl { result = append(result, item.Desc) } return result } // GetValueByDescription looks up a value by its description. // Returns nil if not found. func (vl MIoTSpecValueList) GetValueByDescription(desc string) interface{} { for _, item := range vl { if item.Desc == desc { return item.Value } } return nil } // GetDescriptionByValue looks up a description by its value. // Returns "" if not found. func (vl MIoTSpecValueList) GetDescriptionByValue(value interface{}) string { for _, item := range vl { if fmt.Sprintf("%v", item.Value) == fmt.Sprintf("%v", value) { return item.Desc } } return "" } // Ensure unused import of math for future precision handling. var _ = math.Abs