---
name: energykit
description: "EnergyKit on iOS 26: grid electricity forecasts via ElectricityGuidance, EnergyVenue, EV and HVAC load events, ElectricityInsightService history. Use when building smart home, EV charging, HVAC scheduling or energy dashboard features that shift home power use to cleaner or cheaper grid periods."
metadata:
  source: multi-agent-pipeline
---

# EnergyKit

EnergyKit tells an app when grid electricity is expected to be relatively
cleaner, and cheaper too when the person's rate plan is known, so the app can
move or trim the load of devices it controls. Examples use Swift 6.3 and
assume a deployment target of iOS 26.1 unless a snippet shows its own
availability check.

**Beta-sensitive.** EnergyKit is new in iOS 26 and its API may still shift.
Check Apple's current documentation before depending on any detail below.

A full observable manager, EV and HVAC session managers, a dashboard, chart
preparation, retries and venue discovery are in the
[EnergyKit patterns reference](references/energykit-patterns.md).
Out of scope: HomeKit automation (`homekit-matter`), scheduling background work
(`background-processing`) and app power diagnostics (`metrickit-diagnostics`).

## Setup

### Entitlement first

Add the **EnergyKit** capability, which writes the
`com.apple.developer.energykit` entitlement. Do this before writing any
guidance, venue, load-event or insight code; without it calls fail with errors
such as `EnergyKitError.permissionDenied`.

### Import and availability

```swift
import EnergyKit
```

The core API needs iOS 26.0 or iPadOS 26.0. Some pieces arrived in 26.1 and
need `if #available(iOS 26.1, *)` when the app still deploys to 26.0 (the `iOS`
platform name covers iPadOS; there is no separate `iPadOS` availability name):

- the grid-cleanliness insight breakdown (`dataByGridCleanliness`)
- venue lookup: `EnergyVenue.venues()`, `venue(for:)` and `venue(matchingHomeUniqueIdentifier:)`
- the error cases `.unsupportedRegion`, `.rateLimitExceeded` and `.locationServicesDenied`

Guidance queries, load-event submission and the insight service itself are 26.0.

## Concepts

EnergyKit does two jobs:

1. **Electricity guidance** - a forecast of weighted time intervals showing
   when power is cleaner and, with rate data, cheaper.
2. **Load events** - telemetry from managed devices such as EV chargers and
   heat pumps. The same device and app that asked for guidance submits them,
   and EnergyKit turns them into insights.

| Type | Role |
|---|---|
| `ElectricityGuidance` | Forecast made of weighted intervals |
| `ElectricityGuidance.Service` | Delivers guidance |
| `ElectricityGuidance.Query` | Declares whether you will shift or reduce load |
| `ElectricityGuidance.Value` | One interval with a 0.0 to 1.0 rating |
| `EnergyVenue` | A home registered for energy management |
| `ElectricVehicleLoadEvent` | EV charger telemetry |
| `ElectricHVACLoadEvent` | Heating and cooling telemetry |
| `ElectricityInsightService` | Answers energy and runtime history queries |
| `ElectricityInsightRecord` | Historical energy or runtime, optionally split by tariff or (26.1) cleanliness |
| `ElectricityInsightQuery` | Describes the history you want |

Suggested actions:

- `.shift` - the device can move its consumption to another time (EV charging).
- `.reduce` - the device can use less without stopping (a thermostat setback).

## Getting guidance

```swift
@MainActor
func watchChargingWindows(for venueID: UUID) async throws {
    let query = ElectricityGuidance.Query(suggestedAction: .shift)
    let service = ElectricityGuidance.sharedService

    for try await forecast in service.guidance(using: query, at: venueID) {
        ChargePlanner.shared.token = forecast.guidanceToken      // keep the real token
        ChargePlanner.shared.update(with: forecast.values)
        let ratePlanUsed = forecast.options.contains(.guidanceIncorporatesRatePlan)
        let hasRatePlan = forecast.options.contains(.locationHasRatePlan)
        ChargePlanner.shared.showsCostHint = ratePlanUsed || hasRatePlan
    }
}
```

An `ElectricityGuidance` exposes `guidanceToken`, `interval`, `energyVenueID`,
`options` and `values`.

### Reading the values

Each value has an `interval` and a `rating` from 0.0 (best) to 1.0 (worst).
Lower is better.

```swift
extension Array where Element == ElectricityGuidance.Value {
    var bestSlot: ElectricityGuidance.Value? { self.min { $0.rating < $1.rating } }
    func slots(atOrBelow limit: Double = 0.3) -> [ElectricityGuidance.Value] {
        filter { $0.rating <= limit }
    }
}
```

