Manufacturer-specific traits

Manufacturer-specific (MS) traits are supported by the Home APIs, and are referred to as manufacturer-specific traits in the APIs since they support additional functionality beyond standard traits in Android. They must be defined in the standard .matter IDL format, then converted into an Android package 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.10 or newer.
  • A .matter IDL file with the definition of your MS traits. This file should contain just the client 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 Android IDL format, see matter_idl on GitHub. The /tests/inputs directory there features a number of sample IDL files.

Generate a package

Get the packaged code generator:

Download the Packaged Code Generator

  1. Decide on a Java package name to generate the trait code in. For example, com.mycompany.matter.cluster. This name should align with your app's Application ID. To learn more about package naming conventions, see Package names.
  2. 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
  3. Run the generator:
    ./codegen.py \
      --generator custom:.:kotlin \
      --output-dir ./generated/com/mycompany/matter/cluster \
      --option package:com.mycompany.matter.cluster \
      custom-cluster-idl.matter

Use the package

To use your MS trait package, import it into your app:

import com.mycompany.matter.cluster

Then MS traits should 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 CustomTrait, the following call returns all attributes of CustomTrait:

val trait = homeManager.devices().get(id)?.trait(CustomTrait).firstOrNull()

Additional dependencies

For an app using MS traits to compile, you may also need to add the following dependencies to the build.gradle file for your app:

implementation 'com.google.errorprone:error_prone_annotations:2.35.1'

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:.:kotlin \
  --output-dir ./generated/com/mycompany/matter/cluster \
  --option package:com.mycompany.matter.cluster \
  mycustom.matter
2024-09-03 19:00:09 INFO    Parsing idl from mycustom.matter
2024-09-03 19:00:09 INFO    Using CustomGenerator at plugin path ..kotlin
2024-09-03 19:00:09 INFO    Running code generator CUSTOM
2024-09-03 19:00:10 INFO    File to be generated: MyCustomTrait.kt
2024-09-03 19:00:10 INFO    Template path: ClusterSerialization.kt.jinja, CWD: /usr/local/google/home/username/codegen_test
2024-09-03 19:00:11 INFO    Creating output directory: ./generated/com/mycompany/matter/cluster
2024-09-03 19:00:11 INFO    Writing new data to: ./generated/com/mycompany/matter/cluster/MyCustomTrait.kt
2024-09-03 19:00:11 INFO    File to be generated: MyCustom.kt
2024-09-03 19:00:11 INFO    Template path: Cluster.kt.jinja, CWD: /usr/local/google/home/username/codegen_test
2024-09-03 19:00:11 INFO    Writing new data to: ./generated/com/mycompany/matter/cluster/MyCustom.kt
2024-09-03 19:00:11 INFO    Done

Kotlin output

Two Kotlin files, MyCustom.kt and MyCustomTrait.kt, 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 Android Studio project), they can be used as described in Use the package.

MyCustom.kt

// This file contains machine-generated code.
@file:Suppress("PackageName")
package com.mycompany.matter.cluster

import com.google.home.HomeException
import com.google.home.Id
import com.google.home.Event
import com.google.home.EventFactory
import com.google.home.EventImportance
import com.google.home.Field
import com.google.home.Descriptor as HomeDescriptor
import com.google.home.NoOpDescriptor
import com.google.home.Type as FieldType
import com.google.home.Trait
import com.google.home.TraitFactory
import com.google.home.Updatable
import com.google.home.automation.Attribute as AutomationAttribute
import com.google.home.automation.AttributeToUpdate
import com.google.home.automation.Command as AutomationCommand
import com.google.home.automation.EventField
import com.google.home.automation.TypedExpression
import com.google.home.automation.Updater
import com.google.home.automation.fieldSelect
import com.google.home.matter.EventImpl
import com.google.home.matter.MatterEventFactory
import com.google.home.matter.MatterTrait
import com.google.home.matter.MatterTraitImpl
import com.google.home.matter.MatterTraitFactory
import com.google.home.matter.serialization.BitmapAdapter
import com.google.home.matter.serialization.EnumAdapter
import com.mycompany.matter.cluster.MyCustomTrait
import com.mycompany.matter.cluster.MyCustomTrait.Attributes
import com.mycompany.matter.cluster.MyCustomTrait.AttributesImpl
import com.mycompany.matter.cluster.MyCustomTrait.MutableAttributes
import com.google.home.matter.MatterTraitClient
import com.google.home.matter.serialization.OptionalValue
import java.time.Instant

