I am writing a HomeKit app that successfully shows live data from my supported accessories in-app. I can read single values (HMCharacteristic.readValue
) or use notifications to stay updated (HMCharacteristic.enableNotification
).
Now I want to implement Widgets that show this data on the user's Home Screen. This consists of four steps:
- A dynamic Intent fetches all the registered (and supported) Accessories from the
HMHomeManager
and enables the user to select one of them to be shown on the Widget. - Inside the
IntentTimelineProvider
'sgetTimeline
function I can then again use theHMHomeManager
to retrieve the Accessory I want to display on the Widget (based on the Accessory's UUID which is stored inside thegetTimeline
'sconfiguration
parameter - the Intent). - Still inside the
getTimeline
function I can choose the Services and Characteristics I need for displaying the Accessory's Widget from theHMHomeManager
.
Up until here everything works fine.
- However, when I try to read the values from the Characteristics I chose before using
HMCharacteristic.readValue
, the callback contains an error stating
Error Domain=HMErrorDomain Code=80 "Missing entitlement for API."
The Widget's Info.plist contains the 'Privacy - HomeKit Usage Description' field and the Target has the HomeKit capability.
After some research I came up with the following theory: Obviously the whole WidgetKit API runs my code in background. And it seems like HomeKit does not allow access from a background context. Well, it does allow access to Homes/Services/Characteristics, but it does not allow reading or writing on Characteristics (I guess to make sure App developers use HomeKit Automations and don't try to implement custom automations that are controlled by some background process of their app running on the iPhone).
My (simplified) getTimeline
code:
func getTimeline(for configuration: SelectAccessoryIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
// id stores the uuid of the accessory that was chosen by the user using the dynamic Intent
if let id = configuration.accessory?.identifier {
// Step 2.: fetch the accessory
// hm is a HMHomeManager
let hm = HomeStore.shared.homeManager
// take a short nap until the connection to the local HomeKit instance is established (otherwise hm.homes will create an empty array on first call)
sleep(1)
let accessories = hm.homes.flatMap({ h in h.accessories })
if let a = accessories.filter({ a in a.uniqueIdentifier.uuidString == id }).first {
// a holds our HMAccessory
// Step 3.: select the characteristic I want
// obviously the real code chooses a specific characteristic
let s: HMService = a.services.first!
let c: HMCharacteristic = s.characteristics.first!
// Step 4.: read the characteristic's value
c.readValue(completionHandler: {err in
if let error = err {
print(error)
} else {
print(c.value ?? "nil")
}
// complete with timeline
completion(Timeline(entries: [RenderAccessoryEntry(date: Date(), configuration: configuration, value: c.value)], policy: .atEnd))
})
}
}
}
}
My questions:
First: Is my theory correct?
If so: What can I do? Are there any entitlements that allow me to access HomeKit in background or similar? Do I need to perform the readValue
call elsewhere? Or is it just impossible to use the HomeKit API with WidgetKit with the current versions of HomeKit/WidgetKit/iOS and best I can do is hope they introduce this capability at some point in the future?
If not: What am I missing?