MacOS 27 breaks MACRO Mouse & Keyboard Battery Charge Level Warning

I have the V1.1 2024 of the macro and it reports 0% charge for my Apple Magic Trackpad. The battery is almost fully charged.

Looking at Macro, I assume that something changed about the output of ioreg has changed. Looking at code, I see that this code:

#!/bin/zsh

# Initialize arrays to store the captured values
products=()
device_addresses=()
battery_percents=()

clear

# Run ioreg command to get the AppleDeviceManagementHIDEventService nodes
ioreg -n IOHIDInterface -r -l |
  awk '
  BEGIN {
    # Initialize variables
    products = ""
    device_address = ""
    battery_percent = ""
    has_battery = ""
  }

  /"HasBattery" = / {
    has_battery = $NF
  }

  /"Product" = / {
    gsub("\"","",$3)
    product = substr($0, index($0, $3))
    gsub(/\"$/, "", product) # Remove trailing backslash if any
  }
  

  /"DeviceAddress" = / {
    gsub(/\"/, "", $3)
    device_address = $3
  }

  /"BatteryPercent" = / {
    battery_percent = $3
  }

  # When encountering the closing brace, check if HasBattery is Yes and print the information
  /}/ {
    if (has_battery == "Yes" && battery_percent != "") {
      printf "Product: %s\nDeviceAddress: %s\nBatteryPercent: %d\n", product, device_address, battery_percent
      
    product = ""
    device_address = ""
    battery_percent = ""
    has_battery = ""
  

    }
  }' | while read -r line; do
  case "$line" in
  Product:*)
    products+=("${line#Product: }")
    ;;
  DeviceAddress:*)
    device_addresses+=("${line#DeviceAddress: }")
    ;;
  BatteryPercent:*)
    battery_percents+=("${line#BatteryPercent: }")
    ;;
  esac
done

