Skip to content

Commit

Permalink
Add option for composed device type generation in ZAP XML
Browse files Browse the repository at this point in the history
  • Loading branch information
hasty committed Dec 9, 2024
1 parent ad3d67f commit cb808aa
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 17 deletions.
3 changes: 3 additions & 0 deletions cmd/zap/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand All @@ -41,13 +42,15 @@ 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))
options.Template = append(options.Template, generate.AsciiAttributes(options.AsciiSettings))
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)
Expand Down
172 changes: 172 additions & 0 deletions zap/generate/composed.go
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 7 additions & 0 deletions zap/generate/devicetypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type DeviceTypesPatcher struct {
clusterAliases map[string]string

generateFeatureXml bool
fullEndpointComposition bool
}

type DeviceTypePatcherOption func(dtp *DeviceTypesPatcher)
Expand All @@ -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 {
Expand Down
46 changes: 29 additions & 17 deletions zap/generate/requirements.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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}
Expand All @@ -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")
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit cb808aa

Please sign in to comment.