Android で自動化を構築する

Automation API には Android 用 Home API を通じてアクセスできますが、エントリ ポイントはストラクチャであるため、使用する前にストラクチャに対する権限を付与する必要があります。

構造に対する権限が付与されたら、これらのパッケージをアプリにインポートします。


import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.Id
import com.google.home.Structure

ストラクチャには、次の自動化固有のメソッドを含む HasAutomations インターフェースが含まれます。

API 説明
automations() ストラクチャに属するすべての自動化を一覧表示します。Home API を使用して作成した自動化のみが返されます。
createAutomation(automation) 構造の自動化インスタンスを作成します。
deleteAutomation(automationId) ID で自動化インスタンスを削除します。

自動化の作成

Home のインスタンスを作成し、ユーザーから権限を受け取ったら、ストラクチャとデバイスを取得します。

val structure = homeManager.structures().list().single()
val device = homeManager.devices().get(Id("myDevice"))!!

次に、Automation DSL を使用して自動化のロジックを定義します。Home API では、自動化は Automation インターフェースで表されます。このインターフェースには、一連のプロパティが含まれています。

  • 名前や説明などのメタデータ。
  • 自動化を実行できるかどうかなどを示すフラグ。
  • 自動化のロジックを含むノードのリスト(自動化グラフ)。automationGraph プロパティで表されます。

automationGraph は、デフォルトで SequentialFlow 型です。これは、順次実行されるノードのリストを含むクラスです。各ノードは、スターター、条件、アクションなどの自動化の要素を表します。

自動化に namedescription を割り当てます。

自動化を作成すると、isActive フラグはデフォルトで true に設定されます。そのため、自動化を最初に無効にする場合を除き、このフラグを明示的に設定する必要はありません。このシナリオでは、作成時にフラグを false に設定します。

DraftAutomation インターフェースは自動化の構築と作成に使用され、Automation インターフェースは取得に使用されます。たとえば、別のデバイスがオンになったときにデバイスをオンにする自動化の Automation DSL は次のようになります。

import com.google.home.automation.Action
import com.google.home.automation.Automation
import com.google.home.automation.Condition
import com.google.home.automation.DraftAutomation
import com.google.home.automation.Equals
import com.google.home.automation.Node
import com.google.home.automation.SequentialFlow
import com.google.home.automation.Starter
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.matter.standard.OnOff
import com.google.home.Structure

...

val automation: DraftAutomation = automation {
  name = "MyFirstAutomation"
  description = "Turn on a device when another device is turned on."
  sequential {
    val starterNode = starter<_>(device1, OnOffLightDevice, trait=OnOff)
    condition() { expression = stateReaderNode.onOff equals true }
    action(device2, OnOffLightDevice) { command(OnOff.on()) }
  }
}

自動化 DSL を定義したら、createAutomation() メソッドに渡して DraftAutomation インスタンスを作成します。

val createdAutomation = structure.createAutomation(automation)

ここから、execute()stop()update() など、他のすべての自動化メソッドを自動化で使用できます。

検証エラー

自動化の作成が検証に合格しない場合、警告またはエラー メッセージに問題に関する情報が表示されます。詳細については、ValidationIssueType リファレンスをご覧ください。

コードの例

ここでは、Android で自動化を設計するページで説明した仮想の自動化の一部を実装するために使用できるコード例を紹介します。

シンプルな自動化

午前 8 時にブラインドを上げる自動化は、次のように実装できます。

// get all the automation node candidates in the structure
val allCandidates = structure.allCandidates().first()
// determine whether a scheduled automation can be constructed
val isSchedulingSupported =
  allCandidates.any {
    it is EventCandidate &&
      it.eventFactory == Time.ScheduledTimeEvent &&
      it.unsupportedReasons.isEmpty()
  }
