Skip to content

Commit e5738e8

Browse files
committed
CL: Add hs.battery.privateBluetoothBatteryInfo() (for getting information from AirPods).
Closes #1608
1 parent e3498c3 commit e5738e8

File tree

1 file changed

+108
-0
lines changed

1 file changed

+108
-0
lines changed

extensions/battery/internal.m

+108
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,33 @@
66
#import <IOKit/pwr_mgt/IOPM.h>
77
#import <IOKit/pwr_mgt/IOPMLib.h>
88

9+
@import IOBluetooth;
10+
11+
// Define the private API items of IOBluetooth we wil be using
12+
// Taken from https://github.com/w0lfschild/macOS_headers/blob/master/macOS/Frameworks/IOBluetooth/6.0.2f2/IOBluetoothDevice.h
13+
@interface IOBluetoothDevice (Private)
14+
+ (id)connectedDevices;
15+
- (unsigned short)productID;
16+
- (unsigned short)vendorID;
17+
- (BOOL)isAppleDevice;
18+
@property(readonly) NSString *addressString;
19+
@property(readonly) BOOL isEnhancedDoubleTapSupported;
20+
@property(readonly) BOOL isANCSupported;
21+
@property(readonly) BOOL isInEarDetectionSupported;
22+
@property(nonatomic) unsigned char batteryPercentCombined;
23+
@property(nonatomic) unsigned char batteryPercentCase;
24+
@property(nonatomic) unsigned char batteryPercentRight;
25+
@property(nonatomic) unsigned char batteryPercentLeft;
26+
@property(nonatomic) unsigned char batteryPercentSingle;
27+
@property(nonatomic) unsigned char primaryBud;
28+
@property(nonatomic) unsigned char rightDoubleTap;
29+
@property(nonatomic) unsigned char leftDoubleTap;
30+
@property(nonatomic) unsigned char buttonMode;
31+
@property(nonatomic) unsigned char micMode;
32+
@property(nonatomic) unsigned char secondaryInEar;
33+
@property(nonatomic) unsigned char primaryInEar;
34+
@end
35+
936
// Helper functions to yank an object from a dictionary by key, and push it onto the LUA stack.
1037
// May be switched to use global NSObject_to_lua, if it ever actually lands in Hammerspoon
1138
// core; but since we're only needing a string, number, or boolean, and only at the top-level
@@ -446,6 +473,86 @@ static int battery_others(lua_State*L) {
446473
return 1;
447474
}
448475

