Android 上的制造商特定特征

Home API 支持 Android 的制造商专用 (MS) 特征,由于这些特征支持 Android 中标准特征以外的其他功能,因此在 API 中被称为制造商专用特征。它们必须以标准的 .matter IDL 格式定义,然后转换为可导入到应用中的 Android 软件包。

使用 Google 提供的打包代码生成器执行此转换。 此外,如果需要,还可以使用代码生成器生成临时特征。

前提条件

如需使用代码生成器,您需要:

  • 搭载 Python 3.10 或更高版本的 Linux 机器。
  • 包含 MS 特征定义的 .matter IDL 文件。此文件应仅包含 client cluster 定义。您可以手动创建一个,也可以使用在 Matter SDK build 流程中为设备固件生成的密钥。

如需详细了解 IDL 格式,请参阅 GitHub 上的 matter/idl。该目录中的 /tests/inputs 包含多个 IDL 示例文件。

生成软件包

获取打包的代码生成器:

下载打包的代码生成器

  1. 确定用于生成特征代码的 Java 软件包名称。例如 com.mycompany.matter.cluster。此名称应与应用的 Application ID 保持一致。如需详细了解软件包命名惯例,请参阅软件包名称
  2. 提取并设置生成器:
    mkdir -p ~/tmp/codegen_test
    cd ~/tmp/codegen_test
    tar xfvz ~/tmp/matter_codegen.tar.gz
    pip install -r requirements.txt
  3. 运行生成器:
    ./codegen.py \
      --generator custom:.:kotlin \
      --output-dir ./generated/com/mycompany/matter/cluster \
      --option package:com.mycompany.matter.cluster \
      custom-cluster-idl.matter

使用软件包

如需使用 MS 特征软件包,请将其导入到您的应用中:

import com.mycompany.matter.cluster

然后,只要在 Matter 固件中定义了这些 MS 特征,就应该可以通过 Home API 以与标准 Matter 特征相同的方式使用 MS 特征。只需将标准特征名称替换为您的 MS 特征名称即可。

例如,如果您的 MS 特征名为 CustomTrait,则以下调用会返回 CustomTrait 的所有属性:

val device = devices().get(id)?

val deviceType = devices().get(id)?.type?.value

val trait = device?.type(deviceType)?.map{it.trait(CustomTrait)}.firstOrNull()

其他依赖项

对于使用 MS 特征进行编译的应用,您可能还需要将以下依赖项添加到应用的 build.gradle 文件中:

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

示例

如果您不熟悉 IDL 格式,请参阅 matter/idl/tests/inputs 目录中的示例文件。

IDL 输入

可以在 IDL 中定义一个非常简单的 MS 特征,如下所示:

// 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;
}

在此示例中,4294048768 的特征 ID 对应于十六进制的 0xFFF1FC00,其中 0xFFF1 的前缀表示测试供应商 ID,0xFC00 的后缀是为制造商专用特征预留的值。如需了解详情,请参阅 Matter 规范的制造商可扩展标识符 (MEI) 部分。请确保为 IDL 文件中的每个 MS 特征使用适当的十进制特征 ID。

如果您的设备目前正在使用 MS 特征,您可能已经以这种格式定义了该特征。

运行代码生成器

mycustom.matter 文件与代码生成器位于同一目录的情况下,运行该文件:

./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 输出

现在,您可以在指定的输出目录中找到两个 Kotlin 文件:MyCustom.ktMyCustomTrait.kt。这些文件的格式专门用于搭配 Home API。

一旦可用(例如在应用的 Android Studio 项目中),便可按照使用软件包中所述的方式使用。

MyCustom.kt

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

import com.google.home.BatchableCommand
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.StructDescriptor
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.toDescriptorMap
import com.google.home.DescriptorMap
import com.google.errorprone.annotations.Immutable
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
import javax.annotation.processing.Generated

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


/**
 * API for the MyCustom trait.
 */
@Generated("GoogleHomePlatformCodegen")
interface MyCustom : 
   Attributes, MatterTrait
   , Updatable<MyCustom, MutableAttributes>
{
   /**
   * Descriptor enum for this trait's attributes.
   */
  enum class Attribute(
    override val fieldName: String,
    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("clusterAttr", 1u, "UShort", FieldType.UShort, NoOpDescriptor, false),
    /** The [generatedCommandList][MyCustomTrait.Attributes.generatedCommandList] trait attribute. */
    generatedCommandList("generatedCommandList", 65528u, "UInt", FieldType.UInt, NoOpDescriptor, false),
    /** The [acceptedCommandList][MyCustomTrait.Attributes.acceptedCommandList] trait attribute. */
    acceptedCommandList("acceptedCommandList", 65529u, "UInt", FieldType.UInt, NoOpDescriptor, false),
    /** The [attributeList][MyCustomTrait.Attributes.attributeList] trait attribute. */
    attributeList("attributeList", 65531u, "UInt", FieldType.UInt, NoOpDescriptor, false),
    /** The [featureMap][MyCustomTrait.Attributes.featureMap] trait attribute. */
    featureMap("featureMap", 65532u, "UInt", FieldType.UInt, NoOpDescriptor, false),
    /** The [clusterRevision][MyCustomTrait.Attributes.clusterRevision] trait attribute. */
    clusterRevision("clusterRevision", 65533u, "UShort", FieldType.UShort, NoOpDescriptor, false);
    

    companion object {
      val StructDescriptor = object : StructDescriptor {
        @Suppress("Immutable")
        override val fields: DescriptorMap = entries.toDescriptorMap()
      }
    }
  }

  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.toDescriptorMap
import com.google.home.DescriptorMap
import com.google.home.Descriptor as HomeDescriptor
import com.google.home.ClusterStruct
import com.google.home.TagId
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
import javax.annotation.processing.Generated

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

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

  // Enums

  // Bitmaps

  // Events

  // Structs

  /** Attributes for the MyCustom cluster. */
  @Generated("GoogleHomePlatformCodegen")
  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

}