@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,583 +1,287 @@
|
|
|
1
|
-
# EnergyKit
|
|
1
|
+
# EnergyKit Patterns
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Larger building blocks for an EnergyKit app. Basics live in the
|
|
4
|
+
[skill overview](../SKILL.md). The API is new in iOS 26; confirm names against
|
|
5
|
+
current documentation. The code assumes a 26.1 deployment target, because venue
|
|
6
|
+
lookup and some error cases are 26.1 APIs.
|
|
5
7
|
|
|
6
|
-
##
|
|
7
|
-
|
|
8
|
-
- [Full App Architecture](#full-app-architecture)
|
|
9
|
-
- [EV Charging Session Manager](#ev-charging-session-manager)
|
|
10
|
-
- [HVAC Control Manager](#hvac-control-manager)
|
|
11
|
-
- [SwiftUI Energy Dashboard](#swiftui-energy-dashboard)
|
|
12
|
-
- [Insight Data Visualization](#insight-data-visualization)
|
|
13
|
-
- [Error Handling Strategies](#error-handling-strategies)
|
|
14
|
-
- [Venue Discovery Flow](#venue-discovery-flow)
|
|
15
|
-
|
|
16
|
-
## Full App Architecture
|
|
17
|
-
|
|
18
|
-
An `@Observable` manager that ties together guidance, venues, and load events.
|
|
8
|
+
## Observable guidance manager
|
|
19
9
|
|
|
20
10
|
```swift
|
|
21
11
|
import EnergyKit
|
|
22
|
-
import
|
|
12
|
+
import Observation
|
|
23
13
|
|
|
24
|
-
@Observable
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
var venues: [EnergyVenue] = []
|
|
14
|
+
@Observable @MainActor
|
|
15
|
+
final class GridForecastModel {
|
|
16
|
+
private(set) var venues: [EnergyVenue] = []
|
|
28
17
|
var selectedVenue: EnergyVenue?
|
|
29
|
-
var
|
|
30
|
-
var
|
|
31
|
-
var isLoading = false
|
|
32
|
-
var
|
|
18
|
+
private(set) var latest: ElectricityGuidance?
|
|
19
|
+
private(set) var slots: [ElectricityGuidance.Value] = []
|
|
20
|
+
private(set) var isLoading = false
|
|
21
|
+
private(set) var problem: String?
|
|
33
22
|
|
|
34
|
-
private var
|
|
23
|
+
private var watcher: Task<Void, Never>?
|
|
35
24
|
|
|
36
25
|
func loadVenues() async {
|
|
37
26
|
isLoading = true
|
|
38
|
-
|
|
39
|
-
|
|
27
|
+
defer { isLoading = false }
|
|
40
28
|
do {
|
|
41
29
|
venues = try await EnergyVenue.venues()
|
|
42
30
|
selectedVenue = venues.first
|
|
43
|
-
if let
|
|
44
|
-
startObservingGuidance(for: venue.id)
|
|
45
|
-
}
|
|
46
|
-
} catch let error as EnergyKitError {
|
|
47
|
-
errorMessage = handleError(error)
|
|
31
|
+
if let first = venues.first { startWatching(first.id) }
|
|
48
32
|
} catch {
|
|
49
|
-
|
|
33
|
+
problem = explain(error)
|
|
50
34
|
}
|
|
51
|
-
|
|
52
|
-
isLoading = false
|
|
53
35
|
}
|
|
54
36
|
|
|
55
|
-
func
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let query = ElectricityGuidance.Query(suggestedAction:
|
|
59
|
-
let service = ElectricityGuidance.sharedService
|
|
60
|
-
|
|
37
|
+
func startWatching(_ venueID: UUID, action: ElectricityGuidance.SuggestedAction = .shift) {
|
|
38
|
+
watcher?.cancel()
|
|
39
|
+
watcher = Task { [weak self] in
|
|
40
|
+
let query = ElectricityGuidance.Query(suggestedAction: action)
|
|
61
41
|
do {
|
|
62
|
-
for try await
|
|
63
|
-
self
|
|
64
|
-
self
|
|
42
|
+
for try await forecast in ElectricityGuidance.sharedService.guidance(using: query, at: venueID) {
|
|
43
|
+
guard let self else { return }
|
|
44
|
+
self.latest = forecast
|
|
45
|
+
self.slots = forecast.values
|
|
65
46
|
}
|
|
66
47
|
} catch {
|
|
67
|
-
self?.
|
|
48
|
+
self?.problem = self?.explain(error)
|
|
68
49
|
}
|
|
69
50
|
}
|
|
70
51
|
}
|
|
71
52
|
|
|
72
|
-
func
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
var bestWindow: ElectricityGuidance.Value? {
|
|
78
|
-
guidanceValues.min(by: { $0.rating < $1.rating })
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
var hasRatePlan: Bool {
|
|
82
|
-
currentGuidance?.options.contains(.locationHasRatePlan) ?? false
|
|
53
|
+
func stopWatching() {
|
|
54
|
+
watcher?.cancel()
|
|
55
|
+
watcher = nil
|
|
83
56
|
}
|
|
84
57
|
|
|
85
|
-
var
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
switch
|
|
92
|
-
case .unsupportedRegion:
|
|
93
|
-
|
|
94
|
-
case .
|
|
95
|
-
|
|
96
|
-
case .
|
|
97
|
-
|
|
98
|
-
case .
|
|
99
|
-
|
|
100
|
-
case .
|
|
101
|
-
|
|
102
|
-
case .rateLimitExceeded:
|
|
103
|
-
return "Too many requests. Please try again later."
|
|
104
|
-
case .invalidLoadEvent:
|
|
105
|
-
return "The load event data was invalid."
|
|
106
|
-
case .inProgress:
|
|
107
|
-
return "A request is already in progress."
|
|
108
|
-
case .locationServicesDenied:
|
|
109
|
-
return "Location services are required for energy guidance."
|
|
110
|
-
@unknown default:
|
|
111
|
-
return "An unknown error occurred."
|
|
58
|
+
var cleanestSlot: ElectricityGuidance.Value? { slots.min { $0.rating < $1.rating } }
|
|
59
|
+
var hasRatePlan: Bool { latest?.options.contains(.locationHasRatePlan) ?? false }
|
|
60
|
+
var usesRatePlan: Bool { latest?.options.contains(.guidanceIncorporatesRatePlan) ?? false }
|
|
61
|
+
|
|
62
|
+
private func explain(_ error: Error) -> String {
|
|
63
|
+
guard let energy = error as? EnergyKitError else { return error.localizedDescription }
|
|
64
|
+
switch energy {
|
|
65
|
+
case .unsupportedRegion: return "Forecasts are not offered in this region."
|
|
66
|
+
case .guidanceUnavailable: return "No forecast is available right now."
|
|
67
|
+
case .venueUnavailable: return "Add your home in the Home app to get forecasts."
|
|
68
|
+
case .permissionDenied: return "The app is not allowed to use EnergyKit."
|
|
69
|
+
case .serviceUnavailable: return "The service is down. Try again soon."
|
|
70
|
+
case .rateLimitExceeded: return "Too many requests. Waiting before trying again."
|
|
71
|
+
case .invalidLoadEvent: return "A load event was rejected."
|
|
72
|
+
case .inProgress: return "A request is already running."
|
|
73
|
+
case .locationServicesDenied: return "Location access is needed for forecasts."
|
|
74
|
+
@unknown default: return "Unexpected EnergyKit error."
|
|
112
75
|
}
|
|
113
76
|
}
|
|
114
77
|
}
|
|
115
78
|
```
|
|
116
79
|
|
|
117
|
-
## EV
|
|
80
|
+
## EV charging sessions
|
|
118
81
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
requested guidance. Do not synthesize placeholder tokens.
|
|
82
|
+
Only use a token EnergyKit returned for this venue and the device that asked
|
|
83
|
+
for guidance. Never synthesize one.
|
|
122
84
|
|
|
123
85
|
```swift
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
var
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
var totalEnergy: Double = 0 // kWh
|
|
134
|
-
|
|
135
|
-
private let deviceID: String
|
|
136
|
-
private var venue: EnergyVenue?
|
|
137
|
-
private var guidanceToken: UUID?
|
|
138
|
-
private var pendingEvents: [ElectricVehicleLoadEvent] = []
|
|
139
|
-
|
|
140
|
-
init(deviceID: String) {
|
|
141
|
-
self.deviceID = deviceID
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
func setVenue(_ venue: EnergyVenue) {
|
|
86
|
+
actor ChargeSessionRecorder {
|
|
87
|
+
private let chargerID: String
|
|
88
|
+
private let venue: EnergyVenue
|
|
89
|
+
private var token: UUID?
|
|
90
|
+
private var sessionID: UUID?
|
|
91
|
+
private var queued: [ElectricVehicleLoadEvent] = []
|
|
92
|
+
|
|
93
|
+
init(chargerID: String, venue: EnergyVenue) {
|
|
94
|
+
self.chargerID = chargerID
|
|
145
95
|
self.venue = venue
|
|
146
96
|
}
|
|
147
97
|
|
|
148
|
-
func
|
|
149
|
-
self.guidanceToken = token
|
|
150
|
-
}
|
|
98
|
+
func adopt(token: UUID) { self.token = token } // from ElectricityGuidance.guidanceToken
|
|
151
99
|
|
|
152
|
-
func
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
currentSessionID = sessionID
|
|
157
|
-
self.stateOfCharge = stateOfCharge
|
|
158
|
-
isCharging = true
|
|
159
|
-
|
|
160
|
-
let event = try makeEvent(
|
|
161
|
-
sessionState: .begin,
|
|
162
|
-
stateOfCharge: stateOfCharge,
|
|
163
|
-
power: 0,
|
|
164
|
-
energy: 0
|
|
165
|
-
)
|
|
166
|
-
pendingEvents = [event]
|
|
100
|
+
func plugIn(percent: Int) async throws {
|
|
101
|
+
sessionID = UUID()
|
|
102
|
+
queued.append(try event(.begin, kilowatts: 0, kilowattHours: 0, percent: percent))
|
|
103
|
+
try await flush()
|
|
167
104
|
}
|
|
168
105
|
|
|
169
|
-
func
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
energy: Double
|
|
173
|
-
) async throws {
|
|
174
|
-
guard let venue, isCharging else { return }
|
|
175
|
-
|
|
176
|
-
self.stateOfCharge = stateOfCharge
|
|
177
|
-
self.currentPower = power
|
|
178
|
-
self.totalEnergy = energy
|
|
179
|
-
|
|
180
|
-
let event = try makeEvent(
|
|
181
|
-
sessionState: .active,
|
|
182
|
-
stateOfCharge: stateOfCharge,
|
|
183
|
-
power: power,
|
|
184
|
-
energy: energy
|
|
185
|
-
)
|
|
186
|
-
pendingEvents.append(event)
|
|
187
|
-
|
|
188
|
-
if pendingEvents.count >= 4 {
|
|
189
|
-
try await flushPendingEvents()
|
|
190
|
-
}
|
|
106
|
+
func sample(kilowatts: Double, kilowattHours: Double, percent: Int) async throws {
|
|
107
|
+
queued.append(try event(.active, kilowatts: kilowatts, kilowattHours: kilowattHours, percent: percent))
|
|
108
|
+
if queued.count >= 4 { try await flush() }
|
|
191
109
|
}
|
|
192
110
|
|
|
193
|
-
func
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
sessionState: .end,
|
|
198
|
-
stateOfCharge: stateOfCharge,
|
|
199
|
-
power: 0,
|
|
200
|
-
energy: totalEnergy
|
|
201
|
-
)
|
|
202
|
-
pendingEvents.append(event)
|
|
203
|
-
try await flushPendingEvents()
|
|
204
|
-
|
|
205
|
-
isCharging = false
|
|
206
|
-
currentSessionID = nil
|
|
111
|
+
func unplug(kilowattHours: Double, percent: Int) async throws {
|
|
112
|
+
queued.append(try event(.end, kilowatts: 0, kilowattHours: kilowattHours, percent: percent))
|
|
113
|
+
try await flush()
|
|
114
|
+
sessionID = nil
|
|
207
115
|
}
|
|
208
116
|
|
|
209
|
-
func
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
try await venue.submitEvents(events)
|
|
117
|
+
private func flush() async throws {
|
|
118
|
+
let outgoing = queued
|
|
119
|
+
queued.removeAll()
|
|
120
|
+
guard !outgoing.isEmpty else { return }
|
|
121
|
+
try await venue.submitEvents(outgoing)
|
|
215
122
|
}
|
|
216
123
|
|
|
217
|
-
private func
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
power: Double,
|
|
221
|
-
energy: Double
|
|
222
|
-
) throws -> ElectricVehicleLoadEvent {
|
|
223
|
-
guard let guidanceToken else { throw EVError.missingGuidanceToken }
|
|
224
|
-
guard let currentSessionID else { throw EVError.noActiveSession }
|
|
225
|
-
|
|
226
|
-
let guidanceState = ElectricVehicleLoadEvent.Session.GuidanceState(
|
|
227
|
-
wasFollowingGuidance: true,
|
|
228
|
-
guidanceToken: guidanceToken
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
let session = ElectricVehicleLoadEvent.Session(
|
|
232
|
-
id: currentSessionID,
|
|
233
|
-
state: sessionState,
|
|
234
|
-
guidanceState: guidanceState
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
let measurement = ElectricVehicleLoadEvent.ElectricalMeasurement(
|
|
238
|
-
stateOfCharge: stateOfCharge,
|
|
239
|
-
direction: .imported,
|
|
240
|
-
power: Measurement(value: power, unit: .kilowatts),
|
|
241
|
-
energy: Measurement(value: energy, unit: .kilowattHours)
|
|
242
|
-
)
|
|
243
|
-
|
|
124
|
+
private func event(_ state: ElectricVehicleLoadEvent.Session.State,
|
|
125
|
+
kilowatts: Double, kilowattHours: Double, percent: Int) throws -> ElectricVehicleLoadEvent {
|
|
126
|
+
guard let token, let sessionID else { throw ChargeError.noGuidanceOrSession }
|
|
244
127
|
return ElectricVehicleLoadEvent(
|
|
245
|
-
timestamp:
|
|
246
|
-
measurement:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
enum EVError: Error {
|
|
253
|
-
case noVenue
|
|
254
|
-
case missingGuidanceToken
|
|
255
|
-
case noActiveSession
|
|
128
|
+
timestamp: .now,
|
|
129
|
+
measurement: .init(stateOfCharge: percent, direction: .imported,
|
|
130
|
+
power: Measurement(value: kilowatts, unit: .kilowatts),
|
|
131
|
+
energy: Measurement(value: kilowattHours, unit: .kilowattHours)),
|
|
132
|
+
session: .init(id: sessionID, state: state,
|
|
133
|
+
guidanceState: .init(wasFollowingGuidance: true, guidanceToken: token)),
|
|
134
|
+
deviceID: chargerID)
|
|
256
135
|
}
|
|
257
136
|
}
|
|
137
|
+
|
|
138
|
+
enum ChargeError: Error { case noGuidanceOrSession }
|
|
258
139
|
```
|
|
259
140
|
|
|
260
|
-
## HVAC
|
|
141
|
+
## HVAC stages
|
|
261
142
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
For devices that emit frequent stage/runtime samples, buffer events and submit
|
|
268
|
-
periodically or at the end of a session, while still recording significant state
|
|
269
|
-
changes as they occur.
|
|
143
|
+
Report each heating or cooling stage change with the real token in force for
|
|
144
|
+
the venue. Stage 1 to stage 2 heat, heat to cooling, cooling to idle, and
|
|
145
|
+
shutting down are four separate events, not one summary row. If the equipment
|
|
146
|
+
reports very frequently, buffer and submit on a timer or at session end, but
|
|
147
|
+
still capture every significant state change when it happens.
|
|
270
148
|
|
|
271
149
|
```swift
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
private let deviceID: String
|
|
282
|
-
private var venue: EnergyVenue?
|
|
283
|
-
private var guidanceToken: UUID?
|
|
284
|
-
|
|
285
|
-
init(deviceID: String) {
|
|
286
|
-
self.deviceID = deviceID
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
func configure(venue: EnergyVenue, guidanceToken: UUID) {
|
|
150
|
+
actor ClimateRecorder {
|
|
151
|
+
private let thermostatID: String
|
|
152
|
+
private let venue: EnergyVenue
|
|
153
|
+
private let token: UUID
|
|
154
|
+
private var runID = UUID()
|
|
155
|
+
|
|
156
|
+
init(thermostatID: String, venue: EnergyVenue, token: UUID) {
|
|
157
|
+
self.thermostatID = thermostatID
|
|
290
158
|
self.venue = venue
|
|
291
|
-
self.
|
|
159
|
+
self.token = token
|
|
292
160
|
}
|
|
293
161
|
|
|
294
162
|
func start(stage: Int) async throws {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
currentStage = stage
|
|
298
|
-
isRunning = true
|
|
299
|
-
|
|
300
|
-
let event = try makeEvent(state: .begin, stage: stage)
|
|
301
|
-
try await venue.submitEvents([event])
|
|
163
|
+
runID = UUID()
|
|
164
|
+
try await send(.begin, stage: stage)
|
|
302
165
|
}
|
|
303
166
|
|
|
304
|
-
func
|
|
305
|
-
guard let venue, isRunning else { return }
|
|
306
|
-
currentStage = stage
|
|
167
|
+
func changeStage(to stage: Int) async throws { try await send(.active, stage: stage) }
|
|
307
168
|
|
|
308
|
-
|
|
309
|
-
try await venue.submitEvents([event])
|
|
310
|
-
}
|
|
169
|
+
func stop() async throws { try await send(.end, stage: 0) }
|
|
311
170
|
|
|
312
|
-
func
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
171
|
+
private func send(_ state: ElectricHVACLoadEvent.Session.State, stage: Int) async throws {
|
|
172
|
+
let event = ElectricHVACLoadEvent(
|
|
173
|
+
timestamp: .now,
|
|
174
|
+
measurement: .init(stage: stage),
|
|
175
|
+
session: .init(id: runID, state: state,
|
|
176
|
+
guidanceState: .init(wasFollowingGuidance: true, guidanceToken: token)),
|
|
177
|
+
deviceID: thermostatID)
|
|
316
178
|
try await venue.submitEvents([event])
|
|
317
|
-
|
|
318
|
-
isRunning = false
|
|
319
|
-
sessionID = nil
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private func makeEvent(
|
|
323
|
-
state: ElectricHVACLoadEvent.Session.State,
|
|
324
|
-
stage: Int
|
|
325
|
-
) throws -> ElectricHVACLoadEvent {
|
|
326
|
-
guard let guidanceToken else { throw HVACError.missingGuidanceToken }
|
|
327
|
-
guard let sessionID else { throw HVACError.noActiveSession }
|
|
328
|
-
|
|
329
|
-
let guidanceState = ElectricHVACLoadEvent.Session.GuidanceState(
|
|
330
|
-
wasFollowingGuidance: true,
|
|
331
|
-
guidanceToken: guidanceToken
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
let session = ElectricHVACLoadEvent.Session(
|
|
335
|
-
id: sessionID,
|
|
336
|
-
state: state,
|
|
337
|
-
guidanceState: guidanceState
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
let measurement = ElectricHVACLoadEvent.ElectricalMeasurement(stage: stage)
|
|
341
|
-
|
|
342
|
-
return ElectricHVACLoadEvent(
|
|
343
|
-
timestamp: Date(),
|
|
344
|
-
measurement: measurement,
|
|
345
|
-
session: session,
|
|
346
|
-
deviceID: deviceID
|
|
347
|
-
)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
enum HVACError: Error {
|
|
351
|
-
case missingGuidanceToken
|
|
352
|
-
case noActiveSession
|
|
353
179
|
}
|
|
354
180
|
}
|
|
355
181
|
```
|
|
356
182
|
|
|
357
|
-
##
|
|
358
|
-
|
|
359
|
-
A complete dashboard view showing guidance and insights.
|
|
183
|
+
## Dashboard view
|
|
360
184
|
|
|
361
185
|
```swift
|
|
362
186
|
import SwiftUI
|
|
363
|
-
import EnergyKit
|
|
364
187
|
|
|
365
|
-
struct
|
|
366
|
-
@Environment(
|
|
188
|
+
struct GridDashboard: View {
|
|
189
|
+
@Environment(GridForecastModel.self) private var model
|
|
367
190
|
|
|
368
191
|
var body: some View {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
)
|
|
379
|
-
} else {
|
|
380
|
-
dashboardContent
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
.navigationTitle("Energy")
|
|
384
|
-
.task {
|
|
385
|
-
await energyManager.loadVenues()
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
private var dashboardContent: some View {
|
|
391
|
-
List {
|
|
392
|
-
if let venue = energyManager.selectedVenue {
|
|
393
|
-
Section("Venue") {
|
|
394
|
-
LabeledContent("Name", value: venue.name)
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if let best = energyManager.bestWindow {
|
|
399
|
-
Section("Best Time") {
|
|
400
|
-
VStack(alignment: .leading) {
|
|
401
|
-
Text("Optimal usage window")
|
|
402
|
-
.font(.headline)
|
|
403
|
-
Text(best.interval.start, style: .time)
|
|
404
|
-
+ Text(" - ")
|
|
405
|
-
+ Text(best.interval.end, style: .time)
|
|
406
|
-
Text("Rating: \(best.rating, specifier: "%.2f")")
|
|
407
|
-
.foregroundStyle(.secondary)
|
|
192
|
+
Group {
|
|
193
|
+
if model.isLoading {
|
|
194
|
+
ProgressView("Loading forecast")
|
|
195
|
+
} else if let problem = model.problem {
|
|
196
|
+
ContentUnavailableView("No Forecast", systemImage: "bolt.slash", description: Text(problem))
|
|
197
|
+
} else {
|
|
198
|
+
List {
|
|
199
|
+
if let venue = model.selectedVenue {
|
|
200
|
+
Section("Home") { Text(venue.name) }
|
|
408
201
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
Section("Timeline") {
|
|
414
|
-
ForEach(energyManager.guidanceValues, id: \.interval.start) { value in
|
|
415
|
-
HStack {
|
|
416
|
-
VStack(alignment: .leading) {
|
|
417
|
-
Text(value.interval.start, style: .time)
|
|
418
|
-
Text(value.interval.end, style: .time)
|
|
419
|
-
.font(.caption)
|
|
420
|
-
.foregroundStyle(.secondary)
|
|
421
|
-
}
|
|
422
|
-
Spacer()
|
|
423
|
-
guidanceRatingView(value.rating)
|
|
202
|
+
if let best = model.cleanestSlot {
|
|
203
|
+
Section("Best time") {
|
|
204
|
+
Text(best.interval.start, style: .time)
|
|
205
|
+
Text("Rating \(best.rating, format: .number.precision(.fractionLength(2)))")
|
|
424
206
|
}
|
|
425
207
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
Section("Rate Plan") {
|
|
431
|
-
Label(
|
|
432
|
-
energyManager.usesRatePlan
|
|
433
|
-
? "Guidance incorporates your rate plan"
|
|
434
|
-
: "Rate plan available but not yet incorporated",
|
|
435
|
-
systemImage: "dollarsign.circle"
|
|
436
|
-
)
|
|
208
|
+
Section("Timeline") { ForecastList(values: model.slots) }
|
|
209
|
+
if model.usesRatePlan {
|
|
210
|
+
Label("Includes your rate plan", systemImage: "dollarsign.circle")
|
|
211
|
+
}
|
|
437
212
|
}
|
|
438
213
|
}
|
|
439
214
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
private func guidanceRatingView(_ rating: Double) -> some View {
|
|
443
|
-
let color: Color = rating <= 0.3 ? .green : rating <= 0.6 ? .yellow : .red
|
|
444
|
-
let label = rating <= 0.3 ? "Good" : rating <= 0.6 ? "Fair" : "Avoid"
|
|
445
|
-
|
|
446
|
-
return Text(label)
|
|
447
|
-
.font(.caption)
|
|
448
|
-
.fontWeight(.medium)
|
|
449
|
-
.padding(.horizontal)
|
|
450
|
-
.padding(.vertical)
|
|
451
|
-
.background(color.opacity(0.15))
|
|
452
|
-
.foregroundStyle(color)
|
|
453
|
-
.clipShape(Capsule())
|
|
215
|
+
.task { await model.loadVenues() }
|
|
454
216
|
}
|
|
455
217
|
}
|
|
456
218
|
```
|
|
457
219
|
|
|
458
|
-
##
|
|
220
|
+
## Preparing insight charts
|
|
459
221
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
and guard access when supporting 26.0.
|
|
222
|
+
`dataByGridCleanliness` needs 26.1. Keep the chart model's cleanliness fields
|
|
223
|
+
optional and guard them when the app still supports 26.0.
|
|
463
224
|
|
|
464
225
|
```swift
|
|
465
|
-
struct
|
|
466
|
-
let id
|
|
467
|
-
let
|
|
468
|
-
let
|
|
469
|
-
let
|
|
470
|
-
let lessCleanEnergy: Double?
|
|
226
|
+
struct UsageBar: Identifiable {
|
|
227
|
+
let id: Date
|
|
228
|
+
let totalKWh: Double
|
|
229
|
+
let cleanerKWh: Double?
|
|
230
|
+
let lessCleanKWh: Double?
|
|
471
231
|
}
|
|
472
232
|
|
|
473
|
-
func
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if #available(iOS 26.1, iPadOS 26.1, *) {
|
|
481
|
-
cleanliness = (
|
|
482
|
-
record.dataByGridCleanliness?.cleaner?
|
|
483
|
-
.converted(to: .kilowattHours).value,
|
|
484
|
-
record.dataByGridCleanliness?.lessClean?
|
|
485
|
-
.converted(to: .kilowattHours).value
|
|
486
|
-
)
|
|
487
|
-
} else {
|
|
488
|
-
cleanliness = (nil, nil)
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return InsightDataPoint(
|
|
492
|
-
date: record.range.start,
|
|
493
|
-
energy: total.converted(to: .kilowattHours).value,
|
|
494
|
-
cleanerEnergy: cleanliness.cleaner,
|
|
495
|
-
lessCleanEnergy: cleanliness.lessClean
|
|
496
|
-
)
|
|
233
|
+
func bar(from record: ElectricityInsightRecord<Measurement<UnitEnergy>>) -> UsageBar {
|
|
234
|
+
let total = record.totalEnergy?.converted(to: .kilowattHours).value ?? 0
|
|
235
|
+
var cleaner: Double?
|
|
236
|
+
var lessClean: Double?
|
|
237
|
+
if #available(iOS 26.1, *) {
|
|
238
|
+
cleaner = record.dataByGridCleanliness?.cleaner?.converted(to: .kilowattHours).value
|
|
239
|
+
lessClean = record.dataByGridCleanliness?.lessClean?.converted(to: .kilowattHours).value
|
|
497
240
|
}
|
|
241
|
+
return UsageBar(id: record.range.start, totalKWh: total, cleanerKWh: cleaner, lessCleanKWh: lessClean)
|
|
498
242
|
}
|
|
499
243
|
```
|
|
500
244
|
|
|
501
|
-
##
|
|
245
|
+
## Retrying
|
|
502
246
|
|
|
503
|
-
|
|
247
|
+
Only transient errors deserve a retry: `.serviceUnavailable` and
|
|
248
|
+
`.rateLimitExceeded`, up to three attempts with a `2^(attempt+1)` second pause.
|
|
249
|
+
`.unsupportedRegion`, `.permissionDenied` and `.venueUnavailable` are permanent
|
|
250
|
+
for this session; surface them instead.
|
|
504
251
|
|
|
505
252
|
```swift
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
for attempt in 0..<maxRetries {
|
|
516
|
-
do {
|
|
517
|
-
for try await guidance in service.guidance(using: query, at: venueID) {
|
|
518
|
-
return guidance
|
|
519
|
-
}
|
|
520
|
-
} catch let error as EnergyKitError {
|
|
521
|
-
switch error {
|
|
522
|
-
case .serviceUnavailable, .rateLimitExceeded:
|
|
523
|
-
if attempt < maxRetries - 1 {
|
|
524
|
-
try await Task.sleep(for: .seconds(pow(2.0, Double(attempt + 1))))
|
|
525
|
-
}
|
|
526
|
-
case .unsupportedRegion, .permissionDenied, .venueUnavailable:
|
|
527
|
-
throw error // Do not retry permanent failures
|
|
528
|
-
default:
|
|
529
|
-
throw error
|
|
530
|
-
}
|
|
531
|
-
}
|
|
253
|
+
func retryingEnergyCall<T>(_ work: () async throws -> T) async throws -> T {
|
|
254
|
+
var attempt = 0
|
|
255
|
+
while true {
|
|
256
|
+
do {
|
|
257
|
+
return try await work()
|
|
258
|
+
} catch let error as EnergyKitError where attempt < 2 &&
|
|
259
|
+
(error == .serviceUnavailable || error == .rateLimitExceeded) {
|
|
260
|
+
attempt += 1
|
|
261
|
+
try await Task.sleep(for: .seconds(1 << attempt))
|
|
532
262
|
}
|
|
533
|
-
|
|
534
|
-
return nil
|
|
535
263
|
}
|
|
536
264
|
}
|
|
537
265
|
```
|
|
538
266
|
|
|
539
|
-
## Venue
|
|
540
|
-
|
|
541
|
-
Guide users through venue setup if none exist.
|
|
267
|
+
## Venue picker
|
|
542
268
|
|
|
543
269
|
```swift
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
struct VenueSetupView: View {
|
|
548
|
-
@State private var venues: [EnergyVenue] = []
|
|
549
|
-
@State private var isLoading = true
|
|
550
|
-
@State private var showSetupGuide = false
|
|
270
|
+
struct HomePicker: View {
|
|
271
|
+
@State private var homes: [EnergyVenue] = []
|
|
551
272
|
|
|
552
273
|
var body: some View {
|
|
553
274
|
Group {
|
|
554
|
-
if
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
ContentUnavailableView {
|
|
558
|
-
Label("No Energy Venues", systemImage: "house")
|
|
559
|
-
} description: {
|
|
560
|
-
Text("Set up your home in the Home app to use energy guidance.")
|
|
561
|
-
} actions: {
|
|
562
|
-
Button("Learn More") { showSetupGuide = true }
|
|
563
|
-
.buttonStyle(.borderedProminent)
|
|
564
|
-
}
|
|
275
|
+
if homes.isEmpty {
|
|
276
|
+
ContentUnavailableView("No Home Found", systemImage: "house",
|
|
277
|
+
description: Text("Create your home in the Home app, then come back."))
|
|
565
278
|
} else {
|
|
566
|
-
List(
|
|
567
|
-
NavigationLink(
|
|
568
|
-
Label(venue.name, systemImage: "house.fill")
|
|
569
|
-
}
|
|
279
|
+
List(homes, id: \.id) { home in
|
|
280
|
+
NavigationLink(home.name) { VenueDetail(venue: home) }
|
|
570
281
|
}
|
|
571
282
|
}
|
|
572
283
|
}
|
|
573
|
-
.task {
|
|
574
|
-
do {
|
|
575
|
-
venues = try await EnergyVenue.venues()
|
|
576
|
-
} catch {
|
|
577
|
-
venues = []
|
|
578
|
-
}
|
|
579
|
-
isLoading = false
|
|
580
|
-
}
|
|
284
|
+
.task { homes = (try? await EnergyVenue.venues()) ?? [] }
|
|
581
285
|
}
|
|
582
286
|
}
|
|
583
287
|
```
|