/*
 * This file was machine generated via the code generator
 * in `codegen.clusters.kotlin.CustomGenerator`
 *
 */
/**
 * @suppress
 */


/**
 * @suppress
 * API for the MyCustom trait.
 */
interface MyCustom : 
   Attributes, MatterTrait
   , Updatable<MyCustom, MutableAttributes>
{
   /**
   * Descriptor enum for this trait's attributes.
   */
  enum class Attribute(
    override val tag: UInt,
    override val typeName: String,
    override val typeEnum: FieldType,
    override val descriptor: HomeDescriptor,
    val isNullable: Boolean,
  ) : Field {
    /** The [clusterAttr][MyCustomTrait.Attributes.clusterAttr] trait attribute. */
    clusterAttr(1u, "UShort", FieldType.UShort, NoOpDescriptor, false),
    /** The [generatedCommandList][MyCustomTrait.Attributes.generatedCommandList] trait attribute. */
    generatedCommandList(65528u, "UInt", FieldType.UInt, NoOpDescriptor, false),
    /** The [acceptedCommandList][MyCustomTrait.Attributes.acceptedCommandList] trait attribute. */
    acceptedCommandList(65529u, "UInt", FieldType.UInt, NoOpDescriptor, false),
    /** The [attributeList][MyCustomTrait.Attributes.attributeList] trait attribute. */
    attributeList(65531u, "UInt", FieldType.UInt, NoOpDescriptor, false),
    /** The [featureMap][MyCustomTrait.Attributes.featureMap] trait attribute. */
    featureMap(65532u, "UInt", FieldType.UInt, NoOpDescriptor, false),
    /** The [clusterRevision][MyCustomTrait.Attributes.clusterRevision] trait attribute. */
    clusterRevision(65533u, "UShort", FieldType.UShort, NoOpDescriptor, false);
  }

  fun supports(attribute : Attribute): Boolean

  /**
   * @suppress
   */
  companion object : TraitFactory<MyCustom>(
    MatterTraitFactory(
      clusterId = MyCustomTrait.Id,
      adapter = Attributes.Adapter,
      // Map of enum type name string -> EnumAdapter
      enumAdapters = mapOf<String, EnumAdapter<*>>(
      ),
      bitmapAdapters = mapOf<String, BitmapAdapter<*>>(
      ),
      creator = ::MyCustomImpl,
      supportedEvents = mapOf(
      ),
      // All Trait Commands
      commands = mapOf(
      )
    )
  ) {
    val clusterAttr: AutomationAttribute<UShort?>
      get() = AutomationAttribute<UShort?>(MyCustomTrait.Id.traitId, MyCustom.Attribute.clusterAttr.tag)
    val generatedCommandList: AutomationAttribute<List<UInt>>
      get() = AutomationAttribute<List<UInt>>(MyCustomTrait.Id.traitId, MyCustom.Attribute.generatedCommandList.tag)
    val acceptedCommandList: AutomationAttribute<List<UInt>>
      get() = AutomationAttribute<List<UInt>>(MyCustomTrait.Id.traitId, MyCustom.Attribute.acceptedCommandList.tag)
    val attributeList: AutomationAttribute<List<UInt>>
      get() = AutomationAttribute<List<UInt>>(MyCustomTrait.Id.traitId, MyCustom.Attribute.attributeList.tag)
    val featureMap: AutomationAttribute<UInt>
      get() = AutomationAttribute<UInt>(MyCustomTrait.Id.traitId, MyCustom.Attribute.featureMap.tag)
    val clusterRevision: AutomationAttribute<UShort>
      get() = AutomationAttribute<UShort>(MyCustomTrait.Id.traitId, MyCustom.Attribute.clusterRevision.tag)

    val TypedExpression<out MyCustom?>.clusterAttr: TypedExpression<UShort?>
      get() = fieldSelect<MyCustom, UShort?>(this, MyCustom.Attribute.clusterAttr)
    val TypedExpression<out MyCustom?>.generatedCommandList: TypedExpression<List<UInt>>
      get() = fieldSelect<MyCustom, List<UInt>>(this, MyCustom.Attribute.generatedCommandList)
    val TypedExpression<out MyCustom?>.acceptedCommandList: TypedExpression<List<UInt>>
      get() = fieldSelect<MyCustom, List<UInt>>(this, MyCustom.Attribute.acceptedCommandList)
    val TypedExpression<out MyCustom?>.attributeList: TypedExpression<List<UInt>>
      get() = fieldSelect<MyCustom, List<UInt>>(this, MyCustom.Attribute.attributeList)
    val TypedExpression<out MyCustom?>.featureMap: TypedExpression<UInt>
      get() = fieldSelect<MyCustom, UInt>(this, MyCustom.Attribute.featureMap)
    val TypedExpression<out MyCustom?>.clusterRevision: TypedExpression<UShort>
      get() = fieldSelect<MyCustom, UShort>(this, MyCustom.Attribute.clusterRevision)

    fun Updater<MyCustom>.setClusterAttr(value: UShort) { attributesToUpdate.add(AttributeToUpdate(Attribute.clusterAttr, value)) }

    override fun toString() = "MyCustom"
  }

  override val factory : TraitFactory<MyCustom> get() = Companion
}

