notes by alifeeeprofile picture tagged scripting (23)rss

return to notes / blog / website / weeknotes / linktree

here I may post some short, text-only notes, mostly about programming. source code.

tags: all (58) scripting (23) linux (6) jq (5) android (4) bash (4) geojson (4) obsidian (4) github (3) html (3) ............ see all (+97)

guestbook!

show all /
sign book

assembling the PiBow Mini, a small Macro pad#prevsinglenexttop

2025-09-14 • tags: hardware, keyboards, raspberry-pi, scripting • 618 'words', 3.1 mins @ 200wpm

What is it

I picked up a Keybow Mini from Sheffield Hackspace a few weeks ago, which is a small 3-key mechanical keyboard or "macro pad".

It had been donated to the hackspace by Pimoroni; I also picked up a donated Pi Zero and a lying 1024GB SD card I'd picked up a few weeks ago, and assembled it.

Assembly

Unfortunately, this is a text only-blog. I assure you that the pictures of me putting keycaps and circuitboards together are incredibly riveting — but you'll just have to imagine.

end result

After following the assembly guide and the creating macros guide, I had a little 3-key keyboard which could increase, decrease, or mute my volume (via media keys). I wanted to have more options, so I wrote a little layout in Lua in which the left button switches "layout", and the other two buttons do different actions based on which layout is currently enabled. The keys I started with are:

…where the latter two groups are from the 'expanded function key set'.

AutoKey

It works well! I can use the function keys to run macros with AutoKey. For example, here's a script which prints the current date:

output = system.exec_command("date '+%Y-%m-%d'")
keyboard.send_keys(output)

Firmware

Here's the Lua script for what I described. I think it's pretty readable.

require "keybow"

-- do loads of things --
--   left button switches between mode
--   do not use F13 as by default it opens settings
-- red: media controller
-- blue: send F15 and F16
-- green: send F17 and F18

Kbsetup = 0
TotKbSetups = 3

function setup()
    keybow.use_mini()
    keybow.auto_lights(false)
    keybow.clear_lights()
    keybow.set_pixel(0, 255, 0, 0)
    keybow.set_pixel(1, 255, 0, 0)
    keybow.set_pixel(2, 255, 0, 0)
end

-- left key
function handle_minikey_02(pressed)
    if (pressed) then
        Kbsetup = (Kbsetup + 1) % TotKbSetups
    end
    if (Kbsetup == 0) then
        -- print("setup 1")
        keybow.set_pixel(0, 255, 0, 0)
        keybow.set_pixel(1, 255, 0, 0)
        keybow.set_pixel(2, 255, 0, 0)
    elseif (Kbsetup == 1) then
        -- print("setup 2")
        keybow.set_pixel(0, 0, 255, 0)
        keybow.set_pixel(1, 0, 255, 0)
        keybow.set_pixel(2, 0, 255, 0)
    elseif (Kbsetup == 2) then
        -- print("setup 3")
        keybow.set_pixel(0, 0, 0, 255)
        keybow.set_pixel(1, 0, 0, 255)
        keybow.set_pixel(2, 0, 0, 255)
    end
end

-- middle key
function handle_minikey_01(pressed)
    if Kbsetup == 0 then
        keybow.set_media_key(keybow.MEDIA_VOL_DOWN, pressed)
    elseif Kbsetup == 1 then
        keybow.set_key(keybow.F15, pressed)
    elseif Kbsetup == 2 then
        keybow.set_key(keybow.F17, pressed)
    end
end

-- right key
function handle_minikey_00(pressed)
    if Kbsetup == 0 then
        keybow.set_media_key(keybow.MEDIA_VOL_UP, pressed)
    elseif Kbsetup == 1 then
        keybow.set_key(keybow.F16, pressed)
    elseif Kbsetup == 2 then
        keybow.set_key(keybow.F17, pressed)
    end
end

Final notes

The only annoying thing is that to reprogram the PiBow Mini, you've got to unplug it, remove the SD card, plug it into a laptop (in my case as only my laptop has an SD reader), copy the new Lua files over, unplug the SD card, plug it in, test the new code, and repeat if it doesn't work as intended.

That's why I opted to send function keys, which I can turn into hotkeys using AutoKey, then I only have to change a Python script, and not redo a whole device firmware.

But, it's pretty neat!

back to top

parsing ical files using awk#prevsinglenexttop

2025-08-24 • tags: ics, awk, jq, scripting • 450 'words', 135 secs @ 200wpm

I've been interested in "doing something" with the ical format for a while, as it's quite a simple format, but can be shared easily to create events in other people's calendars.

Here is an example of an ical event, from the wikipedia page

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
BEGIN:VEVENT
UID:uid1@example.com
ORGANIZER;CN=John Doe:MAILTO:john.doe@example.com
DTSTAMP:19970701T100000Z
DTSTART:19970714T170000Z
DTEND:19970715T040000Z
SUMMARY:Bastille Day Party
GEO:48.85299;2.36885
END:VEVENT
END:VCALENDAR

