
Energykit
- 2.5k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
energykit is an iOS skill for ElectricityGuidance forecasts and EV or HVAC load event submission via EnergyKit.
About
EnergyKit helps smart home and energy apps shift or reduce electricity use using grid forecasts and load telemetry on iOS 26 plus with Swift 6.3. Setup requires the com.apple.developer.energykit entitlement enabled in Xcode before any guidance or load-event code. ElectricityGuidance.Service streams time-weighted forecasts with ratings from zero best to one worst, supporting shift actions for movable loads like EV charging and reduce actions for HVAC setback. Queries use guidance(using:at:) async sequences per EnergyVenue, checking options for rate plan incorporation. Apps submit ElectricVehicleLoadEvent and ElectricHVACLoadEvent telemetry from the same device that requested guidance so insights stay consistent. SwiftUI examples render guidance timelines and best charging windows via min rating selection. Insight APIs on iOS 26.1 plus expose historical energy and runtime records with optional tariff or grid cleanliness breakdowns. Beta sensitivity notes warn APIs may change before GM and Apple docs should be rechecked. Common mistakes and a review checklist cover permissionDenied, venue registration, and token handling.
- Requires EnergyKit entitlement before guidance or load-event APIs.
- Guidance ratings run 0.0 best to 1.0 worst per time interval.
- Shift suits EV charging; reduce suits HVAC setback scenarios.
- Load events must come from the guidance-requesting device.
- Beta APIs on iOS 26 may change; verify current Apple documentation.
Energykit by the numbers
- 2,545 all-time installs (skills.sh)
- +107 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #84 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
energykit capabilities & compatibility
- Capabilities
- electricityguidance async stream consumption · shift and reduce action query types · energyvenue and entitlement setup · ev and hvac load event submission · swiftui timeline display patterns
- Use cases
- frontend · api development · ui design
- Platforms
- macOS
- Runs
- Runs locally
- Pricing
- Free
What energykit says it does
Lower ratings indicate better times to use electricity.
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill energykitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.5k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
How do I schedule EV charging or HVAC using grid cleanliness and cost guidance on iOS?
Query grid electricity guidance and submit EV or HVAC load events with EnergyKit on iOS 26+.
Who is it for?
Smart home and energy apps optimizing consumption with Apple grid guidance.
Skip if: Skip for non-Apple platforms or apps without managed EV or HVAC devices.
When should I use this skill?
User mentions EnergyKit, ElectricityGuidance, load events, or grid-aware charging.
What you get
Working guidance streams, venue setup, and load events aligned to EnergyKit beta APIs.
- EnergyKit Swift integration code
- Load event lifecycle guidance
Files
EnergyKit
Provide grid electricity forecasts to help users choose when to use electricity. EnergyKit identifies times when grid electricity is relatively cleaner and, when cost information is available, less expensive. Apps use that guidance to shift or reduce managed device load. Targets Swift 6.3 / iOS 26+.
Beta-sensitive. EnergyKit is new in iOS 26 and may change before GM.
Re-check current Apple documentation before relying on specific API details.
Contents
- Setup
- Core Concepts
- Querying Electricity Guidance
- Working with Guidance Values
- Energy Venues
- Submitting Load Events
- Electricity Insights
- Common Mistakes
- Review Checklist
- References
Setup
Entitlement
EnergyKit requires the com.apple.developer.energykit entitlement. Enable the EnergyKit capability in Xcode so the entitlement is added to the app target. Treat this as a top-level setup prerequisite before writing guidance queries, venue lookup, load-event submission, or insight code. Missing permission can surface as EnergyKitError.permissionDenied.
Import
import EnergyKitPlatform availability: Core EnergyKit APIs are iOS 26.0+ and iPadOS 26.0+. Some insight breakdown APIs, including grid cleanliness categories, are iOS 26.1+ / iPadOS 26.1+ and need availability guards.
Core Concepts
EnergyKit provides two main capabilities:
1. Electricity Guidance -- time-weighted forecasts telling apps when electricity is cleaner and, when rate data is available, less expensive 2. Load Events -- telemetry from managed devices (EV chargers, HVAC) submitted by the same device/app that requested guidance so EnergyKit can generate insights
Key Types
| Type | Role |
|---|---|
ElectricityGuidance | Forecast data with weighted time intervals |
ElectricityGuidance.Service | Interface for obtaining guidance data |
ElectricityGuidance.Query | Query specifying shift or reduce action |
ElectricityGuidance.Value | A time interval with a rating (0.0-1.0) |
EnergyVenue | A physical location (home) registered for energy management |
ElectricVehicleLoadEvent | Load event for EV charger telemetry |
ElectricHVACLoadEvent | Load event for HVAC system telemetry |
ElectricityInsightService | Service for querying energy/runtime insights |
ElectricityInsightRecord | Historical energy or runtime data, optionally broken down by tariff or 26.1+ grid cleanliness |
ElectricityInsightQuery | Query for historical insight data |
Suggested Actions
| Action | Use Case |
|---|---|
.shift | Devices that can move consumption to a different time (EV charging) |
.reduce | Devices that can lower consumption without stopping (HVAC setback) |
Querying Electricity Guidance
Use ElectricityGuidance.Service to get a forecast stream for a venue.
import EnergyKit
func observeGuidance(venueID: UUID) async throws {
let query = ElectricityGuidance.Query(suggestedAction: .shift)
let service = ElectricityGuidance.sharedService
let guidanceStream = service.guidance(using: query, at: venueID)
for try await guidance in guidanceStream {
print("Guidance token: \(guidance.guidanceToken)")
print("Interval: \(guidance.interval)")
print("Venue: \(guidance.energyVenueID)")
// Check if rate plan information is available
if guidance.options.contains(.guidanceIncorporatesRatePlan) {
print("Rate plan data incorporated")
}
if guidance.options.contains(.locationHasRatePlan) {
print("Location has a rate plan")
}
processGuidanceValues(guidance.values)
}
}Working with Guidance Values
Each ElectricityGuidance.Value contains a time interval and a rating from 0.0 to 1.0. Lower ratings indicate better times to use electricity.
func processGuidanceValues(_ values: [ElectricityGuidance.Value]) {
for value in values {
let interval = value.interval
let rating = value.rating // 0.0 (best) to 1.0 (worst)
print("From \(interval.start) to \(interval.end): rating \(rating)")
}
}
// Find the best time to charge
func bestChargingWindow(
in values: [ElectricityGuidance.Value]
) -> ElectricityGuidance.Value? {
values.min(by: { $0.rating < $1.rating })
}
// Find all "good" windows below a threshold
func goodWindows(
in values: [ElectricityGuidance.Value],
threshold: Double = 0.3
) -> [ElectricityGuidance.Value] {
values.filter { $0.rating <= threshold }
}Displaying Guidance in SwiftUI
import SwiftUI
import EnergyKit
struct GuidanceTimelineView: View {
let values: [ElectricityGuidance.Value]
var body: some View {
List(values, id: \.interval.start) { value in
HStack {
VStack(alignment: .leading) {
Text(value.interval.start, style: .time)
Text(value.interval.end, style: .time)
.foregroundStyle(.secondary)
}
Spacer()
RatingIndicator(rating: value.rating)
}
}
}
}
struct RatingIndicator: View {
let rating: Double
var color: Color {
if rating <= 0.3 { return .green }
if rating <= 0.6 { return .yellow }
return .red
}
var label: String {
if rating <= 0.3 { return "Good" }
if rating <= 0.6 { return "Fair" }
return "Avoid"
}
var body: some View {
Text(label)
.padding(.horizontal)
.padding(.vertical)
.background(color.opacity(0.2))
.foregroundStyle(color)
.clipShape(Capsule())
}
}Energy Venues
An EnergyVenue represents a physical location registered for energy management.
// List all venues
func listVenues() async throws -> [EnergyVenue] {
try await EnergyVenue.venues()
}
// Get a specific venue by ID
func getVenue(id: UUID) async throws -> EnergyVenue {
try await EnergyVenue.venue(for: id)
}
// Get a venue matching a HomeKit home
func getVenueForHome(homeID: UUID) async throws -> EnergyVenue {
try await EnergyVenue.venue(matchingHomeUniqueIdentifier: homeID)
}Venue Properties
let venue = try await EnergyVenue.venue(for: venueID)
print("Venue ID: \(venue.id)")
print("Venue name: \(venue.name)")Submitting Load Events
Report device consumption data back to the system. This helps the system generate electricity insights. The same EnergyKit-capable device/app that requested electricity guidance must submit the corresponding load events, using the guidance token returned by EnergyKit. Do not invent a token.
EV Charger Load Events
func submitEVChargingEvent(
at venue: EnergyVenue,
guidanceToken: UUID,
deviceID: String
) async throws {
let session = ElectricVehicleLoadEvent.Session(
id: UUID(),
state: .begin,
guidanceState: ElectricVehicleLoadEvent.Session.GuidanceState(
wasFollowingGuidance: true,
guidanceToken: guidanceToken
)
)
let measurement = ElectricVehicleLoadEvent.ElectricalMeasurement(
stateOfCharge: 45,
direction: .imported,
power: Measurement(value: 7.2, unit: .kilowatts),
energy: Measurement(value: 0, unit: .kilowattHours)
)
let event = ElectricVehicleLoadEvent(
timestamp: Date(),
measurement: measurement,
session: session,
deviceID: deviceID
)
try await venue.submitEvents([event])
}HVAC Load Events
func submitHVACEvent(
at venue: EnergyVenue,
guidanceToken: UUID,
stage: Int,
deviceID: String
) async throws {
let session = ElectricHVACLoadEvent.Session(
id: UUID(),
state: .active,
guidanceState: ElectricHVACLoadEvent.Session.GuidanceState(
wasFollowingGuidance: true,
guidanceToken: guidanceToken
)
)
let measurement = ElectricHVACLoadEvent.ElectricalMeasurement(stage: stage)
let event = ElectricHVACLoadEvent(
timestamp: Date(),
measurement: measurement,
session: session,
deviceID: deviceID
)
try await venue.submitEvents([event])
}Session States
| State | When to Use |
|---|---|
.begin | Device starts consuming electricity |
.active | Device is actively consuming (periodic updates) |
.end | Device stops consuming electricity |
For EV charging, record begin/end, one steady sample about every 15 minutes, and extra samples for user actions, pauses, new guidance, or rapid power changes. For HVAC, submit separate events when equipment starts, when heating or cooling stage changes (heat stage 1 -> 2, heat -> cool, cool -> idle), and when equipment stops. Batch events when practical for performance. Insights are only available for submitted events, and load events for an EnergyVenue are visible to people who share the associated Home in the Home app.
Electricity Insights
Query historical energy and runtime data for devices using ElectricityInsightService. An empty ElectricityInsightQuery.Options option set returns totals only; it does not populate cleanliness or tariff breakdowns. Request .cleanliness and/or .tariff only when the UI needs those breakdowns. Do not substitute MetricKit app power metrics for EnergyKit insights; EnergyKit insights depend on EnergyKit load events submitted for the managed device.
Choose insight granularity from the requested range. For a seven-day view, query .hourly; use .daily only when the query covers at least a calendar month.
func queryEnergyInsights(deviceID: String, venueID: UUID) async throws {
let sevenDaysAgo = Calendar.current.date(
byAdding: .day,
value: -7,
to: Date()
)!
let query = ElectricityInsightQuery(
options: [.cleanliness, .tariff],
range: DateInterval(
start: sevenDaysAgo,
end: Date()
),
granularity: .hourly,
flowDirection: .imported
)
let service = ElectricityInsightService.shared
let stream = try await service.energyInsights(
forDeviceID: deviceID, using: query, atVenue: venueID
)
for await record in stream {
if let total = record.totalEnergy { print("Total: \(total)") }
if #available(iOS 26.1, iPadOS 26.1, *),
let cleaner = record.dataByGridCleanliness?.cleaner {
print("Cleaner: \(cleaner)")
}
}
}Use runtimeInsights(forDeviceID:using:atVenue:) for runtime data instead of energy. Granularity options: .hourly, .daily, .weekly, .monthly, .yearly. Choose a range that matches Apple's minimum aggregation windows: hourly for at least a calendar week, daily for at least a calendar month, weekly for at least six months, and monthly or yearly for at least a calendar year. See references/energykit-patterns.md for full insight examples.
Common Mistakes
DON'T: Forget the EnergyKit entitlement
Without the entitlement, EnergyKit APIs can fail with permission errors such as EnergyKitError.permissionDenied. Treat the EnergyKit capability as setup, not as an implementation detail to discover after writing queries.
DON'T: Ignore unsupported regions
EnergyKit is not available in all regions. Handle the .unsupportedRegion and .guidanceUnavailable errors.
// WRONG: Assume guidance is always available
for try await guidance in service.guidance(using: query, at: venueID) {
updateUI(guidance)
}
// CORRECT: Handle region-specific errors
do {
for try await guidance in service.guidance(using: query, at: venueID) {
updateUI(guidance)
}
} catch let error as EnergyKitError {
switch error {
case .unsupportedRegion:
showUnsupportedRegionMessage()
case .guidanceUnavailable:
showGuidanceUnavailableMessage()
case .venueUnavailable:
showNoVenueMessage()
case .permissionDenied:
showPermissionDeniedMessage()
case .serviceUnavailable:
retryLater()
case .rateLimitExceeded:
backOff()
default:
break
}
}DON'T: Discard the guidance token
The guidanceToken links load events to the guidance that was in effect. Store the token returned from EnergyKit on the device that fetched it and pass that real token to load event submissions.
// WRONG: Ignore the guidance token
for try await guidance in guidanceStream {
startCharging(followingGuidanceToken: UUID()) // fabricated token
}
// CORRECT: Store the token for load events
for try await guidance in guidanceStream {
let token = guidance.guidanceToken
startCharging(followingGuidanceToken: token)
}DON'T: Submit load events without a session lifecycle
Always submit .begin, then .active updates, then .end events.
// WRONG: Only submit one event
let event = ElectricVehicleLoadEvent(/* state: .active */)
try await venue.submitEvents([event])
// CORRECT: Full session lifecycle
try await venue.submitEvents([beginEvent])
// ... periodic active events ...
try await venue.submitEvents([activeEvent])
// ... when done ...
try await venue.submitEvents([endEvent])DON'T: Query guidance without a venue
EnergyKit requires a venue ID. List venues first and select the appropriate one.
// WRONG: Use a hardcoded UUID
let fakeID = UUID()
service.guidance(using: query, at: fakeID) // Will fail
// CORRECT: Discover venues first
let venues = try await EnergyVenue.venues()
guard let venue = venues.first else {
showNoVenueSetup()
return
}
let guidanceStream = service.guidance(using: query, at: venue.id)Review Checklist
- [ ]
com.apple.developer.energykitentitlement added to the project - [ ]
EnergyKitError.unsupportedRegionhandled with user-facing message - [ ]
EnergyKitError.permissionDeniedhandled gracefully - [ ] Guidance token stored and passed to load event submissions
- [ ] No placeholder or fabricated guidance tokens are used in load events
- [ ] The same EnergyKit-capable device/app that requested guidance submits the corresponding load events
- [ ] Venues discovered via
EnergyVenue.venues()before querying guidance - [ ] Load event sessions follow
.begin->.active->.endlifecycle - [ ] EV/HVAC event cadence follows Apple guidance and events are batched when practical
- [ ]
ElectricityGuidance.Value.ratinginterpreted correctly (lower is better) - [ ]
SuggestedActionmatches the device type (.shiftfor EV,.reducefor HVAC) - [ ] Insight queries use appropriate minimum ranges for their granularity
- [ ] Empty insight query options are treated as totals-only, not as cleanliness or tariff requests
- [ ] MetricKit power telemetry is not used as a substitute for EnergyKit load events or insights
- [ ] Grid cleanliness insight fields are guarded for iOS/iPadOS 26.1+
- [ ] Users understand load events are shared with people who share the Home
- [ ] Rate limiting handled via
EnergyKitError.rateLimitExceeded - [ ] Service unavailability handled with retry logic
References
- Extended patterns (full app architecture, SwiftUI dashboard): references/energykit-patterns.md
- EnergyKit framework
- ElectricityGuidance
- ElectricityGuidance.Service
- ElectricityGuidance.Query
- ElectricityGuidance.Value
- EnergyVenue
- ElectricVehicleLoadEvent
- ElectricHVACLoadEvent
- ElectricityInsightService
- ElectricityInsightRecord
- ElectricityInsightQuery
- EnergyKitError
- Optimizing home electricity usage
{
"skill_name": "energykit",
"evals": [
{
"id": 1,
"name": "ev-guidance-load-events",
"prompt": "Review this EnergyKit EV charging integration. It enables the capability, fetches ElectricityGuidance for an EnergyVenue, builds a charging schedule, then submits one ElectricVehicleLoadEvent at the end of the session with `GuidanceState(wasFollowingGuidance: true, guidanceToken: UUID())`. Give corrected Swift-level guidance without turning this into a HomeKit or BackgroundTasks tutorial.",
"expected_output": "A concise review that preserves the EnergyKit boundary, uses the real guidance token, and explains the load event lifecycle and cadence for EV charging.",
"files": [],
"assertions": [
"Lists the EnergyKit capability or `com.apple.developer.energykit` entitlement as a setup prerequisite, not only as an error-handling detail.",
"Rejects fabricating a guidance token and says the event must use the `guidanceToken` returned by EnergyKit guidance on the device/app that requested it.",
"Requires a session lifecycle with `.begin`, `.active`, and `.end` events rather than only one final event.",
"Mentions EV sampling about every 15 minutes when charging is steady and additional events for significant changes such as user actions, pauses, new guidance, or rapid power changes.",
"Keeps the answer focused on EnergyKit rather than expanding into HomeKit automation or BackgroundTasks implementation details."
]
},
{
"id": 2,
"name": "insight-availability-granularity",
"prompt": "Fix this EnergyKit insight snippet for an app that supports iOS 26.0: it requests `.daily` insights for the last 7 days and immediately reads `record.dataByGridCleanliness?.cleaner`. Include the right availability and range guidance.",
"expected_output": "Corrects the query granularity/range mismatch and guards grid cleanliness insight access for iOS/iPadOS 26.1+.",
"files": [],
"assertions": [
"States that `dataByGridCleanliness` / grid cleanliness breakdowns require iOS/iPadOS 26.1+ and should be guarded or omitted on 26.0.",
"Does not claim all EnergyKit insight APIs are 26.1-only.",
"Pairs granularity to the requested range, using `.hourly` for a seven-day or calendar-week view and `.daily` only for at least a calendar month.",
"Mentions that empty `ElectricityInsightQuery.Options` returns totals without cleanliness or tariff breakdown."
]
},
{
"id": 3,
"name": "energykit-boundary-review",
"prompt": "A teammate wrote: 'EnergyKit works anywhere, no entitlement is needed, and if there are no insights we can infer usage from MetricKit power metrics.' Review this plan for a smart thermostat app and identify the minimal corrections.",
"expected_output": "Identifies EnergyKit entitlement, region/venue limitations, submitted load-event dependency for insights, HVAC stage-change event guidance, and avoids MetricKit substitution.",
"files": [],
"assertions": [
"Requires the `com.apple.developer.energykit` entitlement and handles `EnergyKitError.permissionDenied` rather than saying no entitlement is needed.",
"Handles unsupported regions and unavailable or restricted venues instead of claiming EnergyKit works anywhere.",
"States that EnergyKit insights depend on submitted EnergyKit load events and cannot be replaced by MetricKit power metrics.",
"For HVAC, recommends separate load events for heating/cooling stage transitions rather than only fixed timer samples or a final summary."
]
}
]
}
EnergyKit Extended Patterns
Overflow reference for the energykit skill. Contains advanced patterns that exceed the main skill file's scope.
Contents
- Full App Architecture
- EV Charging Session Manager
- HVAC Control Manager
- SwiftUI Energy Dashboard
- Insight Data Visualization
- Error Handling Strategies
- Venue Discovery Flow
Full App Architecture
An @Observable manager that ties together guidance, venues, and load events.
import EnergyKit
import SwiftUI
@Observable
@MainActor
final class EnergyManager {
var venues: [EnergyVenue] = []
var selectedVenue: EnergyVenue?
var currentGuidance: ElectricityGuidance?
var guidanceValues: [ElectricityGuidance.Value] = []
var isLoading = false
var errorMessage: String?
private var guidanceTask: Task<Void, Never>?
func loadVenues() async {
isLoading = true
errorMessage = nil
do {
venues = try await EnergyVenue.venues()
selectedVenue = venues.first
if let venue = selectedVenue {
startObservingGuidance(for: venue.id)
}
} catch let error as EnergyKitError {
errorMessage = handleError(error)
} catch {
errorMessage = error.localizedDescription
}
isLoading = false
}
func startObservingGuidance(for venueID: UUID) {
guidanceTask?.cancel()
guidanceTask = Task { [weak self] in
let query = ElectricityGuidance.Query(suggestedAction: .shift)
let service = ElectricityGuidance.sharedService
do {
for try await guidance in service.guidance(using: query, at: venueID) {
self?.currentGuidance = guidance
self?.guidanceValues = guidance.values
}
} catch {
self?.errorMessage = error.localizedDescription
}
}
}
func stopObserving() {
guidanceTask?.cancel()
guidanceTask = nil
}
var bestWindow: ElectricityGuidance.Value? {
guidanceValues.min(by: { $0.rating < $1.rating })
}
var hasRatePlan: Bool {
currentGuidance?.options.contains(.locationHasRatePlan) ?? false
}
var usesRatePlan: Bool {
currentGuidance?.options.contains(.guidanceIncorporatesRatePlan) ?? false
}
// EnergyKitError cases
private func handleError(_ error: EnergyKitError) -> String {
switch error {
case .unsupportedRegion:
return "Energy guidance is not available in your region."
case .guidanceUnavailable:
return "Grid guidance data is currently unavailable."
case .venueUnavailable:
return "No energy venue found. Set up your home in the Home app."
case .permissionDenied:
return "Permission to access energy data was denied."
case .serviceUnavailable:
return "The energy service is temporarily unavailable."
case .rateLimitExceeded:
return "Too many requests. Please try again later."
case .invalidLoadEvent:
return "The load event data was invalid."
case .inProgress:
return "A request is already in progress."
case .locationServicesDenied:
return "Location services are required for energy guidance."
@unknown default:
return "An unknown error occurred."
}
}
}EV Charging Session Manager
Manage the full lifecycle of an EV charging session with guidance tracking. Use only a guidanceToken returned by EnergyKit for the venue and device that requested guidance. Do not synthesize placeholder tokens.
import EnergyKit
@Observable
@MainActor
final class EVChargingManager {
var isCharging = false
var currentSessionID: UUID?
var stateOfCharge: Int = 0
var currentPower: Double = 0 // kW
var totalEnergy: Double = 0 // kWh
private let deviceID: String
private var venue: EnergyVenue?
private var guidanceToken: UUID?
private var pendingEvents: [ElectricVehicleLoadEvent] = []
init(deviceID: String) {
self.deviceID = deviceID
}
func setVenue(_ venue: EnergyVenue) {
self.venue = venue
}
func setGuidanceToken(_ token: UUID) {
self.guidanceToken = token
}
func startCharging(stateOfCharge: Int) async throws {
guard let venue else { throw EVError.noVenue }
let sessionID = UUID()
currentSessionID = sessionID
self.stateOfCharge = stateOfCharge
isCharging = true
let event = try makeEvent(
sessionState: .begin,
stateOfCharge: stateOfCharge,
power: 0,
energy: 0
)
pendingEvents = [event]
}
func updateCharging(
stateOfCharge: Int,
power: Double,
energy: Double
) async throws {
guard let venue, isCharging else { return }
self.stateOfCharge = stateOfCharge
self.currentPower = power
self.totalEnergy = energy
let event = try makeEvent(
sessionState: .active,
stateOfCharge: stateOfCharge,
power: power,
energy: energy
)
pendingEvents.append(event)
if pendingEvents.count >= 4 {
try await flushPendingEvents()
}
}
func stopCharging() async throws {
guard let venue, isCharging else { return }
let event = try makeEvent(
sessionState: .end,
stateOfCharge: stateOfCharge,
power: 0,
energy: totalEnergy
)
pendingEvents.append(event)
try await flushPendingEvents()
isCharging = false
currentSessionID = nil
}
func flushPendingEvents() async throws {
guard let venue, !pendingEvents.isEmpty else { return }
let events = pendingEvents
pendingEvents.removeAll()
try await venue.submitEvents(events)
}
private func makeEvent(
sessionState: ElectricVehicleLoadEvent.Session.State,
stateOfCharge: Int,
power: Double,
energy: Double
) throws -> ElectricVehicleLoadEvent {
guard let guidanceToken else { throw EVError.missingGuidanceToken }
guard let currentSessionID else { throw EVError.noActiveSession }
let guidanceState = ElectricVehicleLoadEvent.Session.GuidanceState(
wasFollowingGuidance: true,
guidanceToken: guidanceToken
)
let session = ElectricVehicleLoadEvent.Session(
id: currentSessionID,
state: sessionState,
guidanceState: guidanceState
)
let measurement = ElectricVehicleLoadEvent.ElectricalMeasurement(
stateOfCharge: stateOfCharge,
direction: .imported,
power: Measurement(value: power, unit: .kilowatts),
energy: Measurement(value: energy, unit: .kilowattHours)
)
return ElectricVehicleLoadEvent(
timestamp: Date(),
measurement: measurement,
session: session,
deviceID: deviceID
)
}
enum EVError: Error {
case noVenue
case missingGuidanceToken
case noActiveSession
}
}HVAC Control Manager
Track HVAC load events with guidance compliance. Submit events when the heating or cooling stage changes, and use the real guidance token that was in effect for that venue. Treat heat stage 1 -> heat stage 2, heat -> cooling, cooling -> idle, and equipment stop as distinct load events instead of one summarized session row. For devices that emit frequent stage/runtime samples, buffer events and submit periodically or at the end of a session, while still recording significant state changes as they occur.
import EnergyKit
@Observable
@MainActor
final class HVACManager {
var isRunning = false
var currentStage: Int = 0
var sessionID: UUID?
private let deviceID: String
private var venue: EnergyVenue?
private var guidanceToken: UUID?
init(deviceID: String) {
self.deviceID = deviceID
}
func configure(venue: EnergyVenue, guidanceToken: UUID) {
self.venue = venue
self.guidanceToken = guidanceToken
}
func start(stage: Int) async throws {
guard let venue else { return }
sessionID = UUID()
currentStage = stage
isRunning = true
let event = try makeEvent(state: .begin, stage: stage)
try await venue.submitEvents([event])
}
func updateStage(_ stage: Int) async throws {
guard let venue, isRunning else { return }
currentStage = stage
let event = try makeEvent(state: .active, stage: stage)
try await venue.submitEvents([event])
}
func stop() async throws {
guard let venue, isRunning else { return }
let event = try makeEvent(state: .end, stage: 0)
try await venue.submitEvents([event])
isRunning = false
sessionID = nil
}
private func makeEvent(
state: ElectricHVACLoadEvent.Session.State,
stage: Int
) throws -> ElectricHVACLoadEvent {
guard let guidanceToken else { throw HVACError.missingGuidanceToken }
guard let sessionID else { throw HVACError.noActiveSession }
let guidanceState = ElectricHVACLoadEvent.Session.GuidanceState(
wasFollowingGuidance: true,
guidanceToken: guidanceToken
)
let session = ElectricHVACLoadEvent.Session(
id: sessionID,
state: state,
guidanceState: guidanceState
)
let measurement = ElectricHVACLoadEvent.ElectricalMeasurement(stage: stage)
return ElectricHVACLoadEvent(
timestamp: Date(),
measurement: measurement,
session: session,
deviceID: deviceID
)
}
enum HVACError: Error {
case missingGuidanceToken
case noActiveSession
}
}SwiftUI Energy Dashboard
A complete dashboard view showing guidance and insights.
import SwiftUI
import EnergyKit
struct EnergyDashboardView: View {
@Environment(EnergyManager.self) private var energyManager
var body: some View {
NavigationStack {
Group {
if energyManager.isLoading {
ProgressView("Loading energy data...")
} else if let error = energyManager.errorMessage {
ContentUnavailableView(
"Energy Guidance Unavailable",
systemImage: "bolt.slash",
description: Text(error)
)
} else {
dashboardContent
}
}
.navigationTitle("Energy")
.task {
await energyManager.loadVenues()
}
}
}
private var dashboardContent: some View {
List {
if let venue = energyManager.selectedVenue {
Section("Venue") {
LabeledContent("Name", value: venue.name)
}
}
if let best = energyManager.bestWindow {
Section("Best Time") {
VStack(alignment: .leading) {
Text("Optimal usage window")
.font(.headline)
Text(best.interval.start, style: .time)
+ Text(" - ")
+ Text(best.interval.end, style: .time)
Text("Rating: \(best.rating, specifier: "%.2f")")
.foregroundStyle(.secondary)
}
}
}
if !energyManager.guidanceValues.isEmpty {
Section("Timeline") {
ForEach(energyManager.guidanceValues, id: \.interval.start) { value in
HStack {
VStack(alignment: .leading) {
Text(value.interval.start, style: .time)
Text(value.interval.end, style: .time)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
guidanceRatingView(value.rating)
}
}
}
}
if energyManager.hasRatePlan {
Section("Rate Plan") {
Label(
energyManager.usesRatePlan
? "Guidance incorporates your rate plan"
: "Rate plan available but not yet incorporated",
systemImage: "dollarsign.circle"
)
}
}
}
}
private func guidanceRatingView(_ rating: Double) -> some View {
let color: Color = rating <= 0.3 ? .green : rating <= 0.6 ? .yellow : .red
let label = rating <= 0.3 ? "Good" : rating <= 0.6 ? "Fair" : "Avoid"
return Text(label)
.font(.caption)
.fontWeight(.medium)
.padding(.horizontal)
.padding(.vertical)
.background(color.opacity(0.15))
.foregroundStyle(color)
.clipShape(Capsule())
}
}Insight Data Visualization
Prepare insight records for chart display. dataByGridCleanliness is iOS/iPadOS 26.1+, so keep the chart model optional and guard access when supporting 26.0.
struct InsightDataPoint: Identifiable {
let id = UUID()
let date: Date
let energy: Double // kWh
let cleanerEnergy: Double?
let lessCleanEnergy: Double?
}
func processInsightRecords(
_ records: [ElectricityInsightRecord<Measurement<UnitEnergy>>]
) -> [InsightDataPoint] {
records.compactMap { record in
guard let total = record.totalEnergy else { return nil }
let cleanliness: (cleaner: Double?, lessClean: Double?)
if #available(iOS 26.1, iPadOS 26.1, *) {
cleanliness = (
record.dataByGridCleanliness?.cleaner?
.converted(to: .kilowattHours).value,
record.dataByGridCleanliness?.lessClean?
.converted(to: .kilowattHours).value
)
} else {
cleanliness = (nil, nil)
}
return InsightDataPoint(
date: record.range.start,
energy: total.converted(to: .kilowattHours).value,
cleanerEnergy: cleanliness.cleaner,
lessCleanEnergy: cleanliness.lessClean
)
}
}Error Handling Strategies
Comprehensive error handling with retry logic.
@Observable
@MainActor
final class ResilientEnergyService {
private let maxRetries = 3
func fetchGuidanceWithRetry(venueID: UUID) async throws -> ElectricityGuidance? {
let query = ElectricityGuidance.Query(suggestedAction: .shift)
let service = ElectricityGuidance.sharedService
for attempt in 0..<maxRetries {
do {
for try await guidance in service.guidance(using: query, at: venueID) {
return guidance
}
} catch let error as EnergyKitError {
switch error {
case .serviceUnavailable, .rateLimitExceeded:
if attempt < maxRetries - 1 {
try await Task.sleep(for: .seconds(pow(2.0, Double(attempt + 1))))
}
case .unsupportedRegion, .permissionDenied, .venueUnavailable:
throw error // Do not retry permanent failures
default:
throw error
}
}
}
return nil
}
}Venue Discovery Flow
Guide users through venue setup if none exist.
import SwiftUI
import EnergyKit
struct VenueSetupView: View {
@State private var venues: [EnergyVenue] = []
@State private var isLoading = true
@State private var showSetupGuide = false
var body: some View {
Group {
if isLoading {
ProgressView()
} else if venues.isEmpty {
ContentUnavailableView {
Label("No Energy Venues", systemImage: "house")
} description: {
Text("Set up your home in the Home app to use energy guidance.")
} actions: {
Button("Learn More") { showSetupGuide = true }
.buttonStyle(.borderedProminent)
}
} else {
List(venues, id: \.id) { venue in
NavigationLink(value: venue.id) {
Label(venue.name, systemImage: "house.fill")
}
}
}
}
.task {
do {
venues = try await EnergyVenue.venues()
} catch {
venues = []
}
isLoading = false
}
}
}Related skills
How it compares
Use energykit specifically for Apple EnergyKit EV load events, not for general HomeKit or background execution patterns.
FAQ
What entitlement is required?
Enable the EnergyKit capability so com.apple.developer.energykit is on the app target.
Shift or reduce for EV charging?
Use shift suggestedAction to move consumption to better-rated intervals.
Who submits load events?
The same device and app that requested guidance must submit telemetry.
Is Energykit safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.