/**
 * @suppress
 */
class MyCustomImpl

constructor(
  override val metadata: Trait.TraitMetadata,
  client: MatterTraitClient,
  internal val attributes: Attributes
) :
  MyCustom,
  MatterTraitImpl(metadata, client),
  Attributes by attributes,
  Updatable<MyCustom, MutableAttributes>
{
  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (other !is MyCustomImpl) return false

    if (metadata != other.metadata) return false
    if (attributes != other.attributes) return false

    return true
  }

   /**
   * Checks if the trait supports an attribute. Some devices might not
   * implement all attributes in a Trait definition.
   *
   * @param attribute The attribute to check for.
   * @return True if the attribute is supported by the trait, false if it is not.
   */
  override fun supports(attribute : MyCustom.Attribute) = attributes.attributeList.contains(attribute.tag)

  // Commands
  /**
   * @suppress
   */
  override suspend fun update(
    optimisticReturn: (MyCustom) -> Unit,
    init: MutableAttributes.() -> Unit
  ): MyCustom
   {
    val newVal = MutableAttributes(attributes).apply(init)
    val returnVal = MyCustomImpl(metadata, client, newVal)
    optimisticReturn(returnVal)
    write(MutableAttributes, newVal,
    useTimedInteraction = false
    )
    return returnVal
   }

  override fun toString() = attributes.toString()
}

MyCustomTrait.kt


// This file contains machine-generated code.
@file:Suppress("PackageName")
package com.mycompany.matter.cluster