As you can see, it's fairly human-readable. You can see a description of the event as SUMMARY, and a start/end date-time as DTSTART/DTEND. You can imagine writing a file like this manually (though probably don't start doing that).

As for parsing them, you can use awk pretty well. Create a file called parse_ics.awk a bit like this:

# run with:
#   awk -F':' -f parse_ics.awk
{sub("\r$", "")} # remove terrible line endings
$1=="UID"{UID=$2}
$1=="DTSTART"{DTSTART=$2}
$1=="DTEND"{DTEND=$2}
$1=="SUMMARY"{SUMMARY=$2}
$1=="GEO"{GEO=$2}
$1=="END" && $2=="VEVENT"{
  # parsing date format to timestamp
  datespec_fmt = "\\1 \\2 \\3 \\4 \\5 \\6"
  start_ts = mktime(gensub(/(....)(..)(..)T(..)(..)(..)Z/, datespec_fmt, 1, DTSTART))
  end_ts = mktime(gensub(/(....)(..)(..)T(..)(..)(..)Z/, datespec_fmt, 1, DTEND))
  # output
  printf "%s\n", SUMMARY
  printf "  %s – %s\n", strftime("%a %b %d %Y @ %H:%M", start_ts), strftime("%a %b %d %Y @ %H:%M", end_ts)
}

Save the example event above as event.ics and you can parse it with:

$ cat event.ics | awk -F':' -f parse_ics.awk
Bastille Day Party
  Mon Jul 14 1997 @ 17:00 – Tue Jul 15 1997 @ 04:00

If you want to do something more… machiney… you could change the output to be more machine-readable (i.e., JSON) by changing the printing section in awk, e.g., to…

printf "{"
printf "\"SUMMARY\": \"%s\",", SUMMARY
printf "\"START_TS\": %i,", start_ts
printf "\"END_TS\": %s", end_ts
printf "}"

…which you can then use in jq or elsewhere that uses json:

$ cat event.ics | awk -F':' -f parse_ics.awk | jq
{
  "SUMMARY": "Bastille Day Party",
  "START_TS": 868896000,
  "END_TS": 868935600
}

So… if someone publishes their event as an .ics file… you can do some nice scripting with it!

For example, I created a script which parses the Sheffield United football fixtures and sends a message on Discord whenver there is one coming up later in the day (as Sheffield Hackspace is near the football ground so it affects parking for car-brains).

back to top

jumping to column N in Libreoffice Calc with AutoKey#prevsinglenexttop

2025-08-19 • tags: libreoffice, calc, shortcuts, hotkeys, scripting • 281 'words', 84 secs @ 200wpm

I am a director of Sheffield Hackspace and part of my role is to keep track of the membership.

It's currently via a spreadsheet where the membership payments come via bank transactions, and I match them up to the sheet. This involves a lot of finding names with "CTRL+F", and then changing a specific column.

I'd like to be able to "jump to column N", but I couldn't find an appropriate shortcut (I would love something like "Alt" then press "N", but alas).

After some web-searching, I found AutoKey, a Linux program that sounds like a colder version of AutoHotKey. I installed it, and it worked great. I'd tried to use xdotool to send key commands before, but it didn't work when set as a keyboard shortcut.

What I wanted to do using the keyboard was:

After reading the the documentation, I figured a script to do this:

import time
keyboard.send_keys("<ctrl>+<shift>+T")
time.sleep(0.25)
keyboard.send_keys("<left>")
keyboard.send_keys("<shift>+N")
keyboard.send_key("<delete>")
keyboard.send_key("<enter>")

(the 0.25 second wait is to let LibreOffice Calc recognise the shortcut and move the cursor to the Name Box)

…and used the GUI to set the script to run whenever I typed "Ctrl+F8".

It worked great :]

I see myself using this program more into the future.

back to top

using a single curl request to get headers and content at the same time#prevsinglenexttop

2025-07-15 • tags: scripting, curl • 283 'words', 85 secs @ 200wpm

I often make curl requests. I often want to see the HTTP headers and also the content.

Most of the time, you can just add -i or --include to include the headers in the printout

curl -i https://alifeee.co.uk/

In writing this (and looking at man curl), you can also use -v or --verbose to view the headers.