# Print the captured values (or process them further)
 for I in {1..${#products[@]}}; do
   echo "${products[I]}, ${battery_percents[I]}"
 done
# Create a JSON structure and output using jq
# json_output=$(
#   jq -n \
#     --argjson products "$(printf '%s\n' "${products[@]}" | jq -R . | jq -s .)" \
#     --argjson device_addresses "$(printf '%s\n' "${device_addresses[@]}" | jq -R . | jq -s .)" \
#     --argjson battery_percents "$(printf '%s\n' "${battery_percents[@]}" | jq -R . | jq -s .)" \
#     '{devices: [[$products, $device_addresses, $battery_percents] | transpose[] | {product: .[0], device_address: .[1], battery_percent: .[2]}]}'
# )

# echo $json_output

Finds 0 percent charge. Looking at the output of ioreg:

AppleDeviceManagementHIDEventService  <class AppleDeviceManagementHIDEven$
    | {
    |   "PrimaryUsage" = 11
    |   "IOGeneralInterest" = "IOCommand is not serializable"
    |   "IOMatchedAtBoot" = Yes
    |   "LowBatteryNotificationPercentage" = 6
    |   "PrimaryUsagePage" = 65280
    |   "BatteryFaultNotificationType" = "TPBatteryFault"
    |   "HasBattery" = Yes
    |   "VendorID" = 76
    |   "VersionNumber" = 792
    |   "Built-In" = No
    |   "DeviceAddress" = "a0-78-17-e3-ce-37"
    |   "BatteryLowPowerDisconnectionNotificationType" = "TPLowBatteryPowerDisc$
    |   "HardwareID" = 1
    |   "Product" = "MDLMagicTrackpad"
    |   "SerialNumber" = "A0:78:17:E3:CE:37"
    |   "WakeReason" = "Host (0x01)"
    |   "Transport" = "Bluetooth"
    |   "HIDRMDeviceState" = "Unknown"
    |   "Accessory Category" = "Trackpad"
    |   "BatteryLowNotificationType" = "TPLowBattery"
    |   "Manufacturer" = "Apple Inc."
    |   "ConnectionNotificationType" = "TPConnected"
    |   "ProductID" = 613
    |   "DeviceUsagePairs" = ({"DeviceUsagePage"=65280,"DeviceUsage"=11},{"Devi$
    |   "IOPersonalityPublisher" = "com.apple.driver.AppleTopCaseDriverV2"
    |   "PoweredOffNotificationType" = "TPOff"
    |   "MTFW Version" = 1301
    |   "BD_ADDR" = <a07817e3ce37>
    |   "BatteryPercent" = 94
    |   "HostTransportSwitchNotificationType" = "TPTransportSwitch"
    |   "CriticallyLowBatteryNotificationPercentage" = 3
    |   "ReportInterval" = 8000
    |   "RadioFW Version" = 792
    |   "VendorIDSource" = 1
    |   "STFW Version" = 2161
    |   "HIDEventServiceProperties" = {"HIDMouseKeysOptionToggles"=0,"JitterNoC$
    |   "CFBundleIdentifier" = "com.apple.driver.AppleTopCaseHIDEventDriver"
    |   "IOProviderClass" = "IOHIDInterface"
    |   "LocationID" = 400805431
    |   "BluetoothDevice" = Yes
    |   "IOClass" = "AppleDeviceManagementHIDEventService"
    |   "DisconnectionNotificationType" = "TPDisconnected"
    |   "HIDServiceSupport" = Yes
    |   "ProductIDArray" = (613)
    |   "SupportsDeviceInfo" = 1
    |   "CFBundleIdentifierKernel" = "com.apple.driver.AppleTopCaseHIDEventDriv$
    |   "IOMatchCategory" = "IODefaultMatchCategory"
    |   "BatteryStatusFlags" = 0
    |   "IOProbeScore" = 7175
    |   "ColorID" = 0
    |   "CountryCode" = 0
    |   "BTFW Version" = 792
    | }

Neither of HasBattery or BatteryPercent seem to have changed. Is anyone else encountering this?

No LLM was harmed in the creation of this question. (All grammatical and logical errors are mine.)

For those looking to help, the original macro is here:

I took a look, but the Bluetooth side is beyond me. (And I don't have any Apple devices to test with.)

-rob.

Thanks my search fu seems a bit weak today.

I fed the problem into Claude in the end. The core of the problem is ioreg output has nested one level deeper. The original code was hard-coded to a specific column.

Claude’s other comments:

Changes:

  • Values found by content, not position BatteryPercent and DeviceAddress now use the last field ($NF). Producttakes everything after =, so names with spaces still work. This should keep working if ioreg changes its indentation again.
  • Tighter end-of-record match The old /}/ pattern also matched the braces inside inline dictionaries like DeviceUsagePairs. It now only matches the node's own closing brace.
  • Clean reset between devices The variables used to reset only after a battery device was printed. They now reset after every record, so one device's name can't carry over to the next.

I would never have found this since I've never seen ioreg output before to know that it changed. Claude's revised code:

#!/bin/zsh

# Initialize arrays to store the captured values
products=()
device_addresses=()
battery_percents=()

clear

# Run ioreg command to get the AppleDeviceManagementHIDEventService nodes
ioreg -n IOHIDInterface -r -l |
  awk '
  BEGIN {
    # Initialize variables
    product = ""
    device_address = ""
    battery_percent = ""
    has_battery = ""
  }

  /"HasBattery" = / {
    has_battery = $NF
  }

  # Split on " = " rather than field position: macOS 27 nests the node deeper
  /"Product" = / {
    product = substr($0, index($0, " = ") + 3)
    gsub(/"/, "", product)
  }

  /"DeviceAddress" = / {
    device_address = $NF
    gsub(/"/, "", device_address)
  }

  /"BatteryPercent" = / {
    battery_percent = $NF
  }

  # Match only the closing brace of the node, not inline dicts
  /^[ |]*}$/ {
    if (has_battery == "Yes" && battery_percent != "") {
      printf "Product: %s\nDeviceAddress: %s\nBatteryPercent: %d\n", product, device_address, battery_percent
    }
    product = ""
    device_address = ""
    battery_percent = ""
    has_battery = ""
  }' | while read -r line; do
  case "$line" in
  Product:*)
    products+=("${line#Product: }")
    ;;
  DeviceAddress:*)
    device_addresses+=("${line#DeviceAddress: }")
    ;;
  BatteryPercent:*)
    battery_percents+=("${line#BatteryPercent: }")
    ;;
  esac
done

# Print the captured values (or process them further)
 for I in {1..${#products[@]}}; do
   echo "${products[I]}, ${battery_percents[I]}"
 done
# Create a JSON structure and output using jq
# json_output=$(
#   jq -n \
#     --argjson products "$(printf '%s\n' "${products[@]}" | jq -R . | jq -s .)" \
#     --argjson device_addresses "$(printf '%s\n' "${device_addresses[@]}" | jq -R . | jq -s .)" \
#     --argjson battery_percents "$(printf '%s\n' "${battery_percents[@]}" | jq -R . | jq -s .)" \
#     '{devices: [[$products, $device_addresses, $battery_percents] | transpose[] | {product: .[0], device_address: .[1], battery_percent: .[2]}]}'
# )

# echo $json_output

I will let link here from the original post. I won't claim to update the macro, instead I will leave that to the original poster.

FWIW I would love to have a version control system for macros.