import com.google.home.Type as FieldType
import com.google.errorprone.annotations.Immutable
import com.google.home.automation.TypedExpression
import com.google.home.automation.fieldSelect
import com.google.home.CommandDescriptor
import com.google.home.HomeException
import com.google.home.Descriptor as HomeDescriptor
import com.google.home.NoOpDescriptor
import com.google.home.StructDescriptor
import com.google.home.matter.serialization.Bitmap
import com.google.home.matter.serialization.BitmapAdapter
import com.google.home.matter.serialization.CanMutate
import com.google.home.matter.serialization.ClusterBitmap
import com.google.home.matter.serialization.ClusterEnum
import com.google.home.matter.serialization.ClusterId
import com.google.home.matter.serialization.ClusterPayloadReader
import com.google.home.matter.serialization.ClusterPayloadWriter
import com.google.home.matter.serialization.EnumAdapter
import com.google.home.matter.serialization.OptionalValue
import com.google.home.matter.serialization.MutableBitmap
import com.google.home.matter.serialization.ScopedCommandId
import com.google.home.matter.serialization.ScopedEventId
import com.google.home.matter.serialization.StructAdapter
import com.google.home.matter.serialization.unwrapPayload
import com.google.home.matter.serialization.wrapPayload
import kotlin.collections.contentDeepEquals
import kotlin.collections.contentEquals
import kotlin.collections.contentHashCode

/*
 * Serialization object for MyCustomTrait.
 *
 * This file was machine generate via the code generator
 * in `codegen.clusters.kotlin.CustomGenerator`
 *
 */

/**
 * @suppress
 * Attributes for MyCustomTrait.
 */
object MyCustomTrait {
  val Id = ClusterId(4294048768u, "MyCustom")

  // Enums

  // Bitmaps

  // Events

  // Structs

  /** Attributes for the MyCustom cluster. */
  interface Attributes {
    val clusterAttr: UShort?

    /** A list of server-generated commands (server to client) which are supported by this cluster server instance. */
    val generatedCommandList: List<UInt>

    /** A list of client-generated commands which are supported by this cluster server instance. */
    val acceptedCommandList: List<UInt>

    /** A list of the attribute IDs of the attributes supported by the cluster instance. */
    val attributeList: List<UInt>

    /**  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). */
    val featureMap: UInt

    /** The revision of the server cluster specification supported by the cluster instance. */
    val clusterRevision: UShort

    /** @suppress */
    companion object Adapter : StructAdapter<Attributes> {

      override fun write(writer: ClusterPayloadWriter, value: Attributes) {
        if (value is MutableAttributes) {
            MutableAttributes.Adapter.write(writer, value)
            return
        }
        writer.wrapPayload(id = Id)
        if (!writer.strictOperationValidation || value.attributeList.contains(1u)) {
          writer.ushort.write(1u, value.clusterAttr)
        }
        writer.uint.writeList(65528u, value.generatedCommandList)
        writer.uint.writeList(65529u, value.acceptedCommandList)
        writer.uint.writeList(65531u, value.attributeList)
        writer.uint.write(65532u, value.featureMap)
        writer.ushort.write(65533u, value.clusterRevision)
      }
      override fun read(reader: ClusterPayloadReader): Attributes {
        reader.unwrapPayload(id = Id)
        val data = reader.readPayload()
        val attributeList = mutableListOf<UInt>()
        return AttributesImpl(
          data.ushort.getOptionalNullable(1u, "ClusterAttr").also{ if (it.isPresent && it.value != null) attributeList.add(1u) }.getOrNull(),
          data.uint.getList(65528u, "GeneratedCommandList").also{ attributeList.add(65528u)},
          data.uint.getList(65529u, "AcceptedCommandList").also{ attributeList.add(65529u)},
          attributeList.also { attributeList.add(65531u) },
          data.uint.get(65532u, "FeatureMap").also{ attributeList.add(65532u)},
          data.ushort.get(65533u, "ClusterRevision").also{ attributeList.add(65533u)},
        )
      }
    }
  }

