I knew that the speaker could be turned on remotely (within range) using the proprietary Ultimate Ears app, and it was obvious that the bluetooth command was sent by the application itself.
I first installed Apple’s Bluetooth logging profile on my iPhone, then connected it to the Mac via USB and used PacketLogger to trace the packages sent from the phone (specifically ATT Send
type). By opening the UE app and tapping on the remote power button in it I was able to sniff the conversation between the phone and the speaker as shown in this screenshot.
The handle 0x0003
is the specific sequential number tied to the remote power feature, whereas the value 4098ADA356C401
is the bluetooth MAC address of my iPhone (40:98:AD:A3:56:C4
) without semicolons followed by 01
. I then retrieved the MAC address of the speaker and used gatttool
on a Raspberry Pi to perform a write request, and BOOM (pun intended) I can turn on the speaker from my command line1.
This is the command that does the whole work:
gatttool -i hci0 -b $SPEAKER_ADDRESS --char-write-req -a 0x0003 -n ${HOST_ADDRESS}${ON_OFF_COMMAND}
SPEAKER_ADDRESS
is the MAC address of the speakerHOST_ADDRESS
is the MAC address of the audio source device (iPhone, …)ON_OFF_COMMAND
is 01
to turn the speaker on and 02
to turn it offFrom here, I wrote a Homebridge-compatible plugin and published it on NPM to make it available to all the Homebridge users. The plugin itself is mostly boilerplate and wraps the aforementioned command in a JavaScript file where the characteristics of the HomeKit device are declared.
You can find the package on NPM under the name homebridge-ueboom, or by clicking here. If you want to contribute, visit the GitHub repository.
I don’t know the exact specifications so this is pure speculation: the speaker itself has the usual Bluetooth 4.0 module that allows to stream music, in addition to that there’s also a BLE (Bluetooth Low Energy) module that for its own nature is always on and allows to turn the speaker on and off remotely (within range). The only reason why I’m not sure this is the real reason is that the two modules would probably have two separate MAC addresses, and from what I’ve observed there’s only one single address available.
The gatttool
command turns the speaker on but doesn’t associate the speaker with the Raspberry Pi. The speaker connects to the host
device (in my case my iPhone). ↩
Many macOS-based developers and Apple bloggers came up almost immediately with several solutions ranging from replacing the Battery.menu file to more user-friendly solutions such as iStat Menus or coconutBattery, which I strongly recommend.
Since I’m a software engineer, I decided to write my own script and integrate it into an Alfred workflow to see the battery time whenever I have the need. The script that regulates this workflow relies on pmset -g batt
, the command line utility that manages and manipulates the Mac power management.
mode=$(pmset -g batt | tail -n1 | awk '{print $4}')+
out=$(pmset -g batt | tail -n1 | awk '{print $5}')
case $mode in
"discharging"*)
if [[ $out == "(no" ]]
then echo "Calculating Time Remaining..."
else echo "$out Remaining"
fi;;
"charging"*)
if [[ $out == "(no" ]]
then echo "Calculating Time Until Full…"
else echo "$out Until Full"
fi;;
"charged"*)
echo "Battery Is Charged";;
"AC"*)
echo "Battery Is Not Charging";;
"+")
low=$(pmset -g batt | awk '{print $5}' | tail -n2)
if [[ $low == "(no"* ]]
then echo "Calculating Time Remaining..."
else echo "→ WARNING ←" && echo "$low Remaining"
fi;;
esac
Taking advance of the Large Type feature available in Alfred, I came up with an intrusive but fast way to check the remaining battery time (or the remaining time to be fully loaded, if attached to the power). I set up two ways to call the workflow: by typing battery
1 in the Alfred spotlight, or simply by pressing ⌘+⇧+B2.
At the following link you can download the workflow for Alfred.
Once Alfred spotlight has learned and cached your search behaviours, you won’t have to type the full keyword anymore. In my case bat
is already enough to get it on top of my search results, but is still less efficient than invoking it through the keystroke. ↩
Both commands can be easily customised after importing the workflow. ↩
Around the same time, Strava introduced the Suffer Score to quantify the training effort subjectively based on the heart rate and provides a score that would allow athletes from different levels and specialities to compare how hard they have performed. This feature is only available for premium users, which I’m not. Luckily for me, an acquaintance of mine has recently upgraded to premium and he was happy to share his training data with me. From here the decision to reverse engineer the Suffer Score algorithm and then build a Movescount app out of it so I can find out how hard I’m training (according to Strava) despite not being a premium member.
Strava uses five customisable heart rate zones (only available for Premium users) that well adapts to the three most-known systems: Allen & Coggan’s five zones described in Training and Racing with a Power Meter, and the simplified versions of Friel’s Training Bible seven zones and Fitzgerald’s 80/20 Endurance 5+2 zones.
After reading their official blogpost describing the new feature, it is clear that the Suffer Score is the sum of the time spent in each zone multiplied by a corresponding zone constant:
\[score=\sum_{n=1}^{5} t_{n} \cdot z_{n}\]What I was missing were the values of \(z_{n}\), which I was able to infer by interpolating the aforementioned training data. Luckily for me the dataset was well structured and diversified, ranging from easy trainings fully spent in Z1 and Z2 to interval sessions that included all the five zones. Thanks to those easy runs I was able to extrapolate the value of the first two zones constants (\(z_{1}\) and \(z_{2}\)) by using the binomial equation \(score=t_{1} \cdot z_{1}+t_{2} \cdot z_{2}\) with \(score\), \(t_{1}\) and \(t_{2}\) being known values. From there I worked my way up until I got all the five constants.
Unfortunately I’m not allowed to share the data I used since it belongs to an athlete who prefers keeping his trainings private, but these are the end results:
Since only two fixed heart parameters were available, rest (SUUNTO_USER_REST_HR
) and max (SUUNTO_USER_MAX_HR
), I wasn’t able to use the lactate threshold heart rate or other advanced metrics to calculate the five zones. I instead used a more simplistic approach using the max heart rate as a reference point:
The app runs once per second, providing the current heart rate (SUUNTO_HR
) and showing on screen the result value (RESULT
). From here, building a Movescount app was quite straightforward despite the limited syntax1.
RESULT = SCORE;
if(SUUNTO_HR >= SUUNTO_USER_REST_HR && SUUNTO_HR <= 60*SUUNTO_USER_MAX_HR/100) {
SCORE = SCORE + 25/3600;
} else if(SUUNTO_HR > 60*SUUNTO_USER_MAX_HR/100 && SUUNTO_HR <= 70*SUUNTO_USER_MAX_HR/100) {
SCORE = SCORE + 60/3600;
} else if(SUUNTO_HR > 70*SUUNTO_USER_MAX_HR/100 && SUUNTO_HR <= 80*SUUNTO_USER_MAX_HR/100) {
SCORE = SCORE + 115/3600;
} else if(SUUNTO_HR > 80*SUUNTO_USER_MAX_HR/100 && SUUNTO_HR <= 90*SUUNTO_USER_MAX_HR/100) {
SCORE = SCORE + 250/3600;
} else {
SCORE = SCORE + 300/3600;
}
You can find the app in the Movescount Apps section under the name Strava Suffer Score, or by clicking here2.
A switch
statement would have been better, unfortunately the available syntax is really limited. ↩
Movescount doesn’t let you edit the content of your app once it has been downloaded by one or more users, so the code currently available there doesn’t look as clean as the one above. ↩