476+
/// hs.battery.privateBluetoothBatteryInfo() -> table
477+
/// Function
478+
/// Returns information about Bluetooth devices using Apple Private APIs
479+
///
480+
/// Parameters:
481+
/// * None
482+
///
483+
/// Returns:
484+
/// * A table containing information about devices using private Apple APIs.
485+
///
486+
/// Notes:
487+
/// * This function uses private Apple APIs - that means it can break without notice on any macOS version update. Please report breakage to us!
488+
/// * This function will return information for all connected Bluetooth devices, but much of it will be meaningless for most devices
489+
/// * The table contains the following keys:
490+
/// * vendorID - Numerical identifier for the vendor of the device (Apple's ID is 76)
491+
/// * productID - Numerical identifier for the device
492+
/// * address - The bluetooth address of the device
493+
/// * isApple - A string containing "YES" or "NO", depending on whether or not this is an Apple/Beats product, or a third party product
494+
/// * name - A human readable string containing the name of the device
495+
/// * batteryPercentSingle - For some devices this will contain the percentage of the battery (e.g. Beats headphones)
496+
/// * batteryPercentCombined - We do not currently understand what this field represents, please report if you find a non-zero value here
497+
/// * batteryPercentCase - Battery percentage of AirPods cases (note that this will often read 0 - the AirPod case sleeps aggressively)
498+
/// * batteryPercentLeft - Battery percentage of the left AirPod if it is out of the case
499+
/// * batteryPercentRight - Battery percentage of the right AirPod if it is out of the case
500+
/// * buttonMode - We do not currently understand what this field represents, please report if you find a value other than 1
501+
/// * micMode - For AirPods this corresponds to the microphone option in the device's Bluetooth options
502+
/// * leftDoubleTap - For AirPods this corresponds to the left double tap action in the device's Bluetooth options
503+
/// * rightDoubleTap - For AirPods this corresponds to the right double tap action in the device's Bluetooth options
504+
/// * primaryBud - For AirPods this is either "left" or "right" depending on which bud is currently considered the primary device
505+
/// * primaryInEar - For AirPods this is "YES" or "NO" depending on whether or not the primary bud is currently in an ear
506+
/// * secondaryInEar - For AirPods this is "YES" or "NO" depending on whether or not the secondary bud is currently in an ear
507+
/// * isInEarDetectionSupported - Whether or not this device can detect when it is currently in an ear
508+
/// * isEnhancedDoubleTapSupported - Whether or not this device supports double tapping
509+
/// * isANCSupported - We believe this likely indicates whether or not this device supports Active Noise Cancelling (e.g. Beats Solo)
510+
/// * Please report any crashes from this function - it's likely that there are Bluetooth devices we haven't tested which may return weird data
511+
/// * Many/Most/All non-Apple party products will likely return zeros for all of the battery related fields here, as will Apple HID devices. It seems that these private APIs mostly exist to support Apple/Beats headphones.
512+
static int battery_private(lua_State *L) {
513+
LuaSkin *skin = [LuaSkin shared];
514+
[skin checkArgs:LS_TBREAK];
515+
516+
NSMutableArray *privateInfo = [[NSMutableArray alloc] init];
517+
518+
NSDictionary *devices = [IOBluetoothDevice connectedDevices];
519+
for (IOBluetoothDevice *device in devices) {
520+
NSMutableDictionary *deviceInfo = [[NSMutableDictionary alloc] init];
521+
deviceInfo[@"name"] = device.name;
522+
//NSLog(@"Found: %@ %i:%i", device.name, device.vendorID, device.productID);
523+
deviceInfo[@"vendorID"] = [NSString stringWithFormat:@"%i", device.vendorID];
524+
deviceInfo[@"productID"] = [NSString stringWithFormat:@"%i", device.productID];
525+
deviceInfo[@"isApple"] = [NSString stringWithFormat:@"%@", device.isAppleDevice ? @"YES" : @"NO"];
526+
deviceInfo[@"address"] = device.addressString;
527+
528+
deviceInfo[@"buttonMode"] = [NSString stringWithFormat:@"%i", device.buttonMode];
529+
530+
deviceInfo[@"batteryPercentCombined"] = [NSString stringWithFormat:@"%i", device.batteryPercentCombined];
531+
deviceInfo[@"batteryPercentSingle"] = [NSString stringWithFormat:@"%i", device.batteryPercentSingle];
532+
533+
deviceInfo[@"batteryPercentCase"] = [NSString stringWithFormat:@"%i", device.batteryPercentCase];
534+
deviceInfo[@"batteryPercentRight"] = [NSString stringWithFormat:@"%i", device.batteryPercentRight];
535+
deviceInfo[@"batteryPercentLeft"] = [NSString stringWithFormat:@"%i", device.batteryPercentLeft];
536+
537+
deviceInfo[@"primaryBud"] = [NSString stringWithFormat:@"%@", (device.primaryBud == 1) ? @"left" : @"right"];
538+
deviceInfo[@"isInEarDetectionSupported"] = [NSString stringWithFormat:@"%@", device.isInEarDetectionSupported ? @"YES" : @"NO"];
539+
deviceInfo[@"secondaryInEar"] = [NSString stringWithFormat:@"%@", device.secondaryInEar ? @"NO" : @"YES"];
540+
deviceInfo[@"primaryInEar"] = [NSString stringWithFormat:@"%@", device.primaryInEar ? @"NO" : @"YES"];
541+
542+
deviceInfo[@"isEnhancedDoubleTapSupported"] = [NSString stringWithFormat:@"%@", device.isEnhancedDoubleTapSupported ? @"YES" : @"NO"];
543+
deviceInfo[@"rightDoubleTap"] = [NSString stringWithFormat:@"%i", device.rightDoubleTap];
544+
deviceInfo[@"leftDoubleTap"] = [NSString stringWithFormat:@"%i", device.leftDoubleTap];
545+
546+
deviceInfo[@"micMode"] = [NSString stringWithFormat:@"%i", device.micMode];
547+
deviceInfo[@"isANCSupported"] = [NSString stringWithFormat:@"%@", device.isANCSupported ? @"YES" : @"NO"];
548+
549+
// Store the device
550+
[privateInfo addObject:deviceInfo];
551+
}
552+
[skin pushNSObject:privateInfo];
553+
return 1;
554+
}
555+
449556
static const luaL_Reg battery_lib[] = {
450557
{"cycles", battery_cycles},
451558
{"name", battery_name},
@@ -466,6 +573,7 @@ static int battery_others(lua_State*L) {
466573
{"powerSource", battery_powersource},
467574
{"psuSerial", battery_psuSerial},
468575
{"otherBatteryInfo", battery_others},
576+
{"privateBluetoothBatteryInfo", battery_private},
469577
{NULL, NULL}
470578
};
471579

0 commit comments

Comments
 (0)