  /** @suppress */
  open class AttributesImpl(
    override val clusterAttr: UShort? =null,
    override val generatedCommandList: List<UInt> =emptyList(),
    override val acceptedCommandList: List<UInt> =emptyList(),
    override val attributeList: List<UInt> =listOf(1u,65528u,65529u,65531u,65532u,65533u,),
    override val featureMap: UInt =0u,
    override val clusterRevision: UShort =0u,
  ) : Attributes, CanMutate<Attributes, MutableAttributes>{

    constructor(other: Attributes): this(
      clusterAttr  = other.clusterAttr,
      generatedCommandList  = other.generatedCommandList,
      acceptedCommandList  = other.acceptedCommandList,
      attributeList  = other.attributeList,
      featureMap  = other.featureMap,
      clusterRevision  = other.clusterRevision)

    override fun mutate(init: MutableAttributes.() -> Unit): Attributes =
      AttributesImpl(MutableAttributes(this).apply(init))

    companion object {
      val Adapter = Attributes.Adapter
    }

    override fun equals(other: Any?): Boolean {
      if (this === other) return true
      if (other !is Attributes) return false
      if (clusterAttr != other.clusterAttr) { return false; }
      if (generatedCommandList != other.generatedCommandList) { return false; }
      if (acceptedCommandList != other.acceptedCommandList) { return false; }
      if (attributeList != other.attributeList) { return false; }
      if (featureMap != other.featureMap) { return false; }
      if (clusterRevision != other.clusterRevision) { return false; }

      return true
    }

    override fun hashCode(): Int {
      var result = 1
      result = 31 * result + (clusterAttr?.hashCode() ?: 0)
      result = 31 * result + generatedCommandList.hashCode()
      result = 31 * result + acceptedCommandList.hashCode()
      result = 31 * result + attributeList.hashCode()
      result = 31 * result + featureMap.hashCode()
      result = 31 * result + clusterRevision.hashCode()

      return result
    }

    override fun toString(): String {
      return "MyCustom(clusterAttr=$clusterAttr, generatedCommandList=$generatedCommandList, acceptedCommandList=$acceptedCommandList, attributeList=$attributeList, featureMap=$featureMap, clusterRevision=$clusterRevision)"
    }

    fun copy(
      clusterAttr: UShort? = this.clusterAttr,
      generatedCommandList: List<UInt> = this.generatedCommandList,
      acceptedCommandList: List<UInt> = this.acceptedCommandList,
      attributeList: List<UInt> = this.attributeList,
      featureMap: UInt = this.featureMap,
      clusterRevision: UShort = this.clusterRevision,
    ) = AttributesImpl(
      clusterAttr = clusterAttr,
      generatedCommandList = generatedCommandList,
      acceptedCommandList = acceptedCommandList,
      attributeList = attributeList,
      featureMap = featureMap,
      clusterRevision = clusterRevision,
    )
  }

  /** @suppress */
  class MutableAttributes(attributes: Attributes) : 
    AttributesImpl(
      clusterAttr = attributes.clusterAttr,
      generatedCommandList = attributes.generatedCommandList,
      acceptedCommandList = attributes.acceptedCommandList,
      attributeList = attributes.attributeList,
      featureMap = attributes.featureMap,
      clusterRevision = attributes.clusterRevision,
    ) {
    internal var _clusterAttr : UShort? = null
    override val clusterAttr : UShort?
      get() {
        return _clusterAttr ?: super.clusterAttr
      }
    fun setClusterAttr(value : UShort) {
        _clusterAttr = value
    }

    override fun equals(other: Any?): Boolean {
      if (this === other) return true
      if (other !is MutableAttributes) return false
      return super.equals(other)
    }

    override fun toString(): String {
      return "MyCustom.MutableAttributes(${super.toString()})"
    }

    companion object Adapter : StructAdapter<MutableAttributes> {
      override fun write(writer: ClusterPayloadWriter, value: MutableAttributes) {
        writer.wrapPayload(id = Id)
        if (value._clusterAttr != null) {
          if (!writer.strictOperationValidation || value.attributeList.contains(1u)) {
            writer.ushort.write(1u, value._clusterAttr)
          } else {
            throw HomeException.invalidArgument("clusterAttr")
          }
        }
      }

      override fun read(reader: ClusterPayloadReader): MutableAttributes =
        MutableAttributes(Attributes.Adapter.read(reader))
    }
  }

  // Commands

}