But, if you want to download both the headers and the content in one request (e.g., for a particularly large file or a server you don't want to hammer), you can use a script like this:

# customisation
url="https://alifeee.co.uk/"
file="alifeee.html"
filenom="${file%.*}"
fileext="${file#*.}"

# request
curl -i -o "${filepath}" "${url}"

# get index of first blank line
blank=$(cat "${filepath}" | awk '/^\r?$/ {print NR; exit}')
# cut file up to first blank line
head -n "$(($blank - 1))" "${filepath}" > "${filenom}_HEADERS.txt"
# cut file after first blank line
tail -n "+$(($blank + 1))" "${filepath}" > "${filenom}_RESPONSE.${fileext}"

…and view the results like this…

$ ll
total 108K
-rw-rw-r--  1 alifeee alifeee  620 Jul 15 16:27 alifeee_HEADERS.txt
-rw-rw-r--  1 alifeee alifeee  27K Jul 15 16:27 alifeee.html
-rw-rw-r--  1 alifeee alifeee  26K Jul 15 16:27 alifeee_RESPONSE.html

$ cat "${filenom}_HEADERS.txt"
HTTP/2 200
server: GitHub.com
content-type: text/html; charset=utf-8
last-modified: Wed, 25 Jun 2025 11:17:22 GMT
access-control-allow-origin: *
etag: "685bdac2-66b0"
expires: Tue, 15 Jul 2025 13:26:55 GMT
cache-control: max-age=600
age: 96
date: Tue, 15 Jul 2025 15:27:43 GMT

$ file "${filenom}_RESPONSE.${fileext}"
alifeee_RESPONSE.html: HTML document, Unicode text, UTF-8 text
back to top

automating the turning on and off of my Minecraft server#prevsinglenexttop

2025-06-10 • tags: minecraft, scripting, tmux, cron, nginx • 1112 'words', 5.6 mins @ 200wpm

I run a Minecraft server weekly on Tuesdays. Sometimes, I even play on it.

This describes automating the process for turning it on and off. Won't somebody at https://ggservers.com/ please hire me /jk.

The process

Turning it on

My process every Tuesday to turn on the server has been:

Turning it off

Then, on Wednesday mornings (if I remember), I:

The problems

Each of these steps can take a few seconds to run, so I am often multitasking, and I often forget things (like forgetting the backup, forgetting to actually run shutdown after all is done).

So, I've tried to automate it.

Doing it automatically

I found out that Kamatera (the server host) has an API that you can use to remotely turn on/off servers, which is the only thing that I was really missing.

cron tasks - web server

Here are the cron tasks on my web server:

# turn Minecraft server server on/off
45 16 * * 2 /home/alifeee/minecraft/togglepower.sh on >> /home/alifeee/minecraft/cron.log 2>&1
5 4 * * 3 /home/alifeee/minecraft/rsync_backup.sh on >> /home/alifeee/minecraft/cron.log 2>&1
15 4 * * 3 /home/alifeee/minecraft/togglepower.sh off >> /home/alifeee/minecraft/cron.log 2>&1

cron tasks - minecraft box

…and the cron tasks on the minecraft box:

55 16 * * 2 /home/alifeee/minecraft/tmux_make.sh >> /home/alifeee/minecraft/cron.log 2>&1
0 4 * * 2 /home/alifeee/minecraft/tmux_kill.sh >> /home/alifeee/minecraft/cron.log 2>&1

human description of cron jobs

Hopefully you can see the similarities to the process I described above, i.e.,

The scripts

These scripts are pretty simple, they are:

togglepower.sh - turn on/off the minecraft box

$ cat togglepower.sh
#!/bin/bash
# power on server
date
onoroff="${1}"
echo "got instruction: turn server <${onoroff}>"
if [[ ! "${onoroff}" == "on" ]] && [[ ! "${onoroff}" == "off" ]]; then
  echo "usage: ./togglepower.sh [on|off]"
  exit 1
fi
serverid="${serverid}"
auth=$(curl -s --request POST 'https://console.kamatera.com/service/authenticate' \
--header 'Content-Type: application/json' \
--data '{
    "clientId": "${clientId}",
    "secret": "${secret}"
}')
authentication=$(echo "${auth}" | jq -r '.authentication')
status=$(curl -s --request \
  GET "https://console.kamatera.com/service/server/${serverid}" \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer ${authentication}"
)
power=$(echo "${status}" | jq -r '.power')
echo "current power: ${power}"
if [[ "${power}" == "${onoroff}" ]]; then
  echo "power is already ${onoroff}… quitting…"
  exit 1
fi
result=$(curl -s --request PUT \
  "https://console.kamatera.com/service/server/${serverid}/power" \
  --header 'Content-Type: application/json' \
  -H "Authorization: Bearer ${authentication}" \
  --data '{"power": "'"${onoroff}"'"}'
)
echo "complete! got ${result} from API call"

run - run the Minecraft server

$ cat ./run 
#!/bin/bash
java \
  -Xmx1G \
  -jar fabric-server-mc.1.21.4-loader.0.16.10-launcher.1.0.1.jar \
  nogui

tmux_make.sh - make a tmux session and run the Minecraft server in it

$ cat tmux_make.sh 
#!/bin/bash
date
session="minecraft"
echo "making tmux session ${session}"
tmux new-session -d -s "${session}" -c "/home/alifeee/minecraft"
echo "sending run"
tmux send-keys -t "${session}" './run' 'C-m'
echo "created !"

tmux_kill.sh - stop the Minecraft server and stop the tmux session

$ cat tmux_kill.sh 
#!/bin/bash
date
session="minecraft"
echo "sending CTRL+C to ${session}"
tmux send-keys -t "${session}" 'C-c'
echo "sent CTRL+C… sleeping 30s…"
sleep 30
echo "killing session ${session}"
tmux kill-session -t "${session}"
echo "killed session"

rsync_backup.sh - get backups using rsync

$ cat rsync_backup.sh 
#!/bin/bash
date
echo "saving cron log"
rsync minecraft:/usr/alifeee/minecraft/cron.log cron_minecraft.log
date
echo "saving world"
rsync -r minecraft:/usr/alifeee/minecraft/world/ world/
date
echo "saving dynmap"
rsync -r minecraft:/usr/alifeee/minecraft/dynmap/web/ dynmap/web/
date
echo "done!"

What about the map?

Well, I figured this was too annoying to automate, so I just wrote a front page to pick whether you wanted the "dead map" or the "live map" (on https://map.mc.alifeee.net/ – link probably dead).

The HTML for this simple picker makes quite a nice page:

see HTML
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<head>
<style>
html, body {
  background: black;
  color: white;
  height: 100%;
  font-family: sans-serif;
}
body {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
img {
  margin-bottom: 1rem;
}
.options {
  display: flex;
}
.option {
  background: orange;
  padding: 1rem;
  border-radius: 0.5rem;
  margin: 0.5rem;
  color: black;
  text-decoration: none;
  max-width: 10rem;
  text-align: center;
}
.option.two {
  background: purple;
  color: white;
}
.option span {
  opacity: 0.75;
}
</style>
</head>

<body>

<h1>minecraft dynmap</h1>

<img src="/map/tiles/world/flat/0_0/zz_16_4.webp" />

<section class="options">
  <a class="option one" href="/map/">
    <h2>dead map</h2>
    <span>viewable all week, updates on server shutdown</span>
  </a>
  <a class="option two" href="https://livemap.mc.alifeee.net/">
    <h2>live map</h2>
    <span>viewable only when the server is live, shows players</span>
  </a>
</section>
</body>
</html>

This is served with a special nginx configuration which just serves a static file, and otherwise serves content via alias (not root):

server {
    server_name map.mc.alifeee.net;
location / {
        root /var/www/dynmap/;
        try_files /whichmap.html =404;
    }
    location /map/ {
        alias /var/www/dynmap/;
        try_files $uri $uri/ =404;
    }
}

Does it work

I think it works. I'll see if I have to make any edits tomorrow or next week.

I love scripting !

back to top

creating a desktop overlay to view players on a Minecraft server with conky#prevsinglenexttop

2025-06-03 • tags: conky, minecraft, scripting, overlay • 1049 'words', 5.2 mins @ 200wpm

Currently, I'm hosting a Minecraft server weekly on Tuesdays. Sometimes I even play.

It's Vanilla with a proximity voice chat mod (walk near people to hear them). Proximity voice chat is endlessly fun (see Barotrauma, Factorio, et cetera…)

Today, I wanted to have an overlay (think Discord voice chat overlay, or when you pop-out a video in Firefox, or when you use chat heads on mobile) which showed me who was online on the server.

Querying the Minecraft server status

After seeing an "enable status" option in the server's server.properties file, and searching up what it meant (it allows services to "query the status of the server), I'd used https://mcsrvstat.us/ before to check the status of the server, which shows you the player list in a browser.

But a local overlay would need a local way to query the server status. So I did some web searching, found a Python script which wasn't great (and written for Python 2), then a self-hostable server status API, which led me to mcstatus, a Python API (with command line tool) for fetching server status.

I installed and tested it with

$ cd ~/temp/minecraft/
$ python3 -m venv env
$ ./env/bin/python -m mcstatus $SERVER_IP json
{"online": true, "kind": "Java", "status": {"players": {"online": 7, "max": 69, "sample": [{"name": "Boldwolf5491", "id": "289qfhj8-a8f2-298g-19ga-897ahwf8uwa8"}, {"name": "……………

Neat!

How to make an overlay on Linux

Next, a way of having an overlay. Searching for "linux x simple text overlay" led me to xmessage, which can show simple windows, but they're more like confirmation windows, not like long-lasting status windows (i.e., it's hard to update the text).

I was also led to discover conky, which – if nothing else – has a great name. It's designed to be a "system monitor", i.e., a thing wot shows you your CPU temperature, uptime, RAM usage, et cetera. The configuration is also written in Lua, which is super neat! I still want to get more into Lua.

Using conky

By modifying the default configuration (in /etc/conky/conky.conf) like so:

diff --git a/etc/conky/conky.conf b/.config/conky/conky.conf
index 44053d5..cc319e1 100644
--- a/etc/conky/conky.conf
+++ b/.config/conky/conky.conf
@@ -37,8 +37,9 @@ conky.config = {
     out_to_stderr = false,
     out_to_x = true,
     own_window = true,
+    own_window_title = 'Minecraft',
     own_window_class = 'Conky',
-    own_window_type = 'desktop',
+    own_window_type = 'normal', -- or desktop
     show_graph_range = false,
     show_graph_scale = false,
     stippled_borders = 0,
@@ -48,25 +49,9 @@ conky.config = {
     use_xft = true,
 }
 
 conky.text = [[
-${color grey}Info:$color ${scroll 32 Conky $conky_version - $sysname $nodename $kernel $machine}
-$hr
-${color grey}Uptime:$color $uptime
-${color grey}Frequency (in MHz):$color $freq
-${color grey}Frequency (in GHz):$color $freq_g
-${color grey}RAM Usage:$color $mem/$memmax - $memperc% ${membar 4}
-${color grey}Swap Usage:$color $swap/$swapmax - $swapperc% ${swapbar 4}
-${color grey}CPU Usage:$color $cpu% ${cpubar 4}
-${color grey}Processes:$color $processes  ${color grey}Running:$color $running_processes
-$hr
-${color grey}File systems:
- / $color${fs_used /}/${fs_size /} ${fs_bar 6 /}
-${color grey}Networking:
-Up:$color ${upspeed} ${color grey} - Down:$color ${downspeed}
-$hr
-${color grey}Name              PID     CPU%   MEM%
-${color lightgrey} ${top name 1} ${top pid 1} ${top cpu 1} ${top mem 1}
-${color lightgrey} ${top name 2} ${top pid 2} ${top cpu 2} ${top mem 2}
-${color lightgrey} ${top name 3} ${top pid 3} ${top cpu 3} ${top mem 3}
-${color lightgrey} ${top name 4} ${top pid 4} ${top cpu 4} ${top mem 4}
+${execpi 5 ~/temp/minecraft/check.sh}
 ]]

…when we run conky it opens a small window which contains the output of the script ~/temp/minecraft/check.sh (the 5 after execpi means it runs every 5 seconds). If this script was just echo "hi!" then that conky window looks a bit like:

 ———————+x
 |       |
 |  hi!  |
 |_______|

I use Pop!_OS, which uses Gnome/X for all the windows. With that (by default), I can right click the top bar of a window and click "Always on Top", which effectively makes the little window into an overlay, as it always displays on top of other windows, with the added bonus that I can easily drag it around.

Writing a script for conky to use

Now, I can change the script to use the above Minecraft server status JSON information to output something which conky can use as an input, like:

#!/bin/bash
#~/temp/minecraft/check.sh
json=$(~/temp/minecraft/env/bin/python -m mcstatus $SERVER_IP json)
online=$(echo "${json}" | jq -r '.status.players.online')
players=$(echo "${json}" | jq -r '.status.players.sample[] | .name')

echo '${color aaaa99}'"${online} players online"'${color}'
echo "---"
echo "${players}" \
  | sort \
  | awk '
  BEGIN{
    for(n=0;n<256;n++)ord[sprintf("%c",n)]=n
  }{
    r=0; g=0; b=0;
    split($0, arr, "")
    for (i in arr) {c=arr[i]; n=ord[c]; r+=n*11; g+=n*15; b+=n*21}
    printf "${color %X%X%X}%s\n",
      r%128+128, g%128+128, b%128+128, $0
  }
'

The fancy awk is just to make each player be a different colour, and to randomly generate the colours from the ASCII values of the player's username.

The final output

The final output looks like:

 ——————————————————+x
 | 8 players online |
 | ---              |
 | Kick_Flip_Barry  |
 | Blue_Outburst    |
 | Kboy8082         |
 | lele2102         |
 | Compostmelon101  |
 | Nobody808        |
 | Kaithefrog       |
 | BrinnanTheThird  |
 |__________________|

…which I can drag anywhere on my screen. When people join or leave the server, I can see a flash of change out of the corner of my eye.

Conclusions

Is this useful? Should I – instead – just have been playing the game? Do I use too many en-dashes? The world only knows.

Maybe I'll use conky for something else in future… I like to wonder what it could do…

back to top

getting my wifi name and password from the terminal#prevsinglenexttop

2025-05-25 • tags: scripting, wifi, aliases • 265 'words', 80 secs @ 200wpm

I often want to get my current WiFi name (SSID) and password.

How to get name/password manually

Sometimes, it's for a microcontroller. Sometimes, to share it. This time, it's for setting up an info-beamer device with WiFi.

Before today, I would usually open my phone and go to "share" under the WiFi settings, and copy the password manually, and also copy the SSID manually.

It's finally time to write a way to do it with bash!

How to get name/password with bash

After some web-searching, these commands do what I want:

alias wifi=iwgetid -r
alias wifipw=sudo cat "/etc/NetworkManager/system-connections/$(wifi).nmconnection" | pcregrep -o1 "^psk=(.*)"

How to use

…and I can use them like:

$ wifi
the wood raft (2.4G)
$ wifipw
[sudo] password for alifeee: 
**************

Neat!

Using Atuin aliases

Finally, above I suggested I was using Bash aliases, but I actually created them using Atuin, specifically Atuin dotfile aliases, like:

atuin dotfiles alias set wifi 'iwgetid -r'
atuin dotfiles alias set wifipw 'sudo cat "/etc/NetworkManager/system-connections/$(wifi).nmconnection" | pcregrep -o1 "^psk=(.*)"'

Now, they will automatically be enabled on all my computers that use Atuin. This is actually not… amazingly helpful as my other computers all use ethernet, not WiFi, but… it's mainly about having the aliases all in the same place (and "backed up", if you will).

back to top

Getting hackspace Mastodon instances from SpaceAPI#prevsinglenexttop

2025-05-22 • tags: scripting, spaceapi, mastodon, hackspaces, json • 622 'words', 3.1 mins @ 200wpm

We're back on the SpaceAPI grind.

This time, I wanted to see what Mastodon instances different hackspaces used.

The "contact" field in SpaceAPI

SpaceAPI has a "contact" object, which is used for this kind of thing. For example, for Sheffield Hackspace, this is:

$ curl -s "https://www.sheffieldhackspace.org.uk/spaceapi.json" | jq '.contact'
{
  "email": "trustees@sheffieldhackspace.org.uk",
  "twitter": "@shhmakers",
  "facebook": "SHHMakers"
}

Downloading all the SpaceAPI files

Once again, I start by downloading the JSON files, so that (in theory) I can make only one request to each SpaceAPI endpoint, and then work with the data locally (instead of requesting the JSON from the web every time I interact with it).

This script is modified from last time I did it, adding some better feedback of why some endpoints fail.

# download spaces
tot=0; got=0
echo "code,url" > failed.txt
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m'
while read double; do
  tot=$(($tot+1))
  name=$(echo "${double}" | awk -F';' '{print $1}');
  url=$(echo "${double}" | awk -F';' '{print $2}');
  fn=$(echo "${name}" | sed 's+/+-+g')
  echo "saving '${name}' - <${url}> to ./spaces/${fn}.json";

  # skip unless manually deleted  
  if [ -f "./spaces/${fn}.json" ]; then
    echo -e "  ${YELLOW}already saved${NC} this URL!" >> /dev/stderr
    got=$(($got+1))
    continue
  fi
  
  # get, skipping if HTTP status >= 400
  code=$(curl -L -s --fail --max-time 5 -o "./spaces/${fn}.json" --write-out "%{http_code}" "${url}")
  if [[ "${?}" -ne 0 ]] || [[ "${code}" -ne 200 ]]; then
    echo "${code},${url}" >> failed.txt
    echo -e "  ${RED}bad${NC} status code (${code}) for this url!"  >> /dev/stderr
    continue
  fi
  
  echo -e "  ${GREEN}fetched${NC}! maybe it's bad :S" >> /dev/stderr
  got=$(($got+1))
done <<<$(cat directory.json | jq -r 'to_entries | .[] | (.key + ";" + .value)')
echo "done, got ${got} of ${tot} files, $(($tot-$got)) failed with HTTP status >= 400"
echo "codes from failed.txt:"
cat failed.txt | awk -F',' 'NR>1{a[$1]+=1} END{printf "  "; for (i in a) {printf "%s (%i) ", i, a[i]}; printf "\n"}'

# some JSON files are malformed (i.e., not JSON) - just remove them
rem=0
for file in spaces/*.json; do
  cat "${file}" | jq > /dev/null
  if [[ "${?}" -ne 0 ]]; then
    echo "=== ${file} does not parse as JSON... removing it... ==="
    rm -v "${file}"
    rem=$(( $rem + 1 ))
  fi
done
echo "removed ${rem} malformed json files"

Extracting contact information

This is basically copied from last time I did it, changing membership_plans? to contact?, and changing the jq format afterwards.

# parse contact info
for file in spaces/*.json; do
  plans=$(cat "${file}" | jq '.contact?')
  [[ "${plans}" == "null" ]] && continue
  echo "${file}"
  echo "${plans}" | jq -r 'to_entries | .[] | (.key + ": " + (.value|tostring) )'
  echo ""
done > contact.txt

It outputs something like:

$ cat contact.txt | tail -n20 | head -n13
spaces/Westwoodlabs.json
twitter: @Westwoodlabs
irc: ircs://irc.hackint.org:6697/westwoodlabs
email: vorstand@westwoodlabs.de

spaces/xHain.json
phone: +493057714272
email: info@x-hain.de
matrix: #general:x-hain.de
mastodon: @xHain_hackspace@chaos.social

spaces/Zeus WPI.json
email: bestuur@zeus.ugent.be

Calculating Mastodon averages

We can filter this file to only the "mastodon:" lines, and then extract the server with a funky regex, and get a list of which instances are most common.

$ cat contact.txt | grep '^[^:]*mastodon' | pcregrep -o1 '([^:\.@\/]*\.[^\/@]*).*' | sort | uniq -c | sort -n
      1 c3d2.social
      1 caos.social
      1 hachyderm.io
      1 hackerspace.pl
      1 mas.to
      1 social.bau-ha.us
      1 social.flipdot.org
      1 social.okoyono.de
      1 social.saarland
      1 social.schaffenburg.org
      1 telefant.net
      2 social.c3l.lu
      3 mastodon.social
      4 hsnl.social
     39 chaos.social

So… it's mostly chaos.social. Neat.

back to top

comparing historical HMO licence data in Sheffield#prevsinglenexttop

2025-05-14 • tags: scripting, hmos, open-data • 1321 'words', 6.6 mins @ 200wpm

What is an HMO licence

Sheffield city council publishes a list of HMO (House in Multiple Occupation) licences on their HMO page, along with other information about HMOs (in brief, an HMO is a shared house/flat with more than 3 non-family members, and it must be licenced if this number is 5 or more).

How accessible is the data on HMO licences

They provide a list of licences as an Excel spreadsheet (.xlsx). I've asked them before if they could (also) provide a CSV, but they told me that was technically impossible. I also asked if they had historical data (i.e., previous spreadsheets), but they said they deleted it every time they uploaded a new one.

Therefore, as I'm interested in private renting in Sheffield, I've been archiving the data in a GitHub repository, as CSVs. I also add additional data like lat/long coordinates (via geocoding), and parse the data into geographical formats like .geojson, .gpx, and .kml (which can be viewed on a map!).

Calculating statistics from the data

What I hadn't done yet was any statistics on the data (I'd only been interested in visualising it on a map) so that's what I've done now.

I spent the afternoon writing some scripts to parse CSV data and calculate things like mean occupants, most common postcodes, number of expiring licences by date, et cetera.

General Statisitcs

I find shell scripting interesting, but I'm not so sure everyone else does (the script for the interested). So I'm not going to put the scripts here, but I will say that I used these command line tools (CLI tools) this many times:

Anyway, here are the statistics from the script (in text form, as is most shareable):

hmos_2024-09-09.csv
  total licences: 1745
  6.29 mean occupants (IQR 2 [5 - 7]) (median 6)
  amount by postcode:
    S1 (60), S2 (214), S3 (100), S4 (12), S5 (18), S6 (90), S7 (62), 
    S8 (10), S9 (5), S10 (742), S11 (425), S12 (1), S13 (2), S14 (1), S20 (1), S35 (1), S36 (1), 
  streets with most licences: Crookesmoor Road (78), Norfolk Park Road (72), Ecclesall Road (48), Harcourt Road (38), School Road (29), 
  
hmos_2025-01-28.csv
  total licences: 1459
  6.35 mean occupants (IQR 2 [5 - 7]) (median 6)
  amount by postcode:
    S1 (50), S2 (199), S3 (94), S4 (9), S5 (17), S6 (78), S7 (57), 
    S8 (10), S9 (4), S10 (614), S11 (321), S12 (1), S13 (2), S20 (1), S35 (1), S36 (1), 
  streets with most licences: Norfolk Park Road (73), Crookesmoor Road (57), Ecclesall Road (43), Harcourt Road (28), School Road (26), 
  
hmos_2025-03-03.csv
  total licences: 1315
  6.37 mean occupants (IQR 2 [5 - 7]) (median 6)
  amount by postcode:
    S1 (48), S2 (161), S3 (92), S4 (8), S5 (13), S6 (70), S7 (55), 
    S8 (9), S9 (3), S10 (560), S11 (290), S12 (1), S13 (2), S20 (1), S35 (1), S36 (1), 
  streets with most licences: Crookesmoor Road (54), Norfolk Park Road (41), Ecclesall Road (38), Harcourt Road (27), Whitham Road (24), 

Potential Conclusions

Draw your own conclusions there, but some could be that:

Statistics on issuing and expiry dates

I also did some statistics on the licence issue and expiry dates with a second stats script, which – as it parses nearly 5,000 dates – takes longer than "almost instantly" to run. As above, this used:

The script outputs:

hmos_2024-09-09.csv
  1745 dates in 1745 lines (627 unique issuing dates)
    637 expired
    1108 active
  Licence Issue Dates:
    Sun 06 Jan 2019, Sun 06 Jan 2019, … … … Wed 12 Jun 2024, Tue 09 Jul 2024, 
    Monday (275), Tuesday (440), Wednesday (405), Thursday (352), Friday (256), Saturday (5), Sunday (12), 
    2019 (84), 2020 (311), 2021 (588), 2022 (422), 2023 (183), 2024 (157), 
  Licence Expiry Dates:
    Mon 09 Sep 2024, Mon 09 Sep 2024, … … … Mon 11 Jun 2029, Sun 08 Jul 2029, 
    2024 (159), 2025 (824), 2026 (263), 2027 (225), 2028 (185), 2029 (89), 
    
hmos_2025-01-28.csv
  1459 dates in 1459 lines (561 unique issuing dates)
    334 expired
    1125 active
  Licence Issue Dates:
    Mon 28 Oct 2019, Mon 04 Nov 2019, … … … Mon 06 Jan 2025, Tue 14 Jan 2025, 
    Monday (243), Tuesday (380), Wednesday (338), Thursday (272), Friday (211), Saturday (6), Sunday (9), 
    2019 (2), 2020 (130), 2021 (567), 2022 (406), 2023 (181), 2024 (170), 2025 (3), 
  Licence Expiry Dates:
    Thu 30 Jan 2025, Fri 31 Jan 2025, … … … Mon 22 Oct 2029, Wed 28 Nov 2029, 
    2025 (681), 2026 (264), 2027 (225), 2028 (184), 2029 (105), 
    
hmos_2025-03-03.csv
  1315 dates in 1315 lines (523 unique issuing dates)
    189 expired
    1126 active
  Licence Issue Dates:
    Mon 28 Oct 2019, Mon 04 Nov 2019, … … … Wed 05 Mar 2025, Wed 05 Mar 2025, 
    Monday (217), Tuesday (339), Wednesday (314), Thursday (244), Friday (189), Saturday (4), Sunday (8), 
    2019 (2), 2020 (64), 2021 (494), 2022 (399), 2023 (177), 2024 (170), 2025 (9), 
  Licence Expiry Dates:
    Fri 07 Mar 2025, Fri 07 Mar 2025, … … … Mon 22 Oct 2029, Wed 28 Nov 2029, 
    2025 (533), 2026 (262), 2027 (225), 2028 (184), 2029 (111),

Potential conclusions on dates

Again, draw your own conclusions (homework!), but some could be:

Why is this interesting

I started collecting HMO data originally because I wanted to visualise the licences on a map. Over a short time, I have created my own archive of licence history (as the council do not provide such).

Since I had multiple months of data, I could make some comparison, so I made these statistics. I don't find them incredibly useful, but there could be people who do.

Perhaps as time goes on, the long-term comparison (over years) could be interesting. I think the above data might not be greatly useful as it seems that Sheffield council are experiencing delays over licensing at the moment, so the decline in licences probably doesn't reflect general housing trends.

Plus, I just wanted to do some shell-scripting ;]

back to top

taking a small bunch of census data from FindMyPast#prevsinglenexttop

2025-04-29 • tags: jq, scripting, web-scraping, census, data • 507 'words', 152 secs @ 200wpm

The 1939 Register was an basically-census taken in 1939. On the National Archives Page, it says that it is entirely available online.

However, further down, it lists how to access it, which says:

You can search for and view open records on our partner site Findmypast.co.uk (charges apply). A version of the 1939 Register is also available at Ancestry.co.uk (charges apply), and transcriptions without images are on MyHeritage.com (charges apply). It is free to search for these records, but there is a charge to view full transcriptions and download images of documents. Please note that you can view these records online free of charge in the reading rooms at The National Archives in Kew.

So… charges apply.

Anyway, for a while in April 2025 (until May 8th), FindMyPast is giving free access to the 1939 data.

Of course, family history is hard, and what's much easier is "who lived in my house in 1939". For that you can use:

I created an account with a bogus email address (you're not collecting THIS guy's data) and took a look around at some houses.

Then, I figured I could export my entire street, so I did.

The code and more context is in a GitHub Repository, but in brief, I:

Now it looks like:

AddressStreet,Address,Inhabited,LatLon,FirstName,LastName,BirthDate,ApproxAge,OccupationText,Gender,MaritalStatus,Relationship,Schedule,ScheduleSubNumber,Id
Khartoum Road,"1 Khartoum Road, Sheffield",Y,"53.3701,-1.4943",Constance A,Latch,31 Aug 1904,35,Manageress Restaurant & Canteen,Female,Married,Unknown,172,3,TNA/R39/3506/3506E/003/17
Khartoum Road,"4 Khartoum Road, Sheffield",Y,"53.3701,-1.4943",Catherine,Power,? Feb 1897,42,Music Hall Artists,Female,Married,Unknown,171,8,TNA/R39/3506/3506D/015/37
Khartoum Road,"4 Khartoum Road, Sheffield",Y,"53.3701,-1.4943",Charles F R,Kirby,? Nov 1886,53,Newsagent Canvasser,Male,Married,Head,172,1,TNA/R39/3506/3506D/015/39
Khartoum Road,"4 Khartoum Road, Sheffield",Y,"53.3701,-1.4943",Constance A,Latch,31 Aug 1912,27,Manageress Restairant & Cante,Female,Married,Unknown,172,3,TNA/R39/3506/3506D/015/41

Neat!

Some of my favourite jobs in the sheet of streets I collected are:

back to top see more (+13)