### Showing it in SwiftUI

```swift
struct ForecastList: View {
    let values: [ElectricityGuidance.Value]

    var body: some View {
        List(values, id: \.interval.start) { slot in
            HStack {
                Text(slot.interval.start, style: .time)
                Text("to")
                Text(slot.interval.end, style: .time)
                Spacer()
                RatingBadge(rating: slot.rating)
            }
        }
    }
}

struct RatingBadge: View {
    let rating: Double

    private var style: (label: String, tint: Color) {
        switch rating {
        case ...0.3: ("Good", .green)
        case ...0.6: ("Fair", .yellow)
        default:     ("Avoid", .red)
        }
    }

    var body: some View {
        Text(style.label)
            .font(.caption.bold())
            .padding(.horizontal, 8)
            .background(style.tint.opacity(0.2), in: .capsule)
            .foregroundStyle(style.tint)
    }
}
```

## Venues

```swift
let homes = try await EnergyVenue.venues()                             // every venue
let remembered = try await EnergyVenue.venue(for: savedVenueID)        // by UUID
let fromHomeKit = try await EnergyVenue.venue(matchingHomeUniqueIdentifier: home.uniqueIdentifier)
VenueMenu.shared.show(homes.map(\.name) + [remembered.name, fromHomeKit.name])
```

Always discover venues first. A hardcoded or invented venue UUID fails, and the
person may have no venue at all.

## Load events

Load events report actual consumption so the system can build insights. Only
the device and app that requested guidance submit them, and they carry the
`guidanceToken` EnergyKit returned. Never make one up.

### EV charging

```swift
func reportCharging(state: ElectricVehicleLoadEvent.Session.State,
                    sessionID: UUID, kilowatts: Double, kilowattHours: Double,
                    percent: Int, followingGuidance: Bool,
                    token: UUID, charger: String, venue: EnergyVenue) async throws {
    let session = ElectricVehicleLoadEvent.Session(
        id: sessionID,
        state: state,
        guidanceState: .init(wasFollowingGuidance: followingGuidance, guidanceToken: token)
    )
    let reading = ElectricVehicleLoadEvent.ElectricalMeasurement(
        stateOfCharge: percent,
        direction: .imported,
        power: Measurement(value: kilowatts, unit: .kilowatts),
        energy: Measurement(value: kilowattHours, unit: .kilowattHours)
    )
    let event = ElectricVehicleLoadEvent(timestamp: .now, measurement: reading,
                                         session: session, deviceID: charger)
    try await venue.submitEvents([event])
}
```

### Heating and cooling

```swift
let hvacSession = ElectricHVACLoadEvent.Session(
    id: runID, state: .active,
    guidanceState: .init(wasFollowingGuidance: true, guidanceToken: token))
let stageReading = ElectricHVACLoadEvent.ElectricalMeasurement(stage: 2)
let hvacEvent = ElectricHVACLoadEvent(timestamp: .now, measurement: stageReading,
                                      session: hvacSession, deviceID: thermostatID)
try await venue.submitEvents([hvacEvent])
```

### Session lifecycle and cadence

| State | When |
|---|---|
| `.begin` | The device starts drawing power |
| `.active` | Periodic updates while it runs |
| `.end` | The device stops |

- **EV**: a begin and an end event, a steady sample roughly every 15 minutes,
  and extra samples when the user acts, charging pauses, new guidance arrives,
  or power changes quickly.
- **HVAC**: separate events when equipment starts, at every heating or cooling
  stage change (heat stage 1 to 2, heat to cool, cool to idle), and when it stops.
- Batch events when you can for performance.
- Insights exist only for events you submitted.
- Everyone who shares the associated home in the Home app can see that venue's
  load events. Tell users.

## Insights

`ElectricityInsightService` returns energy or runtime history. An empty
`ElectricityInsightQuery.Options` means totals only; add `.cleanliness`,
`.tariff` or both only when the interface shows those splits. MetricKit power
metrics are not a replacement: insights come from the EnergyKit load events
of the managed device.

Match granularity to the range. A seven-day chart uses `.hourly`; `.daily`
needs at least a calendar month.

| Granularity | Shortest allowed range |
|---|---|
| `.hourly` | One calendar week |
| `.daily` | One calendar month |
| `.weekly` | Six months |
| `.monthly`, `.yearly` | One calendar year |

`ElectricityInsightService` is an actor and `ElectricityInsightRecord` is not
`Sendable`, so in Swift 6 the returned stream cannot leave the actor: a call
from a nonisolated or main-actor function fails to compile. Run the loop on the
service's own isolation with an `isolated` parameter and hand back only
`Sendable` values.

