Are there some undocumented (or, well, documented, but overlooked by me) prerequisites to the readValueWithCompletionHandler: method?
The reason I ask is that occasionally I am getting the Read/Write operation failed error in the callback, even in cases where direct, non-deferred reading of the value worked properly. It seems to happen very consistently with some accessories and characteristics, not randomly; thus, it is not likely a temporary quirk in the communication with the device.
Probably I am overlooking something of importance, but it does not make a good sense to me. My code (is it right, or can you see anything wrong in there?)
// in an HMCharacteristic category
if ([self.properties containsObject:HMCharacteristicPropertyReadable]) {
id val=self.value, ident=[NSString stringWithFormat:@" [%@] %@ (%@)", self.uniqueIdentifier, self.localizedDescription, self.service.accessory.name];
NSLog(@"nondeferred '%@'%@", val, ident);
if (self.service.accessory.reachable) {
[self readValueWithCompletionHandler:^(NSError * _Nullable error) {
if (error) NSLog(@"deferred ERROR %@ -> %@", ident, error);
else NSLog(@"deferred '%@'%@", self.value, ident);
}];
}
}
for most accessories/characteristics works properly, but for some of them I am consistently getting results like
nondeferred '70.5' [64998F70-9C11-502F-B8B4-E99DC5C3171B] Current Relative Humidity (Vlhkoměr TH)
deferred '70.5' ERROR [64998F70-9C11-502F-B8B4-E99DC5C3171B] Current Relative Humidity (Vlhkoměr TH) -> Error Domain=HMErrorDomain Code=74 "Read/Write operation failed." UserInfo={NSLocalizedDescription=Read/Write operation failed.}
Do I do something wrong in my code, or is that normal with some devices?
If the latter, is there perhaps a way to know beforehand that I should not use readValueWithCompletionHandler: (for it is bound to fail anyway), and instead I should simply use self.value non-deferred? For some time it seemed to me it happens with bridged accessories, but not really, this hypothesis proved wrong by further testing.
Thanks!
Do I do something wrong in my code?
No, not really.
or is that normal with some devices?
Sort of. As you've noticed, both this:
It seems to happen very consistently with some accessories and characteristics, not randomly; thus, it is not likely a temporary quirk in the communication with the device.
and this:
For some time it seemed to me it happens with bridged accessories, but not really, this hypothesis proved wrong by further testing.
...tend to be true. More specifically, what's actually going on is that there are mutually contradictory "goals" in the API and the (some) accessories. That is:
-
HomeKit wants/expects accessories to respond to commands relatively quickly.
-
To save power, some accessories want to avoid using their radio to save power.
This is actually easier to understand if you reverse the question and ask "what accessories DON'T have this problem". The answer is invariably powered, particularly WiFi, accessories. They're basically always "awake" and ready to respond to commands.
Conversely, things like Bluetooth/Thread/Matter sensors often have these issues— they're desperate to save power, so:
-
They're sleeping frequently, delaying responsiveness.
-
They tend to use low-end hardware, so the accessory itself is slower.
-
Small size and low-power radio make it easier to disrupt communication.
...all of which mean they often take longer than HomeKit is willing to wait before they respond. Even your testing experience with bridges is often true:
For some time it seemed to me it happens with bridged accessories, but not really, this hypothesis proved wrong by further testing.
To start with, not all bridges are really "bridges". For example, one of HomeKit's "rules" is that all the services of an accessory must be in the same room, which is a bit of a problem for HVAC units which have room-specific sensors wired through their ducts. At a hardware level, that device is essentially one big wired accessory but it presents itself as a bridge so that the user can put the sensors in the right rooms.
However, the more common case is that the bridge itself is a powered WiFi accessory but the devices it's bridging are wireless accessories, often either low powered and/or built on protocols that are much slower/higher latency than HomeKit. In some cases, the bridge simply CANNOT service the command it's received within the time window HomeKit expects. That means its only choices are:
-
Fail the command.
-
Lie.
This is what creates the pattern you see on many bridges where:
-
An initial command fails -> because the bridge couldn't talk to the endpoint in time.
-
Shortly after the accessory updates -> because the bridge got the answer to the command.
-
Ongoing reads work fine -> because the bridge has either got the endpoint accessory active or because the bridge is now "lying" to cover up the latency gap.
That leads to a clarification here:
(for it is bound to fail anyway)
What "fail" means for this API is "unable to retrieve a new value within an expected amount of time", NOT necessarily "nothing happened". As I alluded to above, it's entirely possible for a failed readValue to cause the value to update and/or change the behavior of other interactions.
If the latter, is there perhaps a way to know beforehand that I should not use readValueWithCompletionHandler: (for it is bound to fail anyway), and instead I should simply use self.value non-deferred?
So, this really comes down to the question "what are you trying to do". As the documentation of "value" says:
"This is the last value that the system saw for the characteristic. Because multiple apps can access a given home, this value may change without your app changing it. To be sure you have the current value, call readValueWithCompletionHandler: and wait for the response before checking the value property."
What I'll add here is that in most cases I think you should think of this as a "user interface" issue, NOT a "communication" issue. That is, a readValue failure does not (necessarily) mean anything is "wrong". If you watch Home.app carefully at it launches, you can actually see if it issues a read and then mark accessories as "No Response" if the read fails. It doesn't do anything dramatic (like posting an alert or painting the accessory red) because these issues tend to fall into one of two camps:
-
It's a transient issue that will self-correct.
-
The accessory is busted and likely to stay that way.
The subtle point here is that making "noise" about the second case is probably just as much a mistake as the first case. Taking myself as an example, I have a light switch in my home office that's been unresponsive for ~6 months. The switch is ~8 years old and still works as a physical switch. I need to replace it, but I tend to work in the dark so it hasn't bothered me enough to get around to fixing it. Having Home.app tell me there was an issue wouldn't do anything except annoy me.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware