Software www

Check websites with LanguageTool for typos

This is quick and dirty (and with the help of ChatGPT).

FlatTurtle has a new site, and there’s been some fine-tuning here and there that led to a few typos creeping in. I wanted a quick tool to plug in a page, and that would highlight possible mistakes.

I’ve been a personal (paying) user of LanguageTool for a few years now (European, and less spammy and dodgy than Grammarly)

Started off with a terminal tool, but in the end that wasn’t working out (hard to get the colouring to work and make it clear enough).

Figured a website would be easier:

  • Insert a site
  • Let it go through the LanguageTool API for mistakes*
  • Show what is potentially wrong and explain why so I can go and edit it

(*) Surprisingly hard because it needs to trim all HTML and js and other crap. And it has issues detecting headers (without punctuation) from paragraph text, etc).

It’s far from perfect, but it works well enough for half a day of fiddling around.

You can hover your mouse over the red words to get some information as to why something is wrong.

The code, provided as-is, is here, and you can run it using:

python3 -m pip install flask selenium beautifulsoup4 geckodriver-autoinstaller requests
python3 --api-key KEY --username EMAIL

And opening http://localhost:5000.

EMAIL is your login, the KEY can be found here.

Have fun.

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.

Apple Networking Software


Is your company forcing Cloudflare Warp on you, and are you running on a Mac (with sudo access)?

It probably sucks, spies on you, does MitM attacks, breaks most video conferencing tools, and is generally not very stable… Also… Zero trust!

Add this function to your .bashrc or .zshrc (whichever shell you’re using*):

func killwarp() {
	sudo launchctl remove com.cloudflare.1dot1dot1dot1.macos.warp.daemon
	sudo killall Cloudflare\ WARP

Open a new shell window (to reload your dot files), and type killwarp.

This will permanently disable Warp (until your Mac is rebooted; as it’s most likely force installed/started by your admin). So just run this at every reboot.

(*) Find out with echo $SHELL.


Don’t trust corporates

Especially those at the pinnacle that’ve lost touch with their customers.

I’ve blogged about it before.

Here is how platforms die: first, they are good to their users; then they abuse their users to make things better for their business customers; finally, they abuse those business customers to claw back all the value for themselves. Then, they die.

Source: Pluralistic

I’ve just learned that Amazon Kindle killed the book loaning feature… Something they initially used as a selling point when I got my first Kindle in 2017 (or whenever it was).

I’ve spent about an hour on the phone with 4 (!) Amazon support reps to find a solution (tl;dr: there are none, but they recommend one of the following three options: 1/ share your Amazon account/password with whoever you want to loan your book to, 2/ add the person to your household (never mind you can only have 2 adults in your household), 3/ buy the books again).

As I told them none of these options realistically worked (and are not a solution to the problem) I asked for a gift voucher (to re-purchase the books I wanted to loan to a friend) which they initially said were unable to do, and eventually admitted were able to do, but refused to do because they felt they offered workable solutions: “you can just purchase them again”. Sigh.

It is a disheartening reality that companies often take a turn towards evil once they hit a certain size. Lose touch with reality.

The initial promises of exceptional service and genuine customer care gradually fade away. Instead, the focus shifts to maximizing profits, leaving customer satisfaction behind. Shareholders exert pressure, prioritizing returns on investment over the long-term relationship with customers.

Don’t trust corporates.

We’ve turned the world into a place where we don’t actually own anything. SaaS offers ease of access (streaming, cloud, etc), but if consumers are at the whims of corporates to turn features on and off, give or take away access, something is wrong.