Skip to content
Browse files
CL: Add hs.battery.privateBluetoothBatteryInfo() (for getting informa…
…tion from AirPods).

Closes #1608
  • Loading branch information
cmsj committed Nov 27, 2017
1 parent e3498c3 commit e5738e8
Showing 1 changed file with 108 additions and 0 deletions.
@@ -6,6 +6,33 @@
#import <IOKit/pwr_mgt/IOPM.h>
#import <IOKit/pwr_mgt/IOPMLib.h>

@import IOBluetooth;

// Define the private API items of IOBluetooth we wil be using
// Taken from
@interface IOBluetoothDevice (Private)
+ (id)connectedDevices;
- (unsigned short)productID;
- (unsigned short)vendorID;
- (BOOL)isAppleDevice;
@property(readonly) NSString *addressString;
@property(readonly) BOOL isEnhancedDoubleTapSupported;
@property(readonly) BOOL isANCSupported;
@property(readonly) BOOL isInEarDetectionSupported;
@property(nonatomic) unsigned char batteryPercentCombined;
@property(nonatomic) unsigned char batteryPercentCase;
@property(nonatomic) unsigned char batteryPercentRight;
@property(nonatomic) unsigned char batteryPercentLeft;
@property(nonatomic) unsigned char batteryPercentSingle;
@property(nonatomic) unsigned char primaryBud;
@property(nonatomic) unsigned char rightDoubleTap;
@property(nonatomic) unsigned char leftDoubleTap;
@property(nonatomic) unsigned char buttonMode;
@property(nonatomic) unsigned char micMode;
@property(nonatomic) unsigned char secondaryInEar;
@property(nonatomic) unsigned char primaryInEar;

// Helper functions to yank an object from a dictionary by key, and push it onto the LUA stack.
// May be switched to use global NSObject_to_lua, if it ever actually lands in Hammerspoon
// 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) {
return 1;

