diff --git a/dm/devicetype.go b/dm/devicetype.go index b91a316f..9fc48736 100644 --- a/dm/devicetype.go +++ b/dm/devicetype.go @@ -12,6 +12,7 @@ import ( "github.com/project-chip/alchemy/asciidoc" "github.com/project-chip/alchemy/internal" "github.com/project-chip/alchemy/matter" + "github.com/project-chip/alchemy/matter/conformance" "github.com/project-chip/alchemy/matter/types" ) @@ -120,7 +121,7 @@ func renderDeviceType(deviceType *matter.DeviceType) (output string, err error) if err != nil { return } - err = renderElementRequirements(deviceType, cr, clx) + err = renderElementRequirements(deviceType, cr, deviceType.ElementRequirements, clx) if err != nil { return } @@ -128,12 +129,33 @@ func renderDeviceType(deviceType *matter.DeviceType) (output string, err error) } } + type deviceTypeInstance struct { + DeviceTypeName string + Label string + } + type deviceTypeRequirements struct { + DeviceType *matter.DeviceType + deviceRequirements []*matter.DeviceTypeRequirement clusterRequirements []*matter.DeviceTypeClusterRequirement - elementRequirements map[*matter.Cluster][]*matter.ElementRequirement + elementRequirements map[string][]*matter.ElementRequirement } - dtrs := make(map[*matter.DeviceType]*deviceTypeRequirements) + dtrs := make(map[deviceTypeInstance]*deviceTypeRequirements) + + for _, dr := range deviceType.DeviceTypeRequirements { + if dr.DeviceType == nil { + continue + } + + key := deviceTypeInstance{DeviceTypeName: dr.DeviceType.Name, Label: ""} + dtr, ok := dtrs[key] + if !ok { + dtr = &deviceTypeRequirements{DeviceType: dr.DeviceType} + dtrs[key] = dtr + } + dtr.deviceRequirements = append(dtr.deviceRequirements, dr) + } for _, cr := range deviceType.ComposedDeviceTypeClusterRequirements { if cr.DeviceType == nil { @@ -143,10 +165,15 @@ func renderDeviceType(deviceType *matter.DeviceType) (output string, err error) continue } - dtr, ok := dtrs[cr.DeviceType] + label := cr.InstanceLabel + if label == cr.DeviceType.Name { + label = "" + } + key := deviceTypeInstance{DeviceTypeName: cr.DeviceType.Name, Label: label} + dtr, ok := dtrs[key] if !ok { - dtr = &deviceTypeRequirements{} - dtrs[cr.DeviceType] = dtr + dtr = &deviceTypeRequirements{DeviceType: cr.DeviceType} + dtrs[key] = dtr } dtr.clusterRequirements = append(dtr.clusterRequirements, cr) } @@ -158,36 +185,110 @@ func renderDeviceType(deviceType *matter.DeviceType) (output string, err error) if er.ElementRequirement.Cluster == nil { continue } - dtr, ok := dtrs[er.DeviceType] + label := er.InstanceLabel + if label == er.DeviceType.Name { + label = "" + } + key := deviceTypeInstance{DeviceTypeName: er.DeviceType.Name, Label: label} + dtr, ok := dtrs[key] if !ok { - continue + dtr = &deviceTypeRequirements{DeviceType: er.DeviceType} + dtrs[key] = dtr } + + foundCluster := false + for _, cr := range dtr.clusterRequirements { + if cr.ClusterRequirement.ClusterID.Equals(er.ElementRequirement.ClusterID) { + foundCluster = true + break + } + } + if !foundCluster { + cr := matter.NewClusterRequirement(er.DeviceType, er.Source()) + cr.ClusterID = er.ElementRequirement.ClusterID + cr.ClusterName = er.ElementRequirement.ClusterName + cr.Interface = matter.InterfaceServer // Default to server + + + dtcr := matter.NewDeviceTypeClusterRequirement(er.DeviceType, cr, er.Source()) + dtcr.DeviceTypeID = er.DeviceTypeID + dtcr.DeviceTypeName = er.DeviceTypeName + dtcr.InstanceLabel = er.InstanceLabel + + dtr.clusterRequirements = append(dtr.clusterRequirements, dtcr) + } + if dtr.elementRequirements == nil { - dtr.elementRequirements = make(map[*matter.Cluster][]*matter.ElementRequirement) + dtr.elementRequirements = make(map[string][]*matter.ElementRequirement) } - dtr.elementRequirements[er.ElementRequirement.Cluster] = append(dtr.elementRequirements[er.ElementRequirement.Cluster], er.ElementRequirement) + cidStr := er.ElementRequirement.ClusterID.HexString() + dtr.elementRequirements[cidStr] = append(dtr.elementRequirements[cidStr], er.ElementRequirement) } if len(dtrs) > 0 { cx := c.CreateElement("composedDeviceTypes") - deviceTypes := make([]*matter.DeviceType, 0, len(dtrs)) - for _, dtr := range dtrs { - deviceTypes = append(deviceTypes, dtr.clusterRequirements[0].DeviceType) + instances := make([]deviceTypeInstance, 0, len(dtrs)) + for key := range dtrs { + instances = append(instances, key) } - slices.SortStableFunc(deviceTypes, func(a, b *matter.DeviceType) int { - return strings.Compare(a.Name, b.Name) - }) - for _, dt := range deviceTypes { - dtr, ok := dtrs[dt] - if !ok { - continue + slices.SortStableFunc(instances, func(a, b deviceTypeInstance) int { + cmp := strings.Compare(a.DeviceTypeName, b.DeviceTypeName) + if cmp != 0 { + return cmp } + return strings.Compare(a.Label, b.Label) + }) + for _, inst := range instances { + dtr := dtrs[inst] + dt := dtr.DeviceType dte := cx.CreateElement("deviceType") if dt.ID.Valid() { dte.CreateAttr("deviceTypeId", dt.ID.HexString()) } dte.CreateAttr("deviceTypeName", dt.Name) + + location := matter.DeviceTypeRequirementLocationUnknown + if len(dtr.deviceRequirements) > 0 { + location = dtr.deviceRequirements[0].Location + } else { + // Fallback to base requirement for the same device type + baseKey := deviceTypeInstance{DeviceTypeName: dt.Name, Label: ""} + if baseDtr, ok := dtrs[baseKey]; ok && len(baseDtr.deviceRequirements) > 0 { + location = baseDtr.deviceRequirements[0].Location + } + } + if location != matter.DeviceTypeRequirementLocationUnknown { + dte.CreateAttr("deviceTypeLocation", location.String()) + } + + var baseConformance conformance.Set + for _, dr := range deviceType.DeviceTypeRequirements { + if dr.DeviceType == dt { + baseConformance = dr.Conformance + break + } + } + + if len(dtr.deviceRequirements) > 0 { + err = renderConformanceElement(dtr.deviceRequirements[0].Conformance, dte, nil) + if err != nil { + return + } + err = renderConstraintElement(dtr.deviceRequirements[0].Constraint, nil, dte, nil) + if err != nil { + return + } + } else if inst.Label != "" { + if baseConformance != nil { + err = renderConformanceElement(baseConformance, dte, nil) + if err != nil { + return + } + } + + } + if len(dtr.clusterRequirements) > 0 { crx := dte.CreateElement("clusterRequirements") reqs := make([]*matter.DeviceTypeClusterRequirement, len(dtr.clusterRequirements)) @@ -203,46 +304,20 @@ func renderDeviceType(deviceType *matter.DeviceType) (output string, err error) clx := crx.CreateElement("cluster") clx.CreateAttr("id", cr.ClusterRequirement.ClusterID.HexString()) clx.CreateAttr("name", cr.ClusterRequirement.ClusterName) - err = renderConformanceElement(cr.ClusterRequirement.Conformance, clx, nil) - if err != nil { - return + if len(cr.ClusterRequirement.Conformance) > 0 { + err = renderConformanceElement(cr.ClusterRequirement.Conformance, clx, nil) + if err != nil { + return + } } renderQuality(clx, cr.ClusterRequirement.Quality) - renderElementRequirements(dt, cr.ClusterRequirement, clx) + renderElementRequirements(dt, cr.ClusterRequirement, dtr.elementRequirements[cr.ClusterRequirement.ClusterID.HexString()], clx) } } } } - if len(deviceType.ComposedDeviceTypeClusterRequirements) > 0 { - cx := c.CreateElement("composedDeviceTypes") - reqs := make([]*matter.ClusterRequirement, len(deviceType.ClusterRequirements)) - copy(reqs, deviceType.ClusterRequirements) - slices.SortStableFunc(reqs, sortClusterRequirements) - for _, cr := range reqs { - clx := cx.CreateElement("cluster") - clx.CreateAttr("id", cr.ClusterID.HexString()) - clx.CreateAttr("name", cr.ClusterName) - switch cr.Interface { - case matter.InterfaceClient: - clx.CreateAttr("side", "client") - case matter.InterfaceServer: - clx.CreateAttr("side", "server") - } - renderQuality(clx, cr.Quality) - err = renderConformanceElement(cr.Conformance, clx, nil) - if err != nil { - return - } - err = renderElementRequirements(deviceType, cr, clx) - if err != nil { - return - } - - } - } - x.Indent(2) var b bytes.Buffer @@ -265,9 +340,9 @@ type commandRequirement struct { fields []*matter.ElementRequirement } -func renderElementRequirements(deviceType *matter.DeviceType, cr *matter.ClusterRequirement, clx *etree.Element) (err error) { +func renderElementRequirements(deviceType *matter.DeviceType, cr *matter.ClusterRequirement, ers []*matter.ElementRequirement, clx *etree.Element) (err error) { erMap := make(map[types.EntityType][]*matter.ElementRequirement) - for _, er := range deviceType.ElementRequirements { + for _, er := range ers { if er.ClusterID.Equals(cr.ClusterID) { erMap[er.Element] = append(erMap[er.Element], er) } @@ -276,7 +351,7 @@ func renderElementRequirements(deviceType *matter.DeviceType, cr *matter.Cluster var attributeRequirements []*matter.ElementRequirement var commandRequirements []*commandRequirement var eventRequirements []*matter.ElementRequirement - for _, er := range deviceType.ElementRequirements { + for _, er := range ers { if er.ClusterID.Equals(cr.ClusterID) { switch er.Element { case types.EntityTypeFeature: diff --git a/matter/composition.go b/matter/composition.go index a7371c68..bb16a022 100644 --- a/matter/composition.go +++ b/matter/composition.go @@ -63,6 +63,7 @@ type DeviceTypeClusterRequirement struct { ClusterRequirement *ClusterRequirement Origin RequirementOrigin + InstanceLabel string DeviceType *DeviceType DeviceTypeRequirement *DeviceTypeRequirement @@ -78,6 +79,7 @@ func (dtcr *DeviceTypeClusterRequirement) Clone() *DeviceTypeClusterRequirement DeviceTypeName: dtcr.DeviceTypeName, ClusterRequirement: dtcr.ClusterRequirement, Origin: dtcr.Origin, + InstanceLabel: dtcr.InstanceLabel, DeviceType: dtcr.DeviceType, DeviceTypeRequirement: dtcr.DeviceTypeRequirement, } @@ -90,6 +92,7 @@ type DeviceTypeElementRequirement struct { ElementRequirement *ElementRequirement Origin RequirementOrigin + InstanceLabel string DeviceType *DeviceType DeviceTypeRequirement *DeviceTypeRequirement @@ -105,6 +108,7 @@ func (dter *DeviceTypeElementRequirement) Clone() *DeviceTypeElementRequirement DeviceTypeName: dter.DeviceTypeName, ElementRequirement: dter.ElementRequirement, Origin: dter.Origin, + InstanceLabel: dter.InstanceLabel, DeviceType: dter.DeviceType, DeviceTypeRequirement: dter.DeviceTypeRequirement, } diff --git a/matter/devicetype.go b/matter/devicetype.go index 924126fc..410858b7 100644 --- a/matter/devicetype.go +++ b/matter/devicetype.go @@ -228,6 +228,7 @@ const ( DeviceTypeRequirementLocationChildEndpoint DeviceTypeRequirementLocationRootEndpoint DeviceTypeRequirementLocationDescendantEndpoint + DeviceTypeRequirementLocationAnywhere ) var ( @@ -237,6 +238,7 @@ var ( DeviceTypeRequirementLocationChildEndpoint: "childEndpoint", DeviceTypeRequirementLocationRootEndpoint: "rootEndpoint", DeviceTypeRequirementLocationDescendantEndpoint: "descendantEndpoint", + DeviceTypeRequirementLocationAnywhere: "anyEndpoint", } ) diff --git a/matter/spec/requirements.go b/matter/spec/requirements.go index ce694701..f2d74477 100644 --- a/matter/spec/requirements.go +++ b/matter/spec/requirements.go @@ -163,7 +163,7 @@ func (library *Library) toComposedDeviceTypeClusterRequirements(reader asciidoc. } return } - for row := range ti.ContentRows() { + for row, category := range ti.CategorizedRows(reader) { var cr *matter.ClusterRequirement cr, err = library.toClusterRequirement(reader, deviceType, ti, row) if err != nil { @@ -171,6 +171,7 @@ func (library *Library) toComposedDeviceTypeClusterRequirements(reader asciidoc. } dtcr := matter.NewDeviceTypeClusterRequirement(deviceType, cr, row) + dtcr.InstanceLabel = category dtcr.DeviceTypeID, err = ti.ReadID(reader, row, matter.TableColumnDeviceID) if err != nil { return @@ -230,13 +231,14 @@ func (library *Library) toComposedDeviceTypeElementRequirements(reader asciidoc. } return } - for row := range ti.ContentRows() { + for row, category := range ti.CategorizedRows(reader) { var er matter.ElementRequirement er, err = library.toElementRequirement(reader, d, ti, row, deviceType) if err != nil { return } dter := matter.NewDeviceTypeElementRequirement(deviceType, &er, row) + dter.InstanceLabel = category dter.DeviceTypeID, err = ti.ReadID(reader, row, matter.TableColumnDeviceID) if err != nil { return @@ -259,6 +261,7 @@ func (library *Library) toComposedDeviceTypeElementRequirements(reader asciidoc. dtcr := matter.NewDeviceTypeClusterRequirement(deviceType, cr, row) dtcr.DeviceTypeID = dter.DeviceTypeID dtcr.DeviceTypeName = dter.DeviceTypeName + dtcr.InstanceLabel = category if len(dter.ElementRequirement.Conformance) > 0 { dtcr.ClusterRequirement.Conformance = dter.ElementRequirement.Conformance.CloneSet() } diff --git a/matter/spec/table_info.go b/matter/spec/table_info.go index a7c1853b..8ca71cb8 100644 --- a/matter/spec/table_info.go +++ b/matter/spec/table_info.go @@ -99,6 +99,31 @@ func (ti *TableInfo) ContentRows() iter.Seq[*asciidoc.TableRow] { } } +func (ti *TableInfo) CategorizedRows(reader asciidoc.Reader) iter.Seq2[*asciidoc.TableRow, string] { + return func(yield func(*asciidoc.TableRow, string) bool) { + var currentCategory string + for i := ti.HeaderRowIndex + 1; i < len(ti.Rows); i++ { + row := ti.Rows[i] + if len(row.Elements) > 0 { + firstCell := row.Cell(0) + if firstCell.Format.Span.Column.IsSet && firstCell.Format.Span.Column.Value == len(row.Elements) { + var err error + currentCategory, err = RenderTableCell(reader, firstCell) + if err != nil { + slog.Error("failed to render category cell", log.Element("source", ti.Doc.Path, firstCell), slog.Any("error", err)) + currentCategory = "" + } + currentCategory = strings.TrimSpace(currentCategory) + continue + } + } + if !yield(row, currentCategory) { + return + } + } + } +} + func (ti *TableInfo) ReadString(reader asciidoc.Reader, row *asciidoc.TableRow, columns ...matter.TableColumn) (string, error) { for _, column := range columns { offset, ok := ti.ColumnMap[column] @@ -381,6 +406,8 @@ func (ti *TableInfo) ReadLocation(reader asciidoc.Reader, row *asciidoc.TableRow relation = matter.DeviceTypeRequirementLocationRootEndpoint case "Descendant", "DescendantEndpoint": relation = matter.DeviceTypeRequirementLocationDescendantEndpoint + case "Anywhere": + relation = matter.DeviceTypeRequirementLocationAnywhere default: err = newGenericParseError(row, "unknown location: %s", rs) }