Apple Hardware Software

Script to display Mac battery information

You can see how this script makes that couple very happy.

Quick and dirty script that shows your Mac battery information (health, cycles, etc). If an Apple keyboard or mouse is connected, it’ll also display the battery % of those.

# Battery information
battery() {

	if !ioreg > /dev/null 2>&1; then
		echo "ioreg not found. Exiting."
		return 1

	_ioreg=`ioreg -l`
	_profile=`system_profiler SPPowerDataType`

	MOUSE=`echo $_ioreg -l | grep -A 10 "Mouse" | grep '"BatteryPercent" =' | sed 's/[^0-9]*//g'`
	TRACKPAD=`echo $_ioreg -l | grep -A 10 "Track" | grep '"BatteryPercent" =' | sed 's/[^0-9]*//g'`
	KEYBOARD=`echo $_ioreg -l | grep -A 10 "Keyboard" | grep '"BatteryPercent" =' | sed 's/[^0-9]*//g'`
	CYCLE=`echo $_profile | grep "Cycle Count" | awk '{print $3}'`

	if [ -n "$MOUSE" ]; then
		echo "Mouse: "$MOUSE"%"

	if [ -n "$TRACKPAD" ]; then
		echo "Trackpad: "$TRACKPAD"%"

	if [ -n "$KEYBOARD" ]; then
		echo "Keyboard: "$KEYBOARD"%"

	if [ -n "$CYCLE" ] && [ "$CYCLE" -ne 0 ]; then
		echo "Mac battery "`echo $_profile | grep "State of Charge" | awk '{print $5}'`"%"
		echo "Charging: "`echo $_profile | grep "Charging" | head -n 1 | awk '{print $2}'`
		echo "Cycles: "$CYCLE
		echo "Condition: "`echo $_profile | grep "Condition" | awk '{print $2}'`
		echo "Health: "`echo $_profile | grep "Maximum Capacity" | awk '{print $3}'`

Outputs something similar to this (no mouse or keyboard connected):

nazgul ~ $ battery
Mac battery 54%
Charging: No
Cycles: 224
Condition: Normal
Health: 89%

This works on zsh and may not work in bash.

Hardware Linux Networking Software

PoC: Betteruptime + Python-kasa

Content Update

The provided scripts have been updated on 16 Jul 2023. Specifically the SmartStrip part was not working as intended.

I’ve been a big fan of Betteruptime. I’ve started using it to monitor all my assets online (websites, DNS, ping, successful script runs) as well as my servers (using heartbeats).

Screenshot of Betteruptime showing a heartbeat that failed for several hours.
Image of a heartbeat that failed for several hours. After 2 hours of no hearbeat, it turned into an incident, and several hours later the heartbeats resumed.

I have a few Raspberry Pi’s, and once in a while they hang (not sure why, maybe USB-to-SSD issues or something). Nothing too critical, but annoying.

I’ve plugged them all on TP-Link Kasa smart plugs to remotely restart them if I had to (once or twice a year).

Note, to confuse everyone, TP-Link also launched Tapo, which... competes with Kasa and is not compatible, but does the exact same thing... ¯\_(ツ)_/¯

After some Googling (actually Kagi’ing) I found out, there’s a Python library that lets you control your smart plugs.

So, the idea was born to:

  • Check Betteruptime heartbeats, if down, power cycle the smart plug
  • Do this at most once per day (in case something else is causing issues)
  • Betteruptime heartbeats manage when a device is marked as offline (i.e.: it expects a heartbeat every 5 minutes, but will only consider the device down if no heartbeats are received for 2 hours).
  • The bulk of the code had to be written by ChatGPT. Let ChatGPT choose the language (it ended up being a mix of Bash and Python)
  • Everything needs to run in Docker (using a cron, the Docker container doesn’t daemonise)
  • These run on Raspberry Pi’s (but of course the RPi can’t check itself: so RPi1 checks for RPi2, and vice versa. As these RPis are on different networks (my parent’s home, my own home, etc) I had to enable “--net=host” in Docker run to get the correct routes from the host system, but you may not actually need this
  • To top if off, sent an email (using Mailgun EU servers) to warn me something broke and it rebooted

So, after some fiddling (half an evening or so) the proof-of-concept worked.

I should probably throw this in a Git repo but shrug. I don’t want to give the impression that I’ll maintain this and provide support.


FROM python:alpine
RUN apk add bash curl jq
RUN pip3 install python-kasa
VOLUME /tmp/kasa/
CMD ["/"]

Python script (this works with both smart strips and smart plugs):

import sys
import asyncio
from kasa import SmartPlug, SmartStrip

async def main():
	if len(sys.argv) != 4:
		print("Usage: python type IP-address outlet-index")

	device_type = sys.argv[1]
	ip_address = sys.argv[2]
	outlet_index = int(sys.argv[3])

	if device_type == "smartplug":
		await control_smart_plug(ip_address)
	elif device_type == "smartstrip":
		await control_smart_strip(ip_address, outlet_index)
		print(f"Unsupported device type: {device_type}")

async def control_smart_plug(ip_address):
	plug = SmartPlug(ip_address)

		await plug.update()

		# Retrieve the current state
		plug_state = plug.is_on

		# Turn off the plug
		await plug.turn_off()

		print(f"Turned off SmartPlug at {ip_address}")
		await asyncio.sleep(5)

		# Turn on the plug if it was previously on
		if plug_state:
			await plug.turn_on()

		print(f"Turned on SmartPlug at {ip_address}")
	except Exception as e:
		print(f"Failed to control SmartPlug at {ip_address}: {e}")

async def control_smart_strip(ip_address, outlet_index):
	strip = SmartStrip(ip_address)

		await strip.update()

		# Retrieve the current state of the specified child plug
		child_state = strip.children[outlet_index].is_on

		# Turn off the specified child plug
		await strip.children[outlet_index].turn_off()

		print(f"Turned off child plug {outlet_index} in SmartStrip at {ip_address}")
		await asyncio.sleep(5)

		# Turn on the child plug if it was previously on
		await strip.children[outlet_index].turn_on()

		print(f"Turned on child plug {outlet_index} in SmartStrip at {ip_address}")
	except Exception as e:
		print(f"Failed to control SmartStrip at {ip_address}: {e}")

# Run the asyncio event loop — with example devices. Be sure to fill in the variables (including hb, that’s the heartbeat ID you can get from the Betteruptime URL and the IP or DNS hostname of the smartplug):


API_KEY="BetterUptime API token"
BU="" # no need to change this

MAILGUN_API_KEY="Mailgun API token"
MAILGUN_DOMAIN="" # use your own domain

if [[ "$DEVICE" = tyr ]] || [[ "$1" = tyr ]]; then

	# Tyr

elif [[ "$DEVICE" = mammoth ]] || [[ "$1" = mammoth ]]; then

	# Mammoth
	plug_index=0 # plug 2 is rly plug 3 because the index counts from 0 to 2 and not from 1 to 3.

elif [[ "$DEVICE" = liana ]] || [[ "$1" = liana ]]; then

	# Liana
	plug_index=1 # plug 2 is rly plug 3 because the index counts from 0 to 2 and not from 1 to 3.

elif [[ "$DEVICE" = eagle ]] || [[ "$1" = eagle ]]; then

	plug_index=2 # plug 2 is rly plug 3 because the index counts from 0 to 2 and not from 1 to 3.

	echo "Unknown device."
	exit 1


send_alert() {
	from="[email protected]"
	to="[email protected]"
	subject="Smartplug power cycled: $device"
	body="rebooted device $device!"$'\n'"Kasa IP: $plug_host."$'\n'"$bu"

	# Send alert email
	curl -s --user "api:$MAILGUN_API_KEY" \
		-F from="$from" \
		-F to="$to" \
		-F subject="$subject" \
		-F text="$body"

kasa_cycle() {
	echo "Betteruptime heartbeat ($hb) says the service for $device is down, restarting."
	python / "$plug_type" "$plug_host" "$plug_index"
	# Update the last execution date in the file
	echo "$current_date" > "$file"

kasa_info() {
	kasa --host $plug_host

response=$(curl -sL "$url" -H "Authorization: Bearer $API_KEY")
status=$(echo "$response" | jq -r '.data.attributes.status')

if [[ "$status" == "down" ]]; then
	mkdir -p "$dir"

	# Get current date
	current_date=$(date "+%F")
	# Check if the file exists
	if [ -f "$file" ]; then
		# Get last execution date from the file
		last_execution=$(cat "$file")
		# We only want to run this once every 24hrs. If a reboot doesn't fix it, something more
		# serious is going on and likely needs manual intervention. No point spam rebooting the device.
		if [[ "$current_date" != "$last_execution" ]]; then
			echo "Power cycle already executed today."
elif [[ "$status" == "up" ]]; then
	echo "Betteruptime heartbeat says the service ($hb) for $device is up."
	# this could happen if the heartbeat is paused.
	echo "Unknown status."
	exit 1

I run Docker with two scripts, a builder ( and a file that runs it ( It should rebuild in case a docker cleanup script ran (and deleted dangling containers).

I run this as root and probably shouldn’t, but yeah… That’ll be for another lifetime.

Be sure to change the paths (/root/git/kasa-api) in both scripts.

cd /root/git/kasa-api # the path where this project exists

git pull > /dev/null

BASEIMAGE=`cat Dockerfile | grep FROM | awk '{print $2}'`
docker pull $BASEIMAGE
docker build -q -t kasa-api .
rm -f /tmp/.kasa/*.txt


if [ -z "$1" ]; then
	echo "Missing device name."
	exit 1

docker stop kasa-api 2> /dev/null
docker rm kasa-api 2> /dev/null

run_kasa() {
	docker run --net=host -v /tmp/.kasa:/tmp/.kasa --rm -e DEVICE=$DEVICE --name kasa-api kasa-api

if [[ $(docker image ls | grep kasa-api) ]]; then
	run_kasa $1
	cd /root/git/kasa-api
	/root/git/kasa-api/ > /dev/null
	run_kasa $1

And that’s pretty much it. I run this using with cron in /etc/cron.d/. For example (be sure to edit the parameter/device name/path):

# cron-jobs for kasa-api


*/15 * * * *	root	if [ -x /root/git/kasa-api/ ] && [ -f /root/git/kasa-api/ ]; then /root/git/kasa-api/ tyr >/dev/null; fi

I’m sure there must be bugs in this ChatGPT generated code but… so far, it has actually worked.

Games Hardware Software

Micro-mice and mazes

Via Kottke.

Truly wish we learned all this in school. Would’ve attended school significantly more often… 😉

Hardware Software

Flipper Zero: Waiting for SD card

Just got myself a Flipper Zero because .

However, the updating process from 0.6x.y to 0.82.3 was not very smooth sailing:

  • First it was stuck in synchronising on the iOS app refusing to initiate the update (had to force quit + reboot Flipper Zero to get it to continue)
  • After that, once I managed to upload the new firmware, it was stuck on Waiting for SD card after the Flipper rebooted.

The SD card I tried was an old 2Gb Samsung SD I had lying around (have 3-4 of these).

Tried several times, but alas, until I tried a different SD card… Then the update process went fine and the firmware updated as expected.

So looks like this is a sign the SD card is not properly performing (even though format, benchmark and uploading firmware worked fine).


OLED Lego Brick

Via Hackaday.