// get the blinds present in the structure
val blinds =
  allCandidates
    .filter {
      it is CommandCandidate &&
        it.commandDescriptor == WindowCoveringTrait.UpOrOpenCommand &&
        it.unsupportedReasons.isEmpty()
    }
    .map { it.entity }
    .filterIsInstance<HomeDevice>()
    .filter { it.has(WindowCoveringDevice) }
 if (isSchedulingSupported && blinds.isNotEmpty()) {
  // Proceed to create automation
  val automation: DraftAutomation = automation {
    name = "Day time open blinds"
    description = "Open all blinds at 8AM everyday"
    isActive = true
    sequential {
      // At 8:00am local time....
      val unused =
        starter(structure, Time.ScheduledTimeEvent) {
          parameter(Time.ScheduledTimeEvent.clockTime(LocalTime.of(8, 0, 0, 0)))
        }
        // ...open all the blinds
       parallel {
        for (blind in blinds) {
          action(blind, WindowCoveringDevice) { command(WindowCovering.upOrOpen()) }
        }
      }
    }
  }
   val createdAutomation = structure.createAutomation(automation)
} else if (!isSchedulingSupported) {
  // Cannot create automation.
  // Set up your address on the structure, then try again.
} else {
  // You don't have any WindowCoveringDevices.
  // Try again after adding some blinds to your structure.
}

複雑な自動化

動きが検出されたときにライトを点滅させる自動化は、次のように実装できます。

import com.google.home.Home
import com.google.home.HomeClient
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure
import com.google.home.automation.action
import com.google.home.automation.automation
import com.google.home.automation.equals
import com.google.home.automation.parallel
import com.google.home.automation.starter
import com.google.home.google.AssistantBroadcast
import com.google.home.matter.standard.OnOff
import com.google.home.matter.standard.OnOff.Companion.toggle
import com.google.home.matter.standard.OnOffLightDevice
import java.time.Duration

// get all the automation node candidates in the structure
val allCandidates = structure.allCandidates().first()

// get the lights present in the structure
val availableLights = allCandidates.filter {
   it is CommandCandidate &&
   it.commandDescriptor == OnOffTrait.OnCommand
}.map { it.entity }
.filterIsInstance<HomeDevice>()
.filter {it.has(OnOffLightDevice) ||
         it.has(ColorTemperatureLightDevice) ||
         it.has(DimmableLightDevice) ||
         it.has(ExtendedColorLightDevice)}

val selectedLights = ... // user selects one or more lights from availableLights

automation {
isActive = true

sequential {
   // If the presence state changes...
   val starterNode = starter<_>(structure, AreaPresenceState)
   // ...and if the area is occupied...
   condition() {
      expression = starterNode.presenceState equals PresenceState.PresenceStateOccupied
   }
   // "blink" the light(s)
   parallel {
            for(light in selectedLights) {
            action(light, OnOffLightDevice) { command(OnOff.toggle()) }
            delayFor(Duration.ofSeconds(1))
            action(light, OnOffLightDevice) { command(OnOff.toggle()) }
            delayFor(Duration.ofSeconds(1))
            action(light, OnOffLightDevice) { command(OnOff.toggle()) }
            delayFor(Duration.ofSeconds(1))
            action(light, OnOffLightDevice) { command(OnOff.toggle())}
         }
      }
   }
}

エンティティ フィルタを使用してデバイスを動的に選択する

自動化を記述する際に、特定のデバイスを指定する必要はありません。エンティティ フィルタという機能を使用すると、さまざまな条件に基づいて実行時にデバイスを選択できます。

たとえば、エンティティ フィルタを使用すると、自動化で次のものをターゲットにできます。

  • 特定のデバイスタイプのすべてのデバイス
  • 特定の部屋にあるすべてのデバイス
  • 特定の部屋にある特定のデバイスタイプのすべてのデバイス
  • オンになっているすべてのデバイス
  • 特定の部屋でオンになっているすべてのデバイス

エンティティ フィルタを使用するには:

  1. Structure または Room のいずれかで、atExecutionTime() を呼び出します。これにより、TypedExpression<TypedEntity<StructureType>> が返されます。
  2. このオブジェクトで getDevicesOfType() を呼び出し、DeviceType を渡します。

エンティティ フィルタは、開始条件、状態リーダー、アクションで使用できます。

