// Package specs provides SPEC property resolution — mapping semantic property // names (like "on", "mode", "target-temperature") to siid/piid pairs. // // This enables device control abstractions to work across different Xiaomi // device models that may assign different siid/piid numbering to the same // functional property. package specs import ( "strings" "git.zeroonesoft.cn/golib/xiaomihome/miot" ) // PropertyResolver resolves semantic property names to siid/piid pairs. // It indexes a device's SPEC instance by property type (Type field), // description (translated), and data format for flexible lookup. type PropertyResolver struct { spec *miot.MIoTSpecInstance byType map[string]*miot.MIoTSpecProperty // type → first matching property byDesc map[string]*miot.MIoTSpecProperty // description → property byFormat map[string][]*miot.MIoTSpecProperty // format → properties actionMap map[int]map[string]int // siid → (actionType → aiid) } // NewPropertyResolver creates a PropertyResolver from a SPEC instance. // Returns nil if spec is nil. func NewPropertyResolver(spec *miot.MIoTSpecInstance) *PropertyResolver { if spec == nil { return nil } r := &PropertyResolver{ spec: spec, byType: make(map[string]*miot.MIoTSpecProperty), byDesc: make(map[string]*miot.MIoTSpecProperty), byFormat: make(map[string][]*miot.MIoTSpecProperty), actionMap: make(map[int]map[string]int), } // Index all properties across all services for _, svc := range spec.Services { for i := range svc.Properties { prop := svc.Properties[i] // Index by type (first match wins for primary lookup) name := propShortName(prop.Type) if name != "" { if _, exists := r.byType[name]; !exists { r.byType[name] = prop } } // Index by translated description if prop.DescriptionTrans != "" { if _, exists := r.byDesc[prop.DescriptionTrans]; !exists { r.byDesc[prop.DescriptionTrans] = prop } } // Fallback: index by raw description if prop.Description != "" { if _, exists := r.byDesc[prop.Description]; !exists { r.byDesc[prop.Description] = prop } } // Index by format if prop.Format != "" { r.byFormat[prop.Format] = append(r.byFormat[prop.Format], prop) } } // Index actions for _, act := range svc.Actions { if r.actionMap[act.SIID] == nil { r.actionMap[act.SIID] = make(map[string]int) } if act.Type != "" { r.actionMap[act.SIID][act.Type] = act.AIID } } } return r } // FindByType looks up a property by its SPEC type string (e.g., "on", "mode", // "target-temperature", "fan-level", "brightness"). // Returns (siid, piid, found). func (r *PropertyResolver) FindByType(propType string) (siid, piid int, found bool) { if prop, ok := r.byType[propType]; ok { return prop.SIID, prop.PIID, true } return 0, 0, false } // FindByFormat looks up the first property with the given format string within // a specific service (siid). Returns (piid, found). // format examples: "bool", "uint8", "uint16", "float", "string" func (r *PropertyResolver) FindByFormat(format string, siid int) (piid int, found bool) { props, ok := r.byFormat[format] if !ok { return 0, false } for _, p := range props { if p.SIID == siid { return p.PIID, true } } return 0, false } // FindByDescription looks up a property by its description text (translated or raw). // Returns (siid, piid, found). func (r *PropertyResolver) FindByDescription(desc string) (siid, piid int, found bool) { if prop, ok := r.byDesc[desc]; ok { return prop.SIID, prop.PIID, true } return 0, 0, false } // FindAction looks up an action by its type within a specific service. // Returns (aiid, found). func (r *PropertyResolver) FindAction(siid int, actionType string) (aiid int, found bool) { if actions, ok := r.actionMap[siid]; ok { if aiid, ok := actions[actionType]; ok { return aiid, true } } return 0, false } // GetAllProperties returns all properties for a given siid. func (r *PropertyResolver) GetAllProperties(siid int) []*miot.MIoTSpecProperty { var result []*miot.MIoTSpecProperty for _, svc := range r.spec.Services { if svc.IID == siid { return svc.Properties } } return result } // GetProperty returns a specific property by siid/piid. func (r *PropertyResolver) GetProperty(siid, piid int) *miot.MIoTSpecProperty { for _, svc := range r.spec.Services { if svc.IID != siid { continue } for _, p := range svc.Properties { if p.PIID == piid { return p } } } return nil } // Spec returns the underlying SPEC instance. func (r *PropertyResolver) Spec() *miot.MIoTSpecInstance { return r.spec } // propShortName extracts the short name from a SPEC property URN. func propShortName(urn string) string { parts := strings.Split(urn, ":") if len(parts) >= 4 && parts[2] == "property" { return parts[3] } return urn }