Manufacturer-specific (MS) traits are supported by the Home APIs for
iOS, and are referred to as manufacturer-specific
traits in the APIs since they support additional functionality beyond standard
traits in iOS. They must be defined in the standard
.matter
IDL format, then converted into an iOS
module that can be imported into your app.
Use the Google-provided packaged code generator to perform this conversion. Additionally, if needed, provisional traits can also be generated using the code generator.
Prerequisites
In order to use the code generator, you need:
- A Linux-based machine with Python 3.11 or newer.
- A
.matter
IDL file with the definition of your MS traits. This file should contain just theclient cluster
definitions. You can create one manually, or use ones generated as part of the Matter SDK build process for your device firmware.
For more information about the IDL format, see matter/idl on GitHub. The /tests/inputs directory there features a number of sample IDL files.
Generate the Swift files
Get the packaged code generator:
Download the Packaged Code Generator
Decide on a Swift namespace name to generate the trait code in. For example,
MyCompany
.Extract and set up the generator:
mkdir -p ~/tmp/codegen_test
cd ~/tmp/codegen_test
tar xfvz ~/tmp/matter_codegen.tar.gz
pip install -r requirements.txt
Run the generator:
./codegen.py \ --generator custom:.:swift \ --output-dir ./generated/MyCompany \ --option package:MyCompany \ --option generate_namespace:True \ custom-cluster-idl.matter
Setting
generate_namespace:True
is optional. If the traits are in a single.matter
file, it is recommended to set this parameter toTrue
.However, if you plan on generating multiple traits for the same namespace from separate
.matter
source files, you should set this parameter toFalse
. If you don't set it toFalse
in this scenario, multiple namespaceenums
get generated and you will need to discard all but one of theseenums
. When this parameter is set toFalse
, you must generate the namespace yourself in a file located in the same module as the generated trait files. An example of the code needed for a namespace ispublic enum MyCompany {}
.
The generated .swift
files can be added to your app project
or be added to a framework if you want them in a separate module.
Use the module
To use the generated output, copy the file(s) to your Xcode project. In this
case, the files are MyCompany.swift
and MyCustom.swift
.
If you are using a separate framework for your traits, then use an import
statement to import the applicable module.
Then MS traits should now be available through the Home APIs the same way standard Matter traits are, as long as those MS traits are defined in your Matter firmware. Simply substitute a standard trait name with your MS trait name.
For example, if your MS trait is named MyCustomTrait
, the following call
returns all attributes of MyCustomTrait
:
let myCustomTrait = deviceType.traits[MyCompany.MyCustomTrait.self]
Example
If you are not familiar with the IDL format, see the matter/idl/tests/inputs directories for sample files.
IDL input
A very simple MS trait can be defined in the IDL like this:
// mycustom.matter
client cluster MyCustom = 4294048768 {
attribute int16u clusterAttr = 1;
// Global Attributes
readonly attribute command_id generatedCommandList[] = 65528;
readonly attribute command_id acceptedCommandList[] = 65529;
readonly attribute event_id eventList[] = 65530;
readonly attribute attrib_id attributeList[] = 65531;
readonly attribute bitmap32 featureMap = 65532;
readonly attribute int16u clusterRevision = 65533;
}
In this example, the trait ID of 4294048768
corresponds to 0xFFF1FC00
in
hexadecimal, where the prefix of 0xFFF1
represents a test Vendor ID and the
suffix of 0xFC00
is a value reserved for Manufacturer-Specific traits. See
the Manufacturer Extensible Identifier (MEI) section of the
Matter Specification for more information. Make sure to use
an appropriate decimal trait ID for each MS trait in your IDL file.
If MS traits are being used in your device today, you likely have it defined in this format already.
Run the code generator
With the mycustom.matter
file in the same directory as the code generator, run
it:
./codegen.py \
--generator custom:.:swift \
--output-dir ./generated/MyCompany \
--option package:MyCompany \
--option generate_namespace:True \
mycustom.matter
2025-02-27 01:54:49 INFO Parsing idl from mycustom.matter 2025-02-27 01:54:49 INFO Using CustomGenerator at plugin path ..swift 2025-02-27 01:54:49 INFO Running code generator CUSTOM 2025-02-27 01:54:50 INFO File to be generated: MyCompany.swift 2025-02-27 01:54:50 INFO Template path: Namespace.swift.jinja, CWD: /usr/local/google/home/username/Downloads/matter_codegen 2025-02-27 01:54:50 INFO Creating output directory: ./generated/MyCompany 2025-02-27 01:54:50 INFO Writing new data to: ./generated/MyCompany/MyCompany.swift 2025-02-27 01:54:50 INFO File to be generated: MyCustom.swift 2025-02-27 01:54:50 INFO Template path: Cluster.swift.jinja, CWD: /usr/local/google/home/username/Downloads/matter_codegen 2025-02-27 01:54:50 INFO Writing new data to: ./generated/MyCompany/MyCustom.swift 2025-02-27 01:54:50 INFO Done
Swift output
Two Swift files, MyCustom.swift
(named after the trait) and
MyCompany.swift
(named after the namespace), can now be found in
the specified output directory. These files are formatted specifically for use
with the Home APIs.
Once available (such as in your app's Xcode project), the files can be used as described in Use the module.
MyCustom.swift
Click to expand to view `MyCustom.swift`
// This file contains machine-generated code. public import Foundation @_spi(GoogleHomeInternal) import GoogleHomeSDK /* * This file was machine generated via the code generator * in `codegen.clusters.swift.CustomGenerator` * */ extension MyCompany { /// :nodoc: public struct MyCustomTrait: MatterTrait { /// No supported events for `MyCustomTrait`. public static let supportedEventTypes: [Event.Type] = [] /// No supported commands for `MyCustomTrait`. public static let supportedCommandTypes: [Command.Type] = [] public static let identifier = MyCompany.MyCustomTrait.makeTraitID(for: 4294048768) public let metadata: TraitMetadata /// List of attributes for the `MyCustomTrait`. public let attributes: MyCompany.MyCustomTrait.Attributes private let interactionProxy: InteractionProxy public init(decoder: TraitDecoder, interactionProxy: InteractionProxy?, metadata: TraitMetadata) throws { guard let interactionProxy = interactionProxy else { throw HomeError.invalidArgument("InteractionProxy parameter required.") } let unwrappedDecoder = try decoder.unwrapPayload(namespace: Self.identifier.namespace) self.interactionProxy = interactionProxy self.attributes = try Attributes(decoder: unwrappedDecoder) self.metadata = metadata } // Internal for testing. internal init(attributes: MyCompany.MyCustomTrait.Attributes = .init(), interactionProxy: InteractionProxy?, metadata: TraitMetadata = .init()) throws { guard let interactionProxy = interactionProxy else { throw HomeError.invalidArgument("InteractionProxy parameter required.") } self.interactionProxy = interactionProxy self.attributes = attributes self.metadata = metadata } public func encode(with encoder: TraitEncoder) throws { encoder.wrapPayload(namespace: Self.identifier.namespace) try self.attributes.encode(with: encoder) } public func update(_ block: @Sendable (MutableAttributes) -> Void) async throws -> Self { let mutable = MutableAttributes(attributes: self.attributes) block(mutable) if self.interactionProxy.strictOperationValidation { guard self.attributes.$clusterAttr.isSupported || !mutable.clusterAttrIsSet else { throw HomeError.invalidArgument("clusterAttr is not supported.") } } let updatedTrait = try MyCompany.MyCustomTrait(attributes: self.attributes.apply(mutable), interactionProxy: self.interactionProxy, metadata: self.metadata) try await self.interactionProxy.update(trait: mutable, useTimedInteraction: false) return updatedTrait } } } // MARK: - ForceReadableTrait extension MyCompany.MyCustomTrait: ForceReadableTrait { public func forceRead() async throws { try await self.interactionProxy.forceRead(traitID: Self.identifier) } } // MARK: - Attributes extension MyCompany.MyCustomTrait { /// Attributes for the `MyCustomTrait`. public struct Attributes: Sendable { // Attributes required at runtime. /** A list of the attribute IDs of the attributes supported by the cluster instance. */ /// Nullable: false. @TraitAttribute public var attributeList: [UInt32]? /// Nullable: false. @TraitAttribute public var clusterAttr: UInt16? /** A list of server-generated commands (server to client) which are supported by this cluster server instance. */ /// Nullable: false. @TraitAttribute public var generatedCommandList: [UInt32]? /** A list of client-generated commands which are supported by this cluster server instance. */ /// Nullable: false. @TraitAttribute public var acceptedCommandList: [UInt32]? /** Whether the server supports zero or more optional cluster features. A cluster feature is a set of cluster elements that are mandatory or optional for a defined feature of the cluster. If a cluster feature is supported by the cluster instance, then the corresponding bit is set to 1, otherwise the bit is set to 0 (zero). */ /// Nullable: false. @TraitAttribute public var featureMap: UInt32? /** The revision of the server cluster specification supported by the cluster instance. */ /// Nullable: false. @TraitAttribute public var clusterRevision: UInt16? internal init( clusterAttr: UInt16? = nil, generatedCommandList: [UInt32]? = nil, acceptedCommandList: [UInt32]? = nil, attributeList: [UInt32]? = nil, featureMap: UInt32? = nil, clusterRevision: UInt16? = nil ) { self._clusterAttr = .init( wrappedValue: clusterAttr, isSupported: attributeList?.contains(0x01) ?? false, isNullable: false ) self._generatedCommandList = .init( wrappedValue: generatedCommandList, isSupported: attributeList?.contains(0x0FFF8) ?? false, isNullable: false ) self._acceptedCommandList = .init( wrappedValue: acceptedCommandList, isSupported: attributeList?.contains(0x0FFF9) ?? false, isNullable: false ) self._attributeList = .init( wrappedValue: attributeList, isSupported: attributeList?.contains(0x0FFFB) ?? false, isNullable: false ) self._featureMap = .init( wrappedValue: featureMap, isSupported: attributeList?.contains(0x0FFFC) ?? false, isNullable: false ) self._clusterRevision = .init( wrappedValue: clusterRevision, isSupported: attributeList?.contains(0x0FFFD) ?? false, isNullable: false ) } fileprivate init(decoder: TraitDecoder) throws { let decodedAttributeList: [UInt32] = try decoder.decodeOptionalArray(tag: 0x0FFFB) ?? [] var generatedAttributeList = [UInt32]() generatedAttributeList.append(0x0FFFB) let clusterAttrValue: UInt16? = try decoder.decodeOptional(tag: 0x01) let clusterAttrIsSupported = clusterAttrValue != nil if clusterAttrIsSupported { generatedAttributeList.append(0x01) } self._clusterAttr = .init( wrappedValue: clusterAttrIsSupported ? clusterAttrValue : nil, isSupported: clusterAttrIsSupported, isNullable: false ) let generatedCommandListValue: [UInt32]? = try decoder.decodeOptionalArray(tag: 0x0FFF8) let generatedCommandListIsSupported = generatedCommandListValue != nil if generatedCommandListIsSupported { generatedAttributeList.append(0x0FFF8) } self._generatedCommandList = .init( wrappedValue: generatedCommandListIsSupported ? generatedCommandListValue : nil, isSupported: generatedCommandListIsSupported, isNullable: false ) let acceptedCommandListValue: [UInt32]? = try decoder.decodeOptionalArray(tag: 0x0FFF9) let acceptedCommandListIsSupported = acceptedCommandListValue != nil if acceptedCommandListIsSupported { generatedAttributeList.append(0x0FFF9) } self._acceptedCommandList = .init( wrappedValue: acceptedCommandListIsSupported ? acceptedCommandListValue : nil, isSupported: acceptedCommandListIsSupported, isNullable: false ) let featureMapValue: UInt32? = try decoder.decodeOptional(tag: 0x0FFFC) let featureMapIsSupported = featureMapValue != nil if featureMapIsSupported { generatedAttributeList.append(0x0FFFC) } self._featureMap = .init( wrappedValue: featureMapIsSupported ? featureMapValue : nil, isSupported: featureMapIsSupported, isNullable: false ) let clusterRevisionValue: UInt16? = try decoder.decodeOptional(tag: 0x0FFFD) let clusterRevisionIsSupported = clusterRevisionValue != nil if clusterRevisionIsSupported { generatedAttributeList.append(0x0FFFD) } self._clusterRevision = .init( wrappedValue: clusterRevisionIsSupported ? clusterRevisionValue : nil, isSupported: clusterRevisionIsSupported, isNullable: false ) self._attributeList = .init( wrappedValue: generatedAttributeList, isSupported: true, isNullable: false ) } fileprivate func apply(_ update: MyCompany.MyCustomTrait.MutableAttributes) -> Self { let clusterAttrValue = update.clusterAttrIsSet ? update.clusterAttr : self.clusterAttr let generatedCommandListValue = self.generatedCommandList let acceptedCommandListValue = self.acceptedCommandList let attributeListValue = self.attributeList let featureMapValue = self.featureMap let clusterRevisionValue = self.clusterRevision return MyCompany.MyCustomTrait.Attributes( clusterAttr: clusterAttrValue, generatedCommandList: generatedCommandListValue, acceptedCommandList: acceptedCommandListValue, attributeList: attributeListValue, featureMap: featureMapValue, clusterRevision: clusterRevisionValue ) } } } extension MyCompany.MyCustomTrait.Attributes: TraitEncodable { public static var identifier: String { MyCompany.MyCustomTrait.identifier } public func encode(with encoder: TraitEncoder) throws { try encoder.encode(tag: 0x01, value: self.clusterAttr) try encoder.encode(tag: 0x0FFF8, value: self.generatedCommandList) try encoder.encode(tag: 0x0FFF9, value: self.acceptedCommandList) try encoder.encode(tag: 0x0FFFB, value: self.attributeList) try encoder.encode(tag: 0x0FFFC, value: self.featureMap) try encoder.encode(tag: 0x0FFFD, value: self.clusterRevision) } } // MARK: - Hashable & Equatable extension MyCompany.MyCustomTrait: Hashable { public static func ==(lhs: MyCompany.MyCustomTrait, rhs: MyCompany.MyCustomTrait) -> Bool { return lhs.identifier == rhs.identifier && lhs.attributes == rhs.attributes && lhs.metadata == rhs.metadata } public func hash(into hasher: inout Hasher) { hasher.combine(identifier) hasher.combine(attributes) hasher.combine(metadata) } } extension MyCompany.MyCustomTrait.Attributes: Hashable { public static func ==(lhs: MyCompany.MyCustomTrait.Attributes, rhs: MyCompany.MyCustomTrait.Attributes) -> Bool { var result = true result = lhs.clusterAttr == rhs.clusterAttr && result result = lhs.generatedCommandList == rhs.generatedCommandList && result result = lhs.acceptedCommandList == rhs.acceptedCommandList && result result = lhs.attributeList == rhs.attributeList && result result = lhs.featureMap == rhs.featureMap && result result = lhs.clusterRevision == rhs.clusterRevision && result return result } public func hash(into hasher: inout Hasher) { hasher.combine(self.clusterAttr) hasher.combine(self.generatedCommandList) hasher.combine(self.acceptedCommandList) hasher.combine(self.attributeList) hasher.combine(self.featureMap) hasher.combine(self.clusterRevision) } } // MARK: - MutableAttributes extension MyCompany.MyCustomTrait { public final class MutableAttributes: TraitEncodable { public static let identifier: String = MyCompany.MyCustomTrait.identifier private let baseAttributes: Attributes fileprivate var clusterAttr: UInt16? private(set) public var clusterAttrIsSet = false public func setClusterAttr(_ value: UInt16) { self.clusterAttr = value self.clusterAttrIsSet = true } public func clearClusterAttr() { self.clusterAttr = nil self.clusterAttrIsSet = false } internal init(attributes: MyCompany.MyCustomTrait.Attributes) { self.baseAttributes = attributes } public func encode(with encoder: TraitEncoder) throws { // MutableAttributes is encoded individually, e.g. through update(...), // therefore uddm wrapping needs to be applied. encoder.wrapPayload(namespace: Self.identifier.namespace) if self.clusterAttrIsSet { try encoder.encode(tag: 0x01, value: self.clusterAttr) } } } } // MARK: - Attributes definitions extension MyCompany.MyCustomTrait { public enum Attribute: UInt32, Field { case clusterAttr = 1 case generatedCommandList = 65528 case acceptedCommandList = 65529 case attributeList = 65531 case featureMap = 65532 case clusterRevision = 65533 public var id: UInt32 { self.rawValue } public var type: FieldType { switch self { case .clusterAttr: return .uint16 case .generatedCommandList: return .uint32 case .acceptedCommandList: return .uint32 case .attributeList: return .uint32 case .featureMap: return .uint32 case .clusterRevision: return .uint16 } } } public static func attribute(id: UInt32) -> (any Field)? { return Attribute(rawValue: id) } } // MARK: - Attribute fieldSelect definitions extension TypedReference where T == MyCompany.MyCustomTrait { public var clusterAttr: TypedExpression<UInt16> { fieldSelect(from: self, selectedField: T.Attribute.clusterAttr) } public var generatedCommandList: TypedExpression<[UInt32]> { fieldSelect(from: self, selectedField: T.Attribute.generatedCommandList) } public var acceptedCommandList: TypedExpression<[UInt32]> { fieldSelect(from: self, selectedField: T.Attribute.acceptedCommandList) } public var attributeList: TypedExpression<[UInt32]> { fieldSelect(from: self, selectedField: T.Attribute.attributeList) } public var featureMap: TypedExpression<UInt32> { fieldSelect(from: self, selectedField: T.Attribute.featureMap) } public var clusterRevision: TypedExpression<UInt16> { fieldSelect(from: self, selectedField: T.Attribute.clusterRevision) } } extension Updater where T == MyCompany.MyCustomTrait { public func setClusterAttr(_ value: UInt16) { self.set(Parameter(field: T.Attribute.clusterAttr, value: value)) } } // MARK: - Struct Fields definitions
MyCompany.swift
/// Namespace for all MyCompany Traits and DeviceTypes.
public enum MyCompany { }