たとえば、オン/オフ ライトで開始条件から自動化をトリガーするには:

// If any light is turned on or off
val starter =
  starter(
    entityExpression = structure.atExecutionTime().getDevicesOfType(OnOffLightDevice),
    trait = OnOff,
  )

構造内のすべてのライト(具体的には、オン/オフのライト)の OnOff 状態をステート リーダーで取得するには:

// Build a Map<Entity, OnOff>
val onOffStateOfAllLights =
  stateReader(
    entityExpression = structure.atExecutionTime().getDevicesOfType(OnOffLightDevice),
    trait = OnOff,
  )

特定の部屋の照明を取得して条件で使用するには:

val livingRoomLights =
  stateReader(
    entityExpression = livingRoom.atExecutionTime().getDevicesOfType(OnOffLightDevice),
    trait = OnOff,
  )
// Are any of the lights in the living room on?
condition { expression = livingRoomLights.values.any { it.onOff equals true } }

実行時:

シナリオ 結果
開始条件の条件を満たすデバイスがありません。 自動化がトリガーされない。
状態リーダーの条件を満たすデバイスがない。 自動化は開始されますが、何も実行されません。
アクションの条件を満たすデバイスがない。 自動化は開始されますが、アクションは何も行いません。

次の例は、個々の照明がオフになるたびに、廊下の照明を除くすべての照明をオフにする自動化です。

val unused = automation {
  sequential {
    // If any light is turned on or off
    val starter =
      starter(
        entityExpression = structure.atExecutionTime().getDevicesOfType(OnOffLightDevice),
        trait = OnOff,
      )
    condition {
      // Check to see if the triggering light was turned off
      expression = starter.onOff equals false
    }
    // Turn off all lights except the hall light
    action(
      entityExpression =
        structure.atExecutionTime().getDevicesOfType(OnOffLightDevice).filter {
          it notEquals entity(hallwayLight, OnOffLightDevice)
        }
    ) {
      command(OnOff.on())
    }
  }
}

自動化を実行する

execute() メソッドを使用して、作成した自動化を実行します。

createdAutomation.execute()

自動化に手動開始条件がある場合、execute() はその時点から自動化を開始し、手動開始条件の前のすべてのノードを無視します。自動化に手動開始条件がない場合、実行は最初の開始条件ノードの次のノードから開始されます。

execute() オペレーションが失敗すると、HomeException がスローされることがあります。エラー処理をご覧ください。

自動化を停止する

stop() メソッドを使用して、実行中の自動化を停止します。


createdAutomation.stop()

stop() オペレーションが失敗すると、HomeException がスローされることがあります。エラー処理をご覧ください。

ストラクチャの自動化のリストを取得する

オートメーションはストラクチャ レベルで定義されます。構造の automations() を収集して、自動化の Flow にアクセスします。


import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
structure.automations().collect {
  println("Available automations:")
  for (automation in it) {
    println(String.format("%S %S", "$automation.id", "$automation.name"))
  }
}

または、ローカル Collection に割り当てます。

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

var myAutomations: Collection<Automation> = emptyList()
myAutomations = structure.automations()

ID で自動化を取得する

自動化 ID で自動化を取得するには、構造体で automations() メソッドを呼び出し、ID を照合します。

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: DraftAutomation = structure.automations().mapNotNull {
  it.firstOrNull
    { automation -> automation.id == Id("automation-id") }
  }.firstOrNull()

対応:

// Here's how the automation looks like in the get response.
// Here, it's represented as if calling a println(automation.toString())

Automation(
  name = "automation-name",
  description = "automation-description",
  isActive = true,
  id = Id("automation@automation-id"),
  automationGraph = SequentialFlow(
    nodes = [
      Starter(
        entity="device@test-device",
        type="home.matter.0000.types.0101",
        trait="OnOff@6789..."),
      Action(
        entity="device@test-device",
        type="home.matter.0000.types.0101",
        trait="OnOff@8765...",
        command="on")
    ]))

名前で自動化を取得する

