
-
EnergyKitによる家庭の電力消費の最適化
アプリでEnergyKitをサポートして、家庭での電力使用を最適化する方法を学びましょう。EnergyKitを利用すると、電力がよりクリーンで安価な時間帯に家電の使用やEVの充電を行うことができます。オンボーディング、充電スケジュールの作成、使用状況フィードバックを通じてユーザーに電力使用に関する分析情報を提供するための詳細方法を紹介します。
関連する章
- 0:00 - イントロダクション
- 2:08 - EnergyKitのオンボーディング
- 3:28 - 充電スケジュールの作成
- 7:35 - インサイト
- 14:58 - 次のステップ
リソース
関連ビデオ
WWDC25
-
このビデオを検索
こんにちは Dakshilです EnergyKitを紹介できるのを嬉しく思います 家庭の電力は 一日を通して 様々な供給源から供給されています 供給源には 太陽光や風力などの 再生可能エネルギーが含まれます 石炭や天然ガスなどの化石燃料もあります 私たちはグリッド予報という機能を ホームアプリに提供することから始めました この機能を使って 米国本土のユーザーは 比較的クリーンな電力源からの電力を 送電網で利用できる 時間帯を知ることができます またホームアプリ内のエネルギーカテゴリも よりパーソナルで実用的なものにしました これは家庭での電力使用状況を 直接体験に統合することで 実現しました EnergyKitはこれらの機能を推進します このフレームワークを 家庭用アプリで展開できることを 嬉しく思います EnergyKitを使用すると 地域の電力網に関する分析情報を アプリに統合できます そうすることで電力使用量の 削減やシフトをサポートし 節約しやすくします 例えば 電気自動車メーカーや スマートサーモスタットメーカーは EnergyKitで電気を使用する タイミングを選択できるので よりクリーンで割安になりうる 時間帯を優先的に利用できます 例えば EnergyKitにより 電気自動車メーカーは 送電網がよりクリーンで 割安になりうる時間帯に 充電スケジュールをシフトできます 今回ご紹介するのは EnergyKitをアプリに組み込む方法です 電力ガイダンスを取得する方法や アプリが管理する電気自動車の 充電スケジュールを調整する方法を学びます さらに EnergyKitのインサイトを使って 素晴らしいユーザー体験を生み出す 方法も紹介します
さっそく EnergyKitにユーザーを オンボードする方法を見ていきましょう
EnergyKitをアプリに統合するには まず クリーンエネルギー充電機能に オプトインする必要があり よりクリーンな電気を使うかどうかを 充電場所ごとに選択します サンプルアプリには 充電場所の一覧があります これは電気自動車を充電する場所を表します トグルを追加することで ユーザーがオプトインして 電気自動車のクリーンエネルギー充電機能を 選択できるようにしました
EnergyVenueは物理的な場所で アプリによって制御されるデバイスが 送電網から電力を引き出し 所有者がホームアプリまたは EnergyKitオンボーディングフローを介して ホームを構築します クリーンエネルギー充電を有効にすると その場所の近くのEnergyVenueのリストが 取得されます ユーザーが場所を選択したら 選択した場所と充電場所の マッピングを ユーザーがオプトアウトするまで 維持する必要があります ここでの推奨事項は 場所固有の識別子を ローカルに保存することです アプリを起動するたびに 選択した場所が存在することを 確認する必要があります 選択した場所を取得するには 前に保存した場所の識別子を使用します ユーザーがクリーンエネルギー充電を希望し EnergyVenueを選択したことを確認したので 次のステップで クリーンエネルギー充電の スケジュールを生成します クリーンエネルギー充電の スケジュールを生成するには スケジュールの指針となる予測が必要です この予測を電力ガイダンスと呼びます このガイダンスに利用される情報には 家庭の所在地 その場所に関する送電網情報 炭素排出量や 再生可能エネルギーによる発電の有無など 可能であれば公共料金の口座情報も含みます 2種類のガイダンスアクションがあります 削減とシフトです 「削減」はスマートサーモスタットなどの デバイスに使用し 電力使用量を 削減するように設計されています 「シフト」を使用するデバイスには 電気自動車などがあり ある時間帯から別の時間帯に 電力使用量をシフトできます 使用する電力量は同じです では例を見てみましょう この図はEnergyVenueの 電気ガイダンスを示しています ガイダンス値は0から1までで Y軸で表されます 値が低い時間帯は 電力がクリーンであるということで 料金プランの情報があれば 割安になりうることが示されます この例ではユーザーがホームアプリに 公共料金の口座をオンボードしています 加入しているのは時間帯別料金プランで 午後4時から9時の間は 電気料金が割高になります 車は午後6時30分頃に充電されます 充電の設定をみると 車の充電を終えるのが 午前9時頃です この時間まで充電可能ということですね
そのため 充電ウィンドウは 午後6時30分から翌日午前9時となります
充電スケジュールに ガイダンスが適用されない場合 車は午後6時30分に すぐに充電を開始する可能性があります クリーンさとコストの観点から 好ましいとは言えません 代わりに 時間帯を考慮して 充電スケジュールを最適化することで よりクリーンで割安になりうる 時間帯に車両を充電できます
選択したEnergyVenueで この電力ガイダンスを取得する 方法を見てみましょう まず streamGuidanceというメソッドを EnergyVenueManagerに追加します 更新を取得したら ガイダンス変数に格納します
ユースケースのガイダンスを取得する クエリを作成する必要があります 電気自動車の場合 推奨されるアクションは 電力使用量のシフトです これで EnergyVenueの ガイダンスを取得する準備が整いました ElectricityGuidanceには sharedServiceヘルパーがあります これはガイダンスを取得するために使用します AsyncSequenceを返します これが更新されるのは EnergyKitが 更新されたガイダンスを受け取ったときです 詳しくは ビデオ 「Demystify concurrency in SwiftUI」を ご覧ください 更新を継続的に検知する必要がない場合は 最初に取得した後 ループから抜け出すことができます タスクからstreamGuidanceを 呼び出すことができます アプリがバックグラウンドで 更新を要求する場合 これはバックグラウンドタスク ハンドラーから呼び出す必要があります アプリにインタラクティブな 充電ウィジェットがある場合 そのウィジェットを利用してガイダンスを 最新の状態に保つこともできます アプリが実行中でなくても可能です 詳しくは ビデオ「Finish tasks in the background」をご覧ください
これでガイダンス値を反復処理して より充電に適した時間帯を判断できます EnergyKitのインサイトが提供する 電力情報は ユーザーに提示しやすい形式になっています 提供されたインサイトを使用して ユーザーに通知できる内容は 車両の充電に使われた電力のうち よりクリーンな時間帯に 使われた電力の量です これは家庭での電気使用による 二酸化炭素排出量の削減に役立ちます 車両の電力使用量は 送電網のクリーンさによって分類され 3つのカテゴリに従って報告されます クリーン 削減 回避です 比較的割安な時間帯に 使用した電力についても ユーザーに通知できます 車両の電気使用量の分類は 電気料金プランごとに行われ 5つのカテゴリに従って報告されます スーパーオフピーク オフピーク 部分的ピーク ピーク クリティカルピークです
料金プランは料金表とも呼ばれます 次のトピックは これらのインサイトがどう機能するかです インサイトは様々な要因に影響を受けます 電力をシフトまたは削減するアルゴリズムは 提供されたガイダンスに基づいて 判断を下し 制御対象のデバイスが よりクリーンか割安な時間帯に 電力を利用するようにします デバイスが電力を利用する電力網の状態は 様々な要因で判断されます 電力需要 オンラインになっている再生可能発電機 電力網の排出量などです ユーザーがホームアプリに 公共料金の口座をオンボードし 時間帯別料金プランに加入している場合 電気使用量を様々な時間帯に分類して 相対的な電気使用料金を算出できます 特に重要なのは デバイスがいつ どのように電力を利用したかです これを決めるのは デバイスの動作 ユーザーによるインタラクション デバイスへのガイダンスの適用状況です EVメーカーの場合 これは車両の充電セッションの 動作でしょう 車両の充電についての インサイトを生成するには 使用する電気のクリーンさと 相対的なコストをふまえて アプリがEnergyKitに フィードバックを提供する必要があります このフィードバックをLoadEventsと呼びます 定期的にイベントを作成する必要があるのは 車両が充電され 充電セッション中に 充電状態が変化したタイミングです 充電セッションが始まると セッション開始時の車両の状態を表す イベントが作成されます 車両の充電に合わせて 定期的にイベントを作成します 車両が安定した速度で充電されている場合は 15分ごとに1回のイベントを推奨します 重大な事象 たとえば セッションの一時停止 新しいガイダンスセットの適用による 充電スケジュールの変更 消費電力の急激な変化が発生した場合は 同じようにイベントを作成します セッションの終了時に セッションを閉じるイベントで 車両の最終的な状態を表します 作成されたイベントのインサイトを EnergyKitで生成するには EnergyKitに提供する必要があります イベントは発生時に提供することも 定期的にバッチで提供することもできます 充電セッションの最後に バッチで提供しても大丈夫です パフォーマンスのためにイベントを バッチ提供することをお勧めします 充電セッション中に イベントを提供しないでください これらのイベントを作成して 提供する方法を見てみましょう 充電セッションでは 最も重要な情報は バッテリーの充電状態 特定のインスタンスで利用される電力 充電セッションの開始以降に 利用された累積電力です 車両が充電を開始すると それをセッションの始まりと考えます
このイベントは充電セッション開始時の 車両の初期状態を表します ガイダンスの状態は セッションが開始されたかどうかを示します 充電スケジュールが ガイダンストークンで表される 電力ガイダンスに従っていたためです このトークンは有効なガイダンスを取得した 電話に固有のもので その電話でイベントを 作成するために使用されます 充電が進むと セッションは アクティブとしてマークされます 充電セッションが終了すると セッションは終了イベントで閉じられます このイベントは充電セッション終了時の 車両の最終状態を表します サンプルアプリではLoadEventsを作成し キャッシュして バッチで提供する準備を整えます バッチ処理されたイベントは 車両を充電していた EnergyVenueに提供されます 提供されたイベントは Appleのプライバシーポリシーに従い デバイス上のCore Dataに保存され エンドツーエンドの暗号化でCloudKitに バックアップされます このデータはイベントが提供された EnergyVenueに関連付けられている HomeKitのホームをすでに共有している すべての人に共有されます これで インサイトを生み出すために必要な すべての情報がようやく手に入りました インサイトを取得するには まずクエリを作成します クエリでは クリーンさと 可能であれば相対的なコストに関する インサイトに関心があることを示します サンプルアプリでは 前日利用した 電力量の概要を表示させたいと思います クエリを作成したら 以前充電した場所での 車両のインサイトを取得できます AsyncStreamを返します 特定の日のインサイト関心があるので 要件に応じて ストリームをフィルタリングします 取得したインサイトを使用して 概要を作成できるようになりました これで アプリとEnergyKitが統合されました 今回のセッションでは EnergyKitとのインタラクションの 構成要素を説明しました オンボーディングでは ユーザーが アプリを操作してEnergyVenueを 選択する方法を確認しました その後 選択したEnergyVenueの使用方法と 電力ガイダンスの取得方法 そして それを使用して充電スケジュールを 生成する方法を説明しました
充電スケジュールを生成した後 車両が充電されると LoadEventsをフィードバックとして EnergyKitに提供し インサイトの生成を促す重要性に触れました 最後に 取得したガイダンスと アプリから提供された フィードバックに基づきインサイトを要求し ユーザー体験を構築する 方法を確認しました このセッションで使ったサンプルアプリを ダウンロードできます developer.apple.comでEnergyKitの ドキュメントにもアクセスできます Appleの環境保護への取り組みについて 詳しくはapple.com/2030をご覧ください 皆さんがEnergyKitを使用してどのような 素晴らしいアプリを構築するのか楽しみです
-
-
3:13 - Retrive an EnergyVenue
// Retrieve an EnergyVenue import EnergyKit import Foundation @Observable final class EnergyVenueManager { let venue: EnergyVenue init?(venueID: UUID) async { guard let energyVenue = await EnergyVenue.venue(for: venueID) else { return nil } venue = energyVenue } }
-
6:03 - Fetch Electricity Guidance at a selected EnergyVenue
// Fetch ElectricityGuidance import EnergyKit import Foundation @Observable final class EnergyVenueManager { // The current active guidance. var guidance: ElectricityGuidance? fileprivate func streamGuidance( venueID: UUID, update: (_ guidance: ElectricityGuidance) -> Void ) async throws { let query = ElectricityGuidance.Query(suggestedAction: .shift) for try await currentGuidance in ElectricityGuidance.sharedService.guidance( using: query, at: venueID ) { update(currentGuidance) break } } }
-
7:00 - Start monitoring Electricity Guidance
// Fetch ElectricityGuidance import EnergyKit import Foundation @Observable final class EnergyVenueManager { // The task used to stream guidance. private var streamGuidanceTask: Task<(), Error>? ///Start streaming guidance and store the value in the observed property 'guidance'. func startGuidanceMonitoring() { streamGuidanceTask?.cancel() streamGuidanceTask = Task.detached { [weak self] in if let venueID = self?.venue.id { try? await self?.streamGuidance(venueID: venueID) { guidance in self?.guidance = guidance if Task.isCancelled { return } } } } } }
-
11:30 - Update charging measurements
// Update charging measurements import EnergyKit // A controller that handles an electric vehicle @Observable class ElectricVehicleController { fileprivate func chargingMeasurement() -> ElectricVehicleLoadEvent.ElectricalMeasurement { let stateOfCharge = Int(configuration.state.stateOfCharge.rounded(.down)) let power = Measurement<UnitPower>( value: configuration.properties.chargingPower * 1000000, unit: .milliwatts ) let energy = Measurement<UnitEnergy>( value: configuration.state.cummulativeEnergy * 1000000, unit: .EnergyKit.milliwattHours ) return ElectricVehicleLoadEvent.ElectricalMeasurement( stateOfCharge: stateOfCharge, direction: .imported, power: power, energy: energy ) } }
-
11:50 - Start a session
// Start a session import EnergyKit // A controller that handles an electric vehicle @Observable class ElectricVehicleController { // The session var session: ElectricVehicleLoadEvent.Session? // The current guidance stored at the EV var currentGuidance: ElectricityGuidance // Whether the EV is following guidance var isFollowingGuidance: Bool = true fileprivate func beginSession() { session = ElectricVehicleLoadEvent.Session( id: UUID(), state: .begin, guidanceState: .init( wasFollowingGuidance: isFollowingGuidance, guidanceToken: currentGuidance.guidanceToken ) ) } }
-
12:25 - Update a session
// Update a session import EnergyKit // A controller that handles an electric vehicle @Observable class ElectricVehicleController { fileprivate func updateSession() { if let session { self.session = ElectricVehicleLoadEvent.Session( id: session.id, state: .active, guidanceState: .init( wasFollowingGuidance: isFollowingGuidance, guidanceToken: currentGuidance.guidanceToken ) ) } } }
-
12:31 - End a session
// End a session import EnergyKit // A controller that handles an electric vehicle. @Observable class ElectricVehicleController { fileprivate func endSession() { if let session { self.session = ElectricVehicleLoadEvent.Session( id: session.id, state: .end, guidanceState: .init( wasFollowingGuidance: isFollowingGuidance, guidanceToken: currentGuidance.guidanceToken ) ) } } }
-
12:43 - Create a load event
// Create a ElectricVehicleLoadEvent @Observable class ElectricVehicleController { fileprivate func createLoadEvent( sessionState: ElectricVehicleLoadEvent.Session.State ) { switch sessionState { case .begin: beginSession() case .active: updateSession() case .end: endSession() @unknown default: fatalError() } if let session { let event = ElectricVehicleLoadEvent( timestamp: configuration.state.timestamp, measurement: chargingMeasurement(), session: session, deviceID: configuration.properties.vehicleID ) events.append(event) } } }
-
12:50 - Submit events
// Submit events import EnergyKit // A controller that handles an electric vehicle @Observable class ElectricVehicleController { // EnergyVenue // The venue at which the EV uses energy var currentVenue: EnergyVenue // Electric EV Events // The list of generated EV load events var events = [ElectricVehicleLoadEvent]() func submitEvents() async throws { try await currentVenue.submitEvents(events) } }
-
13:25 - Create an insight query
// Create an insight query import EnergyKit @Observable final class EnergyVenueManager { func createInsightsQuery(on date: Date) -> ElectricityInsightQuery { return ElectricityInsightQuery( options: .cleanliness.union(.tariff), range: self.dayInterval(date: date), granularity: .daily, flowDirection: .imported ) } }
-
13:43 - Request insights
// Request an insights import EnergyKit @Observable final class EnergyVenueManager { func generateInsights(for vehicleIdentifier: String, on date: Date) async throws -> ElectricityInsightRecord<Measurement<UnitEnergy>>? { let query = createInsightsQuery(on: date) return try await ElectricityInsightService.shared.energyInsights( forDeviceID: vehicleIdentifier, using: query, atVenue: self.venue.id ).first { record in return record.range.start == query.range.start } } }
-