Get Bluetooth Names & Charges from MenuBar
Hi team.
I've been trying to get the Bluetooth names & charges from the MenuBar but I've failed miserably at it.
This is what I've come up with but of course I'm just getting an empty value, is there a better way of doing this?
let extrasMenuBar = sf.ui.app('com.apple.controlcenter').getElement("AXExtrasMenuBar")
let menuItems = []
extrasMenuBar.children.forEach(item => {
if (item.getString('AXDescription') === 'Bluetooth') {
menuItems.push(item.children.map(i => i.title.value))
};
});
log(menuItems)
Thanks in advance,
Nic.
Raphael Sepulveda @raphaelsepulveda2024-11-19 06:03:53.136Z@Nicolas_Aparicio, I've got a couple of ways for you.
Scraping the UI
This first one is continuing the UI approach you started. It's not very sexy since it needs to open the menu to get the data and feels fragile since I'm relying on the menu to show up in one particular place, so it might not work on other systems or versions of macOS (I'm on Ventura).
function getBluetoothDevicesViaUi() { const controlCenterApp = sf.ui.app('com.apple.controlcenter'); const extrasMenuBar = controlCenterApp.getElement("AXExtrasMenuBar"); const bluetoothMenu = extrasMenuBar.children.find(item => item.getString('AXDescription') === 'Bluetooth' ); if (!bluetoothMenu) return; bluetoothMenu.elementClick(); // Open Bluetooth menu const menuSearchPosition = { x: bluetoothMenu.position.x - 200, y: bluetoothMenu.position.y + 90 }; const popupMenu = controlCenterApp.getElementAtPosition( menuSearchPosition.x, menuSearchPosition.y ); const bluetoothDevices = popupMenu.children.whoseRole.is("AXCheckBox") .map(checkbox => checkbox.getString('AXDescription')); bluetoothMenu.elementClick(); // Close Bluetooth menu return { bluetoothDevices }; } const bluetoothDevices = getBluetoothDevicesViaUi().bluetoothDevices; log(bluetoothDevices);// Returns this 👇🏼 [ "Magic Keyboard, 69%", "MX Vertical, 100%", ]- Pros: Gets you all the devices on the list along with the battery charges
- Cons: Fragile execution
Getting it through Bash
This one gets it straight from the system, although verbose.
After kicking the ball back and forth with chatGPT, here's what we got:
function getBluetoothDevices() { /** @param {string} bluetoothDevicesText */ function parseConnectedDevices(bluetoothDevicesText) { const devices = {}; const lines = bluetoothDevicesText.split(/ {9}/); let currentDevice = null; lines.forEach(line => { // Trim leading/trailing whitespace const trimmed = line.trim(); // Skip empty lines and "Connected:" if (!trimmed || trimmed === 'Connected:') return; // Check for device name (e.g., "Magic Keyboard:") if (trimmed.endsWith(':') && !trimmed.includes('Address')) { currentDevice = trimmed.slice(0, -1); // Remove trailing colon devices[currentDevice] = {}; // Initialize the device object } else if (currentDevice) { // Parse key-value pairs (e.g., "Address: B0:BE:83:F3:56:96") const [key, ...valueParts] = trimmed.split(':'); if (key && valueParts.length > 0) { devices[currentDevice][key.trim()] = valueParts.join(':').trim(); } } }); return devices; } const bluetoothDevicesText = sf.system.exec({ commandLine: `system_profiler SPBluetoothDataType | awk '/Connected:/{flag=1} /Not Connected:/{flag=0} flag'` }).result; const bluetoothDevices = parseConnectedDevices(bluetoothDevicesText); return { bluetoothDevices }; } const bluetoothDevices = getBluetoothDevices().bluetoothDevices; log(bluetoothDevices);// Returns this 👇🏼 { "Magic Keyboard": { "Address": "B0:BE:83:F3:56:96", "Vendor ID": "0x004C", "Product ID": "0x029F", "Firmware Version": "2.0.6", "Minor Type": "Keyboard", "Services": "0x800020 < HID ACL >" }, "MX Vertical": { "Address": "E1:45:3B:86:56:3D", "Vendor ID": "0x046D", "Product ID": "0xB020", "Battery Level": "100%", "Firmware Version": "MPM16.00_0009", "Minor Type": "Mouse", "Services": "0x400000 < BLE >" } }- Pros: Transparent fetching of data.
- Cons: For some reason it doesn't provide battery level for all devices.
Pick your poison!
- NNicolas Aparicio @Nicolas_Aparicio
Hey mate @raphaelsepulveda, you are a legend, thanks a lot!, this will give me tons of choices to be able to get what I need.
It's weird how the computer will have a hard time returning the battery level of the bluetooth items. I've tried many ways to do so through terminal and applescript but the results are inconsistent and won't work on every OS 😟.
These two codes will give what I need in a much robust way than all of that 🤘.Cheers Raph,
Nic.
Dustin Harris @Dustin_HarrisHere's a different version of @raphaelsepulveda 's second approach, using the system profiler CLI. I've filtered the list to only show connected devices, as disconnected devices don't show battery level :)
function getBluetoothDeviceInfo() { const bluetoothDeviceJson = sf.system.exec({ commandLine: "system_profiler SPBluetoothDataType -json" }).result; const bluetoothDevices = JSON.parse(bluetoothDeviceJson); const connectedDevices = bluetoothDevices.SPBluetoothDataType[0]["device_connected"]; return connectedDevices } log(getBluetoothDeviceInfo())
Dustin Harris @Dustin_Harrisbetter yet, this will display the information in a nice way. Still doesn't show all battery levels though like it does in the bluetooth menu for some reason.
function getBluetoothDeviceInfo() { const bluetoothDeviceJson = sf.system.exec({ commandLine: "system_profiler SPBluetoothDataType -json" }).result; const bluetoothDevices = JSON.parse(bluetoothDeviceJson); const connectedDevices = bluetoothDevices.SPBluetoothDataType[0]["device_connected"]; const parsedConnectedDevices = connectedDevices.map(device => { const deviceName = Object.keys(device)[0]; const batteryLevel = device[deviceName]["device_batteryLevelMain"]; return {deviceName, batteryLevel} }) return parsedConnectedDevices; } let devices = getBluetoothDeviceInfo(); const devicesAsStringArray = devices.map(device => `${device.deviceName}: ${device.batteryLevel}`) alert(devicesAsStringArray.join("\n\n"))- NNicolas Aparicio @Nicolas_Aparicio
Oooh nice @Dustin_Harris, I'll try this tomorrow 🤙. It is weird how nothing will be consistent when giving you the battery levels. I've tried terminal codes that would give me everything except the keyboard battery level and it'll change depending on the computer you use it in 🤷.
Thanks for this mate!