```swift
func lastWeekUsage(charger: String, venueID: UUID,
                   service: isolated ElectricityInsightService = .shared) async throws -> [Double] {
    let week = DateInterval(start: .now.addingTimeInterval(-7 * 86_400), end: .now)
    let query = ElectricityInsightQuery(options: [], range: week,
                                        granularity: .hourly, flowDirection: .imported)
    var totals: [Double] = []
    let stream = try await service.energyInsights(forDeviceID: charger, using: query,
                                                  atVenue: venueID)
    for await record in stream {
        totals.append(record.totalEnergy?.converted(to: .kilowattHours).value ?? 0)
        if #available(iOS 26.1, *) {           // only needed when deploying to 26.0
            _ = record.dataByGridCleanliness?.cleaner   // needs .cleanliness in options
        }
    }
    return totals
}
```

For time running rather than energy, call
`runtimeInsights(forDeviceID:using:atVenue:)` in the same isolated way.

## Errors

```swift
do {
    try await watchChargingWindows(for: venueID)
} catch let error as EnergyKitError {
    switch error {
    case .unsupportedRegion:   showMessage("Grid forecasts aren't offered where you live yet.")
    case .guidanceUnavailable: showMessage("No forecast right now.")
    case .venueUnavailable:    showMessage("Set up your home in the Home app first.")
    case .permissionDenied:    showMessage("EnergyKit access is off.")
    case .serviceUnavailable:  scheduleRetry()
    case .rateLimitExceeded:   backOff()
    default:                   showMessage("Something went wrong.")
    }
}
```

## Common mistakes

- Missing entitlement, which surfaces as `permissionDenied`. Treat the capability as setup.
- Assuming every region is supported. Handle `.unsupportedRegion` and `.guidanceUnavailable`.
- Dropping the guidance token, or passing `UUID()` instead of the stored `guidanceToken` from the device that fetched guidance.
- Sending one event with no lifecycle. Send `.begin`, then `.active` updates, then `.end`.
- Querying with a fixed or fake venue UUID instead of calling `EnergyVenue.venues()` and handling an empty result.

## Review checklist

- [ ] EnergyKit entitlement present
- [ ] `.unsupportedRegion` shown to the user clearly
- [ ] `.permissionDenied` handled
- [ ] Guidance token kept and attached to load events
- [ ] No placeholder or invented tokens
- [ ] Load events come from the device and app that requested guidance
- [ ] Venues discovered before guidance is requested
- [ ] Sessions go `.begin`, `.active`, `.end`
- [ ] EV and HVAC cadence follow Apple's guidance; events batched where practical
- [ ] Ratings treated as lower-is-better
- [ ] `.shift` for EV, `.reduce` for HVAC
- [ ] Insight ranges meet the granularity minimums
- [ ] Empty options understood as totals only
- [ ] No MetricKit stand-in for insights
- [ ] Grid-cleanliness fields, venue lookup and the newer error cases behind a 26.1 check when the target is 26.0
- [ ] Users told that Home members can see load events
- [ ] `.rateLimitExceeded` backs off
- [ ] `.serviceUnavailable` retried later

## References

- [EnergyKit patterns](references/energykit-patterns.md) - managers, dashboard, charts, retries, venue discovery
- [EnergyKit](https://developer.apple.com/documentation/energykit)
- [ElectricityGuidance](https://developer.apple.com/documentation/energykit/electricityguidance),
  [Service](https://developer.apple.com/documentation/energykit/electricityguidance/service),
  [Query](https://developer.apple.com/documentation/energykit/electricityguidance/query),
  [Value](https://developer.apple.com/documentation/energykit/electricityguidance/value)
- [EnergyVenue](https://developer.apple.com/documentation/energykit/energyvenue)
- [ElectricVehicleLoadEvent](https://developer.apple.com/documentation/energykit/electricvehicleloadevent)
- [ElectricHVACLoadEvent](https://developer.apple.com/documentation/energykit/electrichvacloadevent)
- [ElectricityInsightService](https://developer.apple.com/documentation/energykit/electricityinsightservice)
- [ElectricityInsightRecord](https://developer.apple.com/documentation/energykit/electricityinsightrecord)
- [ElectricityInsightQuery](https://developer.apple.com/documentation/energykit/electricityinsightquery)
- [EnergyKitError](https://developer.apple.com/documentation/energykit/energykiterror)
- Apple's article on optimizing home electricity usage
