Files
xiaomihome/miot/spec_parser.go
T
4566704 3a44cb9e6a feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写)
- 支持云端 API 调用与局域网设备发现(mDNS)
- 集成国际化(i18n)多语言支持
- 添加 MIoT 设备规约解析器(spec_parser)
- 包含单元测试与使用示例
2026-06-28 22:05:49 +08:00

1290 lines
33 KiB
Go

// 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 <op> <number>
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