/// hs.battery.privateBluetoothBatteryInfo() -> table
/// Function
/// Returns information about Bluetooth devices using Apple Private APIs
/// Parameters:
/// * None
/// Returns:
/// * A table containing information about devices using private Apple APIs.
/// Notes:
/// * This function uses private Apple APIs - that means it can break without notice on any macOS version update. Please report breakage to us!
/// * This function will return information for all connected Bluetooth devices, but much of it will be meaningless for most devices
/// * The table contains the following keys:
/// * vendorID - Numerical identifier for the vendor of the device (Apple's ID is 76)
/// * productID - Numerical identifier for the device
/// * address - The bluetooth address of the device
/// * isApple - A string containing "YES" or "NO", depending on whether or not this is an Apple/Beats product, or a third party product
/// * name - A human readable string containing the name of the device
/// * batteryPercentSingle - For some devices this will contain the percentage of the battery (e.g. Beats headphones)
/// * batteryPercentCombined - We do not currently understand what this field represents, please report if you find a non-zero value here
/// * batteryPercentCase - Battery percentage of AirPods cases (note that this will often read 0 - the AirPod case sleeps aggressively)
/// * batteryPercentLeft - Battery percentage of the left AirPod if it is out of the case
/// * batteryPercentRight - Battery percentage of the right AirPod if it is out of the case
/// * buttonMode - We do not currently understand what this field represents, please report if you find a value other than 1
/// * micMode - For AirPods this corresponds to the microphone option in the device's Bluetooth options
/// * leftDoubleTap - For AirPods this corresponds to the left double tap action in the device's Bluetooth options
/// * rightDoubleTap - For AirPods this corresponds to the right double tap action in the device's Bluetooth options
/// * primaryBud - For AirPods this is either "left" or "right" depending on which bud is currently considered the primary device
/// * primaryInEar - For AirPods this is "YES" or "NO" depending on whether or not the primary bud is currently in an ear
/// * secondaryInEar - For AirPods this is "YES" or "NO" depending on whether or not the secondary bud is currently in an ear
/// * isInEarDetectionSupported - Whether or not this device can detect when it is currently in an ear
/// * isEnhancedDoubleTapSupported - Whether or not this device supports double tapping
/// * isANCSupported - We believe this likely indicates whether or not this device supports Active Noise Cancelling (e.g. Beats Solo)
/// * Please report any crashes from this function - it's likely that there are Bluetooth devices we haven't tested which may return weird data
/// * 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.
static int battery_private(lua_State *L) {
LuaSkin *skin = [LuaSkin shared];
[skin checkArgs:LS_TBREAK];

NSMutableArray *privateInfo = [[NSMutableArray alloc] init];

NSDictionary *devices = [IOBluetoothDevice connectedDevices];
for (IOBluetoothDevice *device in devices) {
NSMutableDictionary *deviceInfo = [[NSMutableDictionary alloc] init];
deviceInfo[@"name"] =;
//NSLog(@"Found: %@ %i:%i",, device.vendorID, device.productID);
deviceInfo[@"vendorID"] = [NSString stringWithFormat:@"%i", device.vendorID];
deviceInfo[@"productID"] = [NSString stringWithFormat:@"%i", device.productID];
deviceInfo[@"isApple"] = [NSString stringWithFormat:@"%@", device.isAppleDevice ? @"YES" : @"NO"];
deviceInfo[@"address"] = device.addressString;

deviceInfo[@"buttonMode"] = [NSString stringWithFormat:@"%i", device.buttonMode];

deviceInfo[@"batteryPercentCombined"] = [NSString stringWithFormat:@"%i", device.batteryPercentCombined];
deviceInfo[@"batteryPercentSingle"] = [NSString stringWithFormat:@"%i", device.batteryPercentSingle];

deviceInfo[@"batteryPercentCase"] = [NSString stringWithFormat:@"%i", device.batteryPercentCase];
deviceInfo[@"batteryPercentRight"] = [NSString stringWithFormat:@"%i", device.batteryPercentRight];
deviceInfo[@"batteryPercentLeft"] = [NSString stringWithFormat:@"%i", device.batteryPercentLeft];

deviceInfo[@"primaryBud"] = [NSString stringWithFormat:@"%@", (device.primaryBud == 1) ? @"left" : @"right"];
deviceInfo[@"isInEarDetectionSupported"] = [NSString stringWithFormat:@"%@", device.isInEarDetectionSupported ? @"YES" : @"NO"];
deviceInfo[@"secondaryInEar"] = [NSString stringWithFormat:@"%@", device.secondaryInEar ? @"NO" : @"YES"];
deviceInfo[@"primaryInEar"] = [NSString stringWithFormat:@"%@", device.primaryInEar ? @"NO" : @"YES"];

deviceInfo[@"isEnhancedDoubleTapSupported"] = [NSString stringWithFormat:@"%@", device.isEnhancedDoubleTapSupported ? @"YES" : @"NO"];
deviceInfo[@"rightDoubleTap"] = [NSString stringWithFormat:@"%i", device.rightDoubleTap];
deviceInfo[@"leftDoubleTap"] = [NSString stringWithFormat:@"%i", device.leftDoubleTap];

deviceInfo[@"micMode"] = [NSString stringWithFormat:@"%i", device.micMode];
deviceInfo[@"isANCSupported"] = [NSString stringWithFormat:@"%@", device.isANCSupported ? @"YES" : @"NO"];

// Store the device
[privateInfo addObject:deviceInfo];
[skin pushNSObject:privateInfo];
return 1;

static const luaL_Reg battery_lib[] = {
{"cycles", battery_cycles},
{"name", battery_name},
@@ -466,6 +573,7 @@ static int battery_others(lua_State*L) {
{"powerSource", battery_powersource},
{"psuSerial", battery_psuSerial},
{"otherBatteryInfo", battery_others},
{"privateBluetoothBatteryInfo", battery_private},

0 comments on commit e5738e8

Please sign in to comment.