From cb808aa24e47095c1068e896c3123224b3e8a55b Mon Sep 17 00:00:00 2001 From: Hasty Granbery Date: Mon, 9 Dec 2024 15:16:20 -0800 Subject: [PATCH] Add option for composed device type generation in ZAP XML --- cmd/zap/cmd.go | 3 + zap/generate/composed.go | 172 +++++++++++++++++++++++++++++++++++ zap/generate/devicetypes.go | 7 ++ zap/generate/requirements.go | 46 ++++++---- 4 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 zap/generate/composed.go diff --git a/cmd/zap/cmd.go b/cmd/zap/cmd.go index 17841ad..041aedc 100644 --- a/cmd/zap/cmd.go +++ b/cmd/zap/cmd.go @@ -21,6 +21,7 @@ func init() { Command.Flags().String("sdkRoot", "connectedhomeip", "the root of your clone of project-chip/connectedhomeip") Command.Flags().Bool("featureXML", true, "write new style feature XML") Command.Flags().Bool("conformanceXML", true, "write new style conformance XML") + Command.Flags().Bool("endpointCompositionXML", false, "write new style endpoint composition XML") Command.Flags().Bool("specOrder", false, "write ZAP template XML in spec order") } @@ -41,6 +42,7 @@ func zapTemplates(cmd *cobra.Command, args []string) (err error) { featureXML, _ := cmd.Flags().GetBool("featureXML") options.Template = append(options.Template, generate.GenerateFeatureXML(featureXML)) conformanceXML, _ := cmd.Flags().GetBool("conformanceXML") + endpointCompositionXML, _ := cmd.Flags().GetBool("endpointCompositionXML") specOrder, _ := cmd.Flags().GetBool("specOrder") options.Template = append(options.Template, generate.GenerateConformanceXML(conformanceXML)) options.Template = append(options.Template, generate.SpecOrder(specOrder)) @@ -48,6 +50,7 @@ func zapTemplates(cmd *cobra.Command, args []string) (err error) { options.Template = append(options.Template, generate.SpecRoot(specRoot)) options.DeviceTypes = append(options.DeviceTypes, generate.DeviceTypePatcherGenerateFeatureXML(featureXML)) + options.DeviceTypes = append(options.DeviceTypes, generate.DeviceTypePatcherFullEndpointComposition(endpointCompositionXML)) var output generate.Output output, err = generate.Pipeline(cxt, specRoot, sdkRoot, args, options) diff --git a/zap/generate/composed.go b/zap/generate/composed.go new file mode 100644 index 0000000..3b3bd81 --- /dev/null +++ b/zap/generate/composed.go @@ -0,0 +1,172 @@ +package generate + +import ( + "log/slog" + "strings" + + "github.com/beevik/etree" + "github.com/project-chip/alchemy/internal/xml" + "github.com/project-chip/alchemy/matter" + "github.com/project-chip/alchemy/matter/conformance" + "github.com/project-chip/alchemy/matter/spec" + "github.com/project-chip/alchemy/matter/types" +) + +func (p DeviceTypesPatcher) setEndpointCompositionElement(spec *spec.Specification, cxt conformance.Context, deviceType *matter.DeviceType, parent *etree.Element) { + + composedDeviceTypes, composedDeviceTypeRequirements, composedDeviceTypeElementRequirements := p.buildComposedDeviceRequirements(deviceType, spec) + + endpointCompositionElement := parent.SelectElement("endpointComposition") + if len(composedDeviceTypes) == 0 { + if endpointCompositionElement != nil { + parent.RemoveChild(endpointCompositionElement) + } + return + } + if endpointCompositionElement == nil { + endpointCompositionElement = parent.CreateElement("endpointComposition") + } + xml.SetOrCreateSimpleElement(endpointCompositionElement, "compositionType", "tree") + endpointElement := xml.SetOrCreateSimpleElement(endpointCompositionElement, "endpoint", "") + endpointElement.RemoveAttr("conformance") + endpointElement.CreateAttr("constraint", "min 1") + xml.RemoveElements(endpointElement, "deviceType") + for _, dt := range composedDeviceTypes { + dte := endpointElement.CreateElement("deviceType") + req := composedDeviceTypeRequirements[dt] + dte.CreateAttr("id", dt.ID.HexString()) + dte.CreateAttr("name", dt.Name) + renderConformance(spec, dt, deviceType, req.Conformance, dte) + clusterRequirements := make([]*matter.ClusterRequirement, 0, len(dt.ClusterRequirements)) + for _, cr := range dt.ClusterRequirements { + clusterRequirements = append(clusterRequirements, cr.Clone()) + } + elementRequirements := make([]*matter.ElementRequirement, 0, len(dt.ElementRequirements)) + for _, dtr := range dt.ElementRequirements { + elementRequirements = append(elementRequirements, dtr.Clone()) + } + creq := composedDeviceTypeElementRequirements[dt] + for _, cdtr := range creq { + if cdtr.Element == types.EntityTypeUnknown { // Element Requirements with no feature are changing the qualities of the cluster requirement + var matched bool + for _, cr := range clusterRequirements { + if cdtr.ClusterID.Valid() && !cdtr.ClusterID.Equals(cr.ClusterID) { + continue + } + if !strings.EqualFold(cdtr.ClusterName, cr.ClusterName) { + continue + } + cdtr.Quality.Inherit(cr.Quality) + cr.Quality = cdtr.Quality + if len(cdtr.Conformance) > 0 { + cr.Conformance = cdtr.Conformance.CloneSet() + } + matched = true + break + } + if !matched { + slog.Warn("Composed device type requirement references unknown cluster", + slog.String("deviceTypeId", deviceType.ID.HexString()), + slog.String("deviceTypeName", deviceType.Name), + slog.String("composedDeviceTypeId", dt.ID.HexString()), + slog.String("composedDeviceTypeName", dt.Name), + slog.String("clusterId", cdtr.ClusterID.HexString()), + slog.String("clusterName", cdtr.ClusterName), + ) + } + } else { + var matched bool + for i, dtr := range elementRequirements { + if cdtr.ClusterID.Valid() && !cdtr.ClusterID.Equals(dtr.ClusterID) { + continue + } + if !strings.EqualFold(cdtr.ClusterName, dtr.ClusterName) { + continue + } + if cdtr.Element != dtr.Element { + continue + } + if !strings.EqualFold(cdtr.Name, dtr.Name) { + continue + } + if !strings.EqualFold(cdtr.Field, dtr.Field) { + continue + } + cdter := cdtr.ElementRequirement.Clone() + if cdtr.Constraint == nil && dtr.Constraint != nil { + cdter.Constraint = dtr.Constraint.Clone() + } + if len(cdtr.Conformance) == 0 && len(dtr.Conformance) > 0 { + cdter.Conformance = dtr.Conformance.CloneSet() + } + cdter.Access.Inherit(dtr.Access) + cdter.Quality.Inherit(dtr.Quality) + elementRequirements[i] = cdter + matched = true + break + } + if !matched { + elementRequirements = append(elementRequirements, &cdtr.ElementRequirement) + } + } + + } + clusterRequirementsByID := p.buildClusterRequirements(spec, cxt, clusterRequirements, elementRequirements) + p.setClustersElement(spec, cxt, dt, clusterRequirementsByID, dte) + } +} + +func (DeviceTypesPatcher) buildComposedDeviceRequirements(deviceType *matter.DeviceType, spec *spec.Specification) ([]*matter.DeviceType, map[*matter.DeviceType]*matter.DeviceTypeRequirement, map[*matter.DeviceType][]*matter.ComposedDeviceTypeRequirement) { + var composedDeviceTypes []*matter.DeviceType + composedDeviceTypeRequirements := make(map[*matter.DeviceType]*matter.DeviceTypeRequirement) + for _, dtr := range deviceType.DeviceTypeRequirements { + var dt *matter.DeviceType + var ok bool + if dtr.DeviceTypeID.Valid() { + dt, ok = spec.DeviceTypesByID[dtr.DeviceTypeID.Value()] + if !ok { + slog.Warn("unknown composed device type ID", slog.String("deviceTypeId", dtr.DeviceTypeID.HexString())) + continue + } + } else { + dt, ok = spec.DeviceTypesByName[dtr.DeviceTypeName] + if !ok { + slog.Warn("unknown composed device type name", slog.String("deviceTypeName", dtr.DeviceTypeName)) + continue + } + } + composedDeviceTypes = append(composedDeviceTypes, dt) + _, ok = composedDeviceTypeRequirements[dt] + if ok { + slog.Warn("Duplicate composed device type requirement, ignoring...", slog.String("deviceTypeId", deviceType.ID.HexString()), slog.String("deviceTypeName", deviceType.Name), slog.String("composedDeviceTypeId", dt.ID.HexString()), slog.String("composedDeviceTypeName", dt.Name)) + continue + } + composedDeviceTypeRequirements[dt] = dtr + } + composedDeviceTypeElementRequirements := make(map[*matter.DeviceType][]*matter.ComposedDeviceTypeRequirement) + for _, cdtr := range deviceType.ComposedDeviceTypeRequirements { + var dt *matter.DeviceType + var ok bool + if cdtr.DeviceTypeID.Valid() { + dt, ok = spec.DeviceTypesByID[cdtr.DeviceTypeID.Value()] + if !ok { + slog.Warn("unknown composed device type ID", slog.String("deviceTypeId", cdtr.DeviceTypeID.HexString())) + continue + } + } else { + dt, ok = spec.DeviceTypesByName[cdtr.DeviceTypeName] + if !ok { + slog.Warn("unknown composed device type name", slog.String("deviceTypeName", cdtr.DeviceTypeName)) + continue + } + } + _, ok = composedDeviceTypeRequirements[dt] + if !ok { + // Hunh; there's an element requirement for a device type that wasn't in the list of device types; we'll just pretend it was there and optional + composedDeviceTypes = append(composedDeviceTypes, dt) + composedDeviceTypeRequirements[dt] = &matter.DeviceTypeRequirement{DeviceTypeID: dt.ID.Clone(), DeviceTypeName: dt.Name, Conformance: conformance.Set{&conformance.Optional{}}} + } + composedDeviceTypeElementRequirements[dt] = append(composedDeviceTypeElementRequirements[dt], cdtr) + } + return composedDeviceTypes, composedDeviceTypeRequirements, composedDeviceTypeElementRequirements +} diff --git a/zap/generate/devicetypes.go b/zap/generate/devicetypes.go index 093ccf6..536a2de 100644 --- a/zap/generate/devicetypes.go +++ b/zap/generate/devicetypes.go @@ -24,6 +24,7 @@ type DeviceTypesPatcher struct { clusterAliases map[string]string generateFeatureXml bool + fullEndpointComposition bool } type DeviceTypePatcherOption func(dtp *DeviceTypesPatcher) @@ -34,6 +35,12 @@ func DeviceTypePatcherGenerateFeatureXML(generate bool) DeviceTypePatcherOption } } +func DeviceTypePatcherFullEndpointComposition(generate bool) DeviceTypePatcherOption { + return func(dtp *DeviceTypesPatcher) { + dtp.fullEndpointComposition = generate + } +} + func NewDeviceTypesPatcher(sdkRoot string, spec *spec.Specification, clusterAliases pipeline.Map[string, []string], options ...DeviceTypePatcherOption) *DeviceTypesPatcher { dtp := &DeviceTypesPatcher{sdkRoot: sdkRoot, spec: spec, clusterAliases: make(map[string]string)} clusterAliases.Range(func(cluster string, aliases []string) bool { diff --git a/zap/generate/requirements.go b/zap/generate/requirements.go index a91f30c..d6965a5 100644 --- a/zap/generate/requirements.go +++ b/zap/generate/requirements.go @@ -32,18 +32,6 @@ func (p DeviceTypesPatcher) applyDeviceTypeToElement(spec *spec.Specification, d xml.SetOrCreateSimpleElement(dte, "deviceId", deviceType.ID.HexString(), "name", "domain", "typeName", "profileId").CreateAttr("editable", "false") xml.SetOrCreateSimpleElement(dte, "class", deviceType.Class, "name", "domain", "typeName", "profileId", "deviceId") xml.SetOrCreateSimpleElement(dte, "scope", deviceType.Scope, "name", "domain", "typeName", "profileId", "deviceId", "class") - clustersElement := dte.SelectElement("clusters") - if len(deviceType.ClusterRequirements) == 0 { - if clustersElement != nil { - dte.RemoveChild(clustersElement) - } - return - } - if clustersElement == nil { - clustersElement = dte.CreateElement("clusters") - } - clusterRequirementsByID := make(map[uint64]*clusterRequirements) - var hasClient, hasServer bool for _, cr := range deviceType.ClusterRequirements { @@ -70,7 +58,18 @@ func (p DeviceTypesPatcher) applyDeviceTypeToElement(spec *spec.Specification, d "Server": hasServer, }, } - for _, cr := range deviceType.ClusterRequirements { + + if p.fullEndpointComposition { + p.setEndpointCompositionElement(spec, cxt, deviceType, dte) + } + clusterRequirementsByID := p.buildClusterRequirements(spec, cxt, deviceType.ClusterRequirements, deviceType.ElementRequirements) + p.setClustersElement(spec, cxt, deviceType, clusterRequirementsByID, dte) + return +} + +func (p *DeviceTypesPatcher) buildClusterRequirements(spec *spec.Specification, conformanceContext conformance.Context, clusterReqs []*matter.ClusterRequirement, elementReqs []*matter.ElementRequirement) (clusterRequirementsByID map[uint64]*clusterRequirements) { + clusterRequirementsByID = make(map[uint64]*clusterRequirements) + for _, cr := range clusterReqs { if !cr.ClusterID.Valid() { continue } @@ -82,7 +81,7 @@ func (p DeviceTypesPatcher) applyDeviceTypeToElement(spec *spec.Specification, d crr.requirementsFromDeviceType = append(crr.requirementsFromDeviceType, cr) } - for _, er := range deviceType.ElementRequirements { + for _, er := range elementReqs { if !er.ClusterID.Valid() { continue } @@ -116,9 +115,9 @@ func (p DeviceTypesPatcher) applyDeviceTypeToElement(spec *spec.Specification, d crr.requirementsFromBaseDeviceType = append(crr.requirementsFromBaseDeviceType, cr) slog.Debug("adding base device type cluster requirement", slog.String("cluster", cr.ClusterName)) } else if !conformance.IsMandatory(cr.Conformance) { - conf, confErr := cr.Conformance.Eval(cxt) + conf, confErr := cr.Conformance.Eval(conformanceContext) if confErr != nil { - slog.Warn("Error evaluating conformance of cluster requirement", slog.String("deviceTypeId", deviceType.ID.HexString()), slog.String("clusterName", cr.ClusterName), slog.Any("error", confErr)) + slog.Warn("Error evaluating conformance of cluster requirement", slog.String("clusterName", cr.ClusterName), slog.Any("error", confErr)) } else if conf == conformance.StateMandatory { // If the Base Device Type has a requirement that is not plain Mandatory ("M"), but it returns Mandatory when evaulated, then include it crr = &clusterRequirements{id: cr.ClusterID, name: cr.ClusterName} @@ -127,6 +126,20 @@ func (p DeviceTypesPatcher) applyDeviceTypeToElement(spec *spec.Specification, d } } } + return +} + +func (p DeviceTypesPatcher) setClustersElement(spec *spec.Specification, cxt conformance.Context, deviceType *matter.DeviceType, clusterRequirementsByID map[uint64]*clusterRequirements, parent *etree.Element) { + clustersElement := parent.SelectElement("clusters") + if len(deviceType.ClusterRequirements) == 0 { + if clustersElement != nil { + parent.RemoveChild(clustersElement) + } + return + } + if clustersElement == nil { + clustersElement = parent.CreateElement("clusters") + } for _, include := range clustersElement.SelectElements("include") { ca := include.SelectAttr("cluster") @@ -163,7 +176,6 @@ func (p DeviceTypesPatcher) applyDeviceTypeToElement(spec *spec.Specification, d } } } - return } func (p *DeviceTypesPatcher) setIncludeAttributes(clustersElement *etree.Element, include *etree.Element, spec *spec.Specification, deviceType *matter.DeviceType, cr *clusterRequirements, cxt conformance.Context) {