Highlight
EnergyKit 把电网清洁度与电价的实时预测以 AsyncSequence 形式开放给 App,让电动车和智能恒温器可以把负载挪到电更干净、更便宜的时段。
核心内容
晚上 6:30 把电动车插上电,车自动开始充电——这是大多数 App 的默认行为。问题在于,6:30 到 9:00 是美国大多数时区的用电高峰,电价最贵,电网里燃煤和天然气机组贡献最多。等到凌晨风电上线、负荷下来,车其实早就充满了。用户既花了更多钱,又用了更脏的电。Grid Forecast 这个 Home App 的功能去年就给用户看到了这个问题,但第三方 App 没法把这种”什么时候电更干净”的信号接进自己的充电调度逻辑。
WWDC25 的 EnergyKit 把驱动 Grid Forecast 的那套数据直接暴露成框架。核心抽象是 EnergyVenue(一个物理充电地点,绑定到 HomeKit Home)和 ElectricityGuidance(0 到 1 的预测值,越低表示越干净/便宜)。EV 厂商和恒温器厂商现在可以拉到 24 小时窗口的引导值,把”立刻开始充电”换成”在引导值最低的几个小时里充电”。配套的 LoadEvent 反馈通道把实际充电行为回写给 EnergyKit,最终生成”本次充电期间多少电来自清洁时段”的洞察,回到用户面前。整套数据存在 Core Data 本地,端到端加密同步到 CloudKit,跟 HomeKit Home 的成员共享。
详细内容
EnergyKit 的接入分四步:选定 venue、订阅 guidance、回报 LoadEvents、查询 insights。
第一步是 onboarding(03:13)。用户在 App 里给某个充电地点开启 Clean Energy Charging,App 拿到该地点附近的 EnergyVenue 列表让用户选择,然后把 venue 的 UUID 持久化到本地。每次启动重新校验 venue 是否仍然存在:
// 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
}
}
关键点:
EnergyVenue.venue(for:)是 async 调用,因为 venue 元数据可能需要从系统服务读取。- 返回 optional——venue 可能因为用户在 Home App 删除而失效,所以构造器是 failable。
- 持久化的应该是
UUID,不是整个 venue 对象;venue 本身每次启动重新解析。
第二步是订阅 guidance(06:03)。ElectricityGuidance.Query 选择 action 类型——EV 用 .shift(同样的电量挪到更好的时段),恒温器用 .reduce(直接少用)。sharedService.guidance(...) 返回 AsyncSequence,每次电网预测刷新都会推一个新值过来:
// 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
}
}
}
关键点:
Query(suggestedAction: .shift)明确告诉系统你是位移型负载(EV),系统会按这个语义优化预测。for try await是 AsyncSequence 的标准消费方式,配合 throws 处理网络/权限失败。break体现一次性消费——只取最新的一份 guidance 用来生成调度,不持续监听。update用回调而不是直接写属性,方便调用方在 actor 边界外更新观察对象。
如果需要长期跟踪(比如充电过程中遇到电网突变要重新调度),把 break 去掉就好。后台刷新场景必须包到 BackgroundTask 里(07:00):
// 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
}
}
}
}
}
}
关键点:
streamGuidanceTask?.cancel()在启动新流之前先取消旧的,避免泄漏并发任务。Task.detached与[weak self]保证 manager 释放时任务不阻止其析构。Task.isCancelled检查放在 update 闭包里,确保取消信号能及时退出回调链。
第三步是 LoadEvents 反馈(11:30 起)。一次充电会话由 begin / active / end 三种 state 组成,Apple 推荐稳态下每 15 分钟生成一个 active 事件,遇到暂停、调度变更、功率突变时也要生事件:
// 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
)
)
}
}
关键点:
- 一次会话用一个
UUID贯穿 begin/active/end,三个事件共享同一个 id。 guidanceToken来自当时拉到的那份 guidance,且只对获取它的那台手机有效。wasFollowingGuidance让 EnergyKit 区分”用户主动覆盖”和”按引导执行”两种调度行为。
测量数据用 Foundation 的 Measurement<UnitPower> 和 Measurement<UnitEnergy>,单位是毫瓦和毫瓦时(11:30)。事件创建后批量提交:
// 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)
}
}
关键点:
submitEvents直接挂在 venue 上——事件归属哪个充电地点是固定的,不需要再传。- 官方明确推荐批量提交以减少 IPC 开销,且禁止在两次充电之间提交事件。
- 提交的数据走 CloudKit 端到端加密,跟 HomeKit Home 的家庭成员共享。
第四步是查询 insights(13:25)。ElectricityInsightQuery 用 .cleanliness 和 .tariff 的 OptionSet 决定要哪些维度,granularity 选 .daily 时返回每日聚合:
// 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
)
}
}
关键点:
flowDirection: .imported表示从电网”取电”(充电),如果做 V2G 反向放电要换成 exported。.cleanliness把电量分桶到 clean/reduce/avoid 三档;.tariff分到 super off peak 到 critical peak 五档。- tariff 维度只在用户在 Home App 绑定了电力账户、且套餐是 time-of-use 时才有数据。
整体流程下来,App 从”有没有电”变成”电从哪来、几点用最划算、用户实际用得怎么样”。
核心启发
-
做什么:在你的 EV 或储能 App 里加一个 Clean Energy Charging 开关
- 为什么值得做:默认即时充电会让用户在峰段用最贵最脏的电,开启 EnergyKit 后系统替你算好窗口,App 只需要在更优时段触发充电;这是用户能感知的差异化卖点。
- 怎么开始:先做 onboarding 流程,让用户给每个充电地点选
EnergyVenue并存 UUID;再在原有充电触发逻辑里插入一个streamGuidance调用,按返回值决定是否立刻启动。
-
做什么:把 LoadEvents 当作产品分析数据回灌
- 为什么值得做:LoadEvents 不只是给 EnergyKit 算 insights 的,它本身就是一份带功率/电量/时间戳的细粒度充电日志,回到 App 里可以做”本周清洁充电占比”卡片、年度回顾、社区排行等内容。
- 怎么开始:在充电控制器里加一层 event cache,按 15 分钟窗口或功率变化触发 append;会话结束统一
submitEvents,同时把这份缓存复制一份留作本地分析。
-
做什么:智能恒温器/热水器接
.reduce引导- 为什么值得做:EV 是 shift(挪时段),但恒温器是 reduce(直接少用),EnergyKit 同一个框架两种语义都覆盖;对配电紧张地区的用户来说,关键时段降一两度是真金白银。
- 怎么开始:把 query 的 suggestedAction 换成
.reduce,引导值高时把目标温度调宽 1–2°C 并通知用户原因;配合 critical peak 标签做更激进的策略。
-
做什么:用 BackgroundTask 维持长充电的 guidance 实时性
- 为什么值得做:一次 Level 2 充电可能 6–8 小时,App 不可能一直前台运行;guidance 在中途刷新时要能让车跟上。
- 怎么开始:把
streamGuidanceTask包到BGAppRefreshTask或 widget 的更新回调里,guidance 变化超过阈值就重排 charging schedule,并写一个新的guidanceToken进下一批 LoadEvent。
关联 Session
- Finish tasks in the background — 在后台任务里持续刷新 ElectricityGuidance 的官方推荐做法
- Demystify concurrency in SwiftUI — 理解 EnergyKit AsyncSequence 在 SwiftUI 中的消费方式
- Meet the Smart Home — Home app 与 HomeKit Home 的整体能力背景
- Build a UIKit document-based app — 同期 system services 框架的另一种系统集成范式
评论
GitHub Issues · utterances