Troubleshooting - Displaying keyboard/mouse battery on Stream Deck

Hi,

Thanks to the brilliant work of these folks (How to set custom mouse/keyboard battery level alert?) I modified the macro to display the remaining battery of my keyboard and magic mouse on my Stream Deck. When the battery reaches a certain level rather than triggering a notification it changes the icon colour red on the Stream Deck so I don't forget to charge it next time I turn off my computer.

Everything works fine except the 'Local_Mouse' and 'Local_Keyboard' variables randomly seem to switch places. So the remaining keyboard percentage will be displayed on the mouse Stream Deck key and vice versa.

This was a very 'hacky' solution modifying the macro in the forum post I linked so I'm not sure whether it's a problem inherent in the macro or a potential bug.

If anyone could take a look at the macro and let me know why they might be randomly switching places it would be much appreciated! Thanks.

Display Mouse:Keyboard Battery Percentage.kmmacros (92.0 KB)

If I had to guess (and this is only a guess), I bet that sometimes ioreg is giving you the information in a different order than you are expecting.

The key to avoiding that is to never trust that ioreg will give you the information in the same order, and deciding what to do about that.

Here’s an alternative solution that I have used for this.

This command:

ioreg -r -k "BatteryPercent" \
| egrep '"Product"|"BatteryPercent"' \

gives me this result:

      "Product" = "Magic Trackpad 2"
      "BatteryPercent" = 92
      "Product" = "Magic Keyboard"
      "BatteryPercent" = 90

So far so good. Except that the percent is on a different line from the device. If we had something like this:

      "Product" = "Magic Trackpad 2"      "BatteryPercent" = 92
      "Product" = "Magic Keyboard"      "BatteryPercent" = 90

That would be better because we’d know that each device and its percent would be together on the same line.

Q: But how do we say “remove some of the line breaks”?

Answer: Don’t know.

Notice the formatting of the output. The product names are "quoted" but the percentages are not.

Q: What if we could say “Only remove a line break if it comes immediately after a quotation mark?” Then it wouldn't remove the ones after "92" and "90" because they don't have a quotation mark.

In perl you can replace a line break by searching for \n. So we add a quick perl line to do that, telling perl only to replace \n if it comes immediately after " and it looks like this:

ioreg -r -k "BatteryPercent" \
| egrep '"Product"|"BatteryPercent"' \
| perl -p -e 's#"\n#" #'

Technically we told perl to:

  1. look for a " followed by \n
  2. replace it with a " followed by a space (blank).

The # are just there to mark the beginning, middle, and end of the perl regular expression.

Now the output is:

      "Product" = "Magic Trackpad 2"       "BatteryPercent" = 92
      "Product" = "Magic Keyboard"       "BatteryPercent" = 90

Getting close now. We really don’t needs the words "Product" and "BatteryPercent" or the equal signs. But those are easy enough to get rid of.

