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:
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
fi
_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"%"
fi
if [ -n "$TRACKPAD" ]; then
echo "Trackpad: "$TRACKPAD"%"
fi
if [ -n "$KEYBOARD" ]; then
echo "Keyboard: "$KEYBOARD"%"
fi
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}'`
fi
}
Outputs something similar to this (no mouse or keyboard connected):
nazgul ~ $ battery
Mac battery 54%
Charging: No
Cycles: 224
Condition: Normal
Health: 89%
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).
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... ¯\_(ツ)_/¯
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.
Dockerfile:
FROM python:alpine
RUN apk add bash curl jq
RUN pip3 install python-kasa
COPY heartbeat.sh kasa-api.py /
VOLUME /tmp/kasa/
CMD ["/heartbeat.sh"]
import sys
import asyncio
from kasa import SmartPlug, SmartStrip
async def main():
if len(sys.argv) != 4:
print("Usage: python kasa-api.py type IP-address outlet-index")
return
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)
else:
print(f"Unsupported device type: {device_type}")
async def control_smart_plug(ip_address):
plug = SmartPlug(ip_address)
try:
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)
try:
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
asyncio.run(main())
heartbeat.sh — 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):
#!/bin/bash
API_KEY="BetterUptime API token"
BU="https://uptime.betterstack.com/api/v2/heartbeats/" # no need to change this
MAILGUN_API_KEY="Mailgun API token"
MAILGUN_DOMAIN="mg.you.com" # use your own domain
if [[ "$DEVICE" = tyr ]] || [[ "$1" = tyr ]]; then
# Tyr
device="Tyr"
hb=1111
bu="https://uptime.betterstack.com/team/1/heartbeats/$hb"
plug_type="smartplug"
plug_host="smartplug1.kasa.you.com"
elif [[ "$DEVICE" = mammoth ]] || [[ "$1" = mammoth ]]; then
# Mammoth
device="mammoth"
hb=2222
bu="https://uptime.betterstack.com/team/1/heartbeats/$hb"
plug_type="smartstrip"
plug_host="smartstrip1.kasa.you.com"
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
device="liana"
hb=3333
bu="https://uptime.betterstack.com/team/1/heartbeats/$hb"
plug_type="smartstrip"
plug_host="smartstrip1.kasa.you.com"
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
device="eagle"
hb=4444
bu="https://uptime.betterstack.com/team/1/heartbeats/$hb"
plug_type="smartstrip"
plug_host="smartstrip1.kasa.yeri.be"
plug_index=2 # plug 2 is rly plug 3 because the index counts from 0 to 2 and not from 1 to 3.
else
echo "Unknown device."
exit 1
fi
url=$BU/$hb
send_alert() {
MAILGUN_URL="https://api.eu.mailgun.net/v3/$MAILGUN_DOMAIN/messages"
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" \
"$MAILGUN_URL" \
-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 /kasa-api.py "$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
dir="/tmp/.kasa/"
mkdir -p "$dir"
file="${dir}${device}.txt"
# 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
kasa_cycle
send_alert
else
echo "Power cycle already executed today."
fi
else
kasa_cycle
send_alert
fi
elif [[ "$status" == "up" ]]; then
echo "Betteruptime heartbeat says the service ($hb) for $device is up."
else
# this could happen if the heartbeat is paused.
echo "Unknown status."
kasa_info
exit 1
fi
I run Docker with two scripts, a builder (rebuild.sh) and a file that runs it (start.sh). 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.
rebuild.sh:
#!/bin/bash
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
start.sh:
#!/bin/bash
if [ -z "$1" ]; then
echo "Missing device name."
exit 1
fi
docker stop kasa-api 2> /dev/null
docker rm kasa-api 2> /dev/null
run_kasa() {
DEVICE=$1
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
else
cd /root/git/kasa-api
/root/git/kasa-api/rebuild.sh > /dev/null
run_kasa $1
fi
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
#
MAILTO=root
*/15 * * * * root if [ -x /root/git/kasa-api/start.sh ] && [ -f /root/git/kasa-api/start.sh ]; then /root/git/kasa-api/start.sh tyr >/dev/null; fi
I’m sure there must be bugs in this ChatGPT generated code but… so far, it has actually worked.
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.
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.
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.