Kotlin の filter() メソッドを使用すると、API 呼び出しをさらに絞り込むことができます。名前で自動化を取得するには、構造体の自動化を取得し、自動化名でフィルタします。

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: DraftAutomation = structure.automations().filter {
  it.name.equals("Sunset Blinds") }

デバイスのすべてのアクションを取得する

特定のデバイスを参照するすべての自動化を取得するには、ネストされたフィルタリングを使用して、各自動化の automationGraph をスキャンします。

import android.util.Log
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure
import com.google.home.automation.Action
import com.google.home.automation.Automation
import com.google.home.automation.Automation.automationGraph
import com.google.home.automation.Node
import com.google.home.automation.ParallelFlow
import com.google.home.automation.SelectFlow
import com.google.home.automation.SequentialFlow
import com.google.home.automation.Starter
import com.google.home.automation.StateReader

...

fun collectDescendants(node: Node): List<Node> {
  val d: MutableList<Node> = mutableListOf(node)

  val children: List<Node> =
    when (node) {
      is SequentialFlow -> node.nodes
      is ParallelFlow -> node.nodes
      is SelectFlow -> node.nodes
      else -> emptyList()
    }
  for (c in children) {
    d += collectDescendants(c)
  }
  return d
}

val myDeviceId = "device@452f78ce8-0143-84a-7e32-1d99ab54c83a"
val structure = homeManager.structures().list().single()
val automations =
  structure.automations().first().filter {
    automation: Automation ->
    collectDescendants(automation.automationGraph!!).any { node: Node ->
      when (node) {
        is Starter -> node.entity.id.id == myDeviceId
        is StateReader -> node.entity.id.id == myDeviceId
        is Action -> node.entity.id.id == myDeviceId
        else -> false
      }
    }
  }

自動化を更新する

自動化のメタデータを更新するには、update() メソッドを呼び出し、メタデータを設定するラムダ式を渡します。

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: DraftAutomation = structure.automations().mapNotNull {
  it.firstOrNull
    { automation -> automation.id == Id("automation-id") }
  }.firstOrNull()
automation.update { this.name = "Flashing lights 2" }

update() メソッドは、自動化グラフの完全な置き換えをサポートしていますが、グラフのノードごとの編集はサポートしていません。ノード間の相互依存関係があるため、ノードごとの編集はエラーが発生しやすくなります。自動化のロジックを変更する場合は、新しいグラフを生成して、既存のグラフを完全に置き換えます。

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: Automation = structure.automations().mapNotNull {
  it.firstOrNull
    { automation -> automation.id == Id("automation-id") }
  }.firstOrNull()
automation.update {
  this.automationGraph = sequential {
    val laundryWasherCompletionEvent =
      starter<_>(laundryWasher, LaundryWasherDevice, OperationCompletionEvent)
    condition {
      expression =
        laundryWasherCompletionEvent.completionErrorCode equals
          // UByte 0x00u means NoError
          0x00u
    }
    action(speaker, SpeakerDevice) { command(AssistantBroadcast.broadcast("laundry is done")) }
    }
  }
}

自動化を削除する

自動化を削除するには、構造体の deleteAutomation() メソッドを使用します。自動化は ID を使用して削除する必要があります。

import com.google.home.automation.Automation
import com.google.home.Home
import com.google.home.HomeDevice
import com.google.home.HomeManager
import com.google.home.Id
import com.google.home.Structure

...

val structure = homeManager.structures().list().single()
val automation: DraftAutomation = structure.automations().first()
structure.deleteAutomation(automation.id)

削除に失敗すると、HomeException がスローされることがあります。エラー処理をご覧ください。

デバイスの削除が自動化に与える影響

自動化で使用されているデバイスをユーザーが削除すると、削除されたデバイスは開始条件をトリガーできなくなり、自動化はそのデバイスから属性を読み取ったり、コマンドを発行したりできなくなります。たとえば、ユーザーが家から OccupancySensorDevice を削除し、自動化に OccupancySensorDevice に依存する開始条件がある場合、その開始条件は自動化を有効にできなくなります。