(Note: someone who knows perl better than me could probably avoid also needing sed but I barely know any perl so… I'm using sed also. Please accept my apologies if this offends you.)

ioreg -r -k "BatteryPercent" \
| egrep '"Product"|"BatteryPercent"' \
| perl -p -e 's#"\n#" #' \
| sed 	-e 's#^ *"Product" = ##g' \
		-e 's#  "BatteryPercent" = #	#g'

There are two sed commands there. The first gets rid of 'Product' the equals sign (and space) that follows it, and the blank spaces in front of it.

The second sed command gets rid of BatteryPercent and the equals sign and space, and replace them with a tab character. There are 2 reasons for using a tab which we'll discuss in a moment, but just to be clear where the tab is, it's where you see the letters TAB here:

-e 's#  "BatteryPercent" = #TAB#g'

The first reason for using a tab is that it gives this nicely formatted output:

"Magic Trackpad 2"     	92
"Magic Keyboard"     	90

That's very pleasing.

But the second reason for using a tab will be in a moment when we need to search for a specific character, and tab is a good one.

Note that we still haven’t necessarily solved the underlying issue.

What happens if next time we run ioreg we get the keyboard battery info first and the trackpad second?

What if we use a different keyboard someday that doesn’t report its battery percentage at all?

What if we trade in our “Magic Trackpad 2” for a USB mouse?

(OK, I realize that last one is a stretch, but for the sake of discussion.)

Ideally what we would like to do is be able to look at the output of ioreg and say “Show me the Trackpad battery percent” and “Show me the keyboard percent” and not care which order that they are in.

Turns out, we can do that too.

First we’re going to put the entire output of our commands into a variable named $INFO:

INFO=$(ioreg -r -k "BatteryPercent" \
| egrep '"Product"|"BatteryPercent"' \
| perl -p -e 's#"\n#" #' \
| sed 	-e 's#^ *"Product" = ##g' \
		-e 's#  "BatteryPercent" = #	#g')

You'll notice that I just added INFO=$( to the beginning and ) to the end. All that does is save our nice output from above into a variable called $INFO.

Now that we have a variable with information we can re-use without having to keep using ioreg. All that is left is to figure out how to say “Give me the part of $INFO which has the keyboard info” and “Give me the part of $INFO which has the trackpad info.”

echo "$INFO" | awk -F'	' '/Keyboard/{print $NF}'

Let me be clear that there is a tab after the -F

echo "$INFO" | awk -F'TAB' '/Keyboard/{print $NF}'

What that says is: “Give me ALL of the information from the variable $INFO but then use awk to limit it just to the line which includes the word Keyboard and then show me the last ‘word’ on that line.”

{print $NF} in awk means “Show me the last ‘word’ i.e. anything that is not whitespace”

Now we do the same thing for the trackpad:

echo "$INFO" | awk -F'	' '/Trackpad/{print $NF}'

Now it doesn’t matter what order the information comes to us, because we are being very specific about what information we want. We can put those two commands into variables, like this:

KEYBOARD_BATTERY_PERCENT=$(echo "$INFO" | awk -F'	' '/Keyboard/{print $NF}')

TRACKPAD_BATTERY_PERCENT=$(echo "$INFO" | awk -F'	' '/Trackpad/{print $NF}')

And now we can use $KEYBOARD_BATTERY_PERCENT or $TRACKPAD_BATTERY_PERCENT anywhere we want.

Guess what happens if there is no Trackpad information in the variable $INFO?

$TRACKPAD_BATTERY_PERCENT will be empty! You won’t accidentally get the keyboard information. And vice versa.

Ok, but how do I use this with Stream Deck and Keyboard Maestro ?

My solution would be to do it all in a shell script (try to contain your surprise), and have that script call certain Keyboard Maestro macros.

First I would create 4 Keyboard Maestro macros, named something like this:

  1. “StreamDeck Trackpad Battery Good”
  2. “StreamDeck Trackpad Battery Low”
  3. “StreamDeck Keyboard Battery Good”
  4. “StreamDeck Keyboard Battery Low”

Each of those 4 macros should set the button image that you want in each of those cases.

You might also want two more:

  1. “StreamDeck Trackpad Battery Unknown”
  2. “StreamDeck Keyboard Battery Unknown”

for that “just in case” scenario if the script does not get the information that you are expecting.

In a shell script, you can trigger a Keyboard Maestro macro with this syntax:

osascript -e 'tell application "Keyboard Maestro Engine" to do script "StreamDeck Trackpad Battery Good"'

You can also use the UUID of the macro, which is better in that it won’t change, but worse in that it’s harder to know which is which. (If I use a macro by name in a script, I make sure to add a comment to the top of that macro in Keyboard Maestro which says “DO NOT RENAME!”)

Conditionals in shell scripts

Our old friends $KEYBOARD_BATTERY_PERCENT or $TRACKPAD_BATTERY_PERCENT will now come in handy because we can use them to check the battery percentage, and run different macros depending on their value. From your original macro I see that you wanted to change the icon if the mouse battery was 10% or less and keyboard battery is 15% or less.

That’s easy enough to script. I’m also going to check to make sure that the variables are not empty.

if [[ "$KEYBOARD_BATTERY_PERCENT" == "" ]]
then

	echo "$NAME: 'KEYBOARD_BATTERY_PERCENT' is empty" >>/dev/stderr

	osascript -e 'tell application "Keyboard Maestro Engine" to do script "StreamDeck Keyboard Battery Unknown"'

else

	# if we get here, we know it is not empty, so now we just need to know if it is
	# within the warning range.

	if [[ "$KEYBOARD_BATTERY_PERCENT" -le "15" ]]
	then
		osascript -e 'tell application "Keyboard Maestro Engine" to do script "StreamDeck Keyboard Battery Low"'
	else
		osascript -e 'tell application "Keyboard Maestro Engine" to do script "StreamDeck Keyboard Battery Good"'
	fi

fi

Same thing for the trackpad, except 10% not 15 (note that -le means “less than or equal”)

if [[ "$TRACKPAD_BATTERY_PERCENT" == "" ]]
then

	echo "$NAME: 'TRACKPAD_BATTERY_PERCENT' is empty" >>/dev/stderr

	osascript -e 'tell application "Keyboard Maestro Engine" to do script "StreamDeck Trackpad Battery Unknown"'

else

	# if we get here, we know it is not empty, so now we just need to know if it is
	# within the warning range.

	if [[ "$TRACKPAD_BATTERY_PERCENT" -le "10" ]]
	then
		osascript -e 'tell application "Keyboard Maestro Engine" to do script "StreamDeck Trackpad Battery Low"'
	else
		osascript -e 'tell application "Keyboard Maestro Engine" to do script "StreamDeck Trackpad Battery Good"'
	fi

fi

Whew! Well, that was a trip!

Now, I have to admit that the reason I wrote all of this out is because when I saw this idea, I knew that I wanted to use it for myself, so really you just got to watch me figure out how I’m going to implement this.

I’m also going to put the final script into a gist in Github so it will be easy to find and easy for others to fork if they wish. I will add that back here once I’m done.

Questions?

I realize that was probably a very different answer than you expected, and possibly different than you wanted, so let me know if you have questions.

1 Like

Thank you for such a detailed walkthrough of how you would do it! It's working great.

Time will tell whether the values stick for sure but I've tried altering the -le thresholds and so far so good. It seems way more reliable than before at least.