Capturing Govee Temperature in Docker

In my previous post I discussed reading Govee sensor temperature in a script. And that is perfectly fine. However, this is not ideal for my server environment. What I want is a Docker container.

Since I like Alpine images, the first step was to compile GoveeBTTempLogger. After installing prerequisites, I was greeted with bunch of errors:

/root/GoveeBTTempLogger/goveebttemplogger.cpp: In function 'bool ValidateDirectory(const std::filesystem::__cxx11::path&)':
/root/GoveeBTTempLogger/goveebttemplogger.cpp:924:23: error: aggregate 'ValidateDirectory(const std::filesystem::__cxx11::path&)::stat64 StatBuffer' has incomplete type and cannot be defined
  924 |         struct stat64 StatBuffer;
      |                       ^~~~~~~~~~
/root/GoveeBTTempLogger/goveebttemplogger.cpp:925:59: error: invalid use of incomplete type 'struct ValidateDirectory(const std::filesystem::__cxx11::path&)::stat64'
  925 |         if (0 == stat64(DirectoryName.c_str(), &StatBuffer))
      |                                                           ^
...

As lovers of Alpine know, due to its use of musl, this is not an uncommon occurrence. Fix was easy enough so I created a pull request myself. With this sorted out, it was time for Dockerfile.

Base prerequisites were obvious:

FROM alpine:latest
USER root

RUN apk add dbus bluez bluez-dev libstdc++
RUN rc-update add dbus bluetooth default
...

However, depending on services is not something Alpine does out-of-box. Openrc runlevel requires more direct access to machine. But, since I was not the first person needing it, solution already exists and it’s called softlevels. To enable them, three lines are enough:

RUN apk add openrc
RUN mkdir -p /run/openrc/exclusive
RUN touch /run/openrc/softlevel

This and a couple of wrapper scripts was all that was needed to get it running. But, I was still one step away from making it work in my environment. I needed compose.yaml and this is what I came up with (notice dbus volume):

services:
  govee2telegraf:
    container_name: govee2telegraf
    image: medo64/govee2telegraf:latest
    restart: unless-stopped
    privileged: true
    environment:
      TELEGRAF_HOST: <host>
      TELEGRAF_PORT: <port>
      TELEGRAF_BUCKET: <bucket>
      TELEGRAF_USERNAME: <username>
      TELEGRAF_PASSWORD: <password>
    volumes:
      - /var/run/dbus/:/var/run/dbus/:z

Image is available on DockerHub and everything else is on GitHub.

Capturing Temperature of Govee Bluetooth Sensors

I have an quite a few Govee temperature and humidity sensors. They’re reasonably priced, quite accurate, and they’re bluetooth LE. Yes, that allows them to sip power but at a cost that I cannot reach them when outside of home. Well, unless I get one of Govee hubs and connect them to cloud. But, is there a way to bypass the cloud and push all to my telegraf instance? Well, now there is!

First of all, why Telegraf? Obvious answer is because I have it already setup in my network and connected with my Grafana GUI. Longer answer is because I like the idea of telegraf. You have a centralized database and pushing to it is as easy as sending HTTP request. Everything is quite free-form and any mess you create is sorted out when data is displayed in Grafana.

Next question is, how? Well, I originally planned to roll my own script by abusing bluetoothctl scripting. However, during research I fount out that gentleman named William C Bonner already did pretty much the exact thing I wanted to. His GoveeBTTempLogger already both captures and decodes Govee temperature and humidity data.

And yes, there is no x64 package precompiled but, surprisingly, README.md instructions actually work. That said, I opted to build binaries a bit differently. This allowed me to install binary into /usr/local/bin/.

sudo apt install build-essential cmake git libbluetooth-dev libdbus-1-dev
git clone https://github.com/wcbonner/GoveeBTTempLogger.git
cd GoveeBTTempLogger
cmake -B ./build
sudo cmake --build ./build --target install

Once compiled, we can start application and, hopefully, see all the temperatures.

goveebttemplogger

And, if you just want to see the current values, that’s enough. If you check into README.md a bit more, you can also setup application to output web pages. Unfortunately, there is no telegraf output option. Or thankfully, since this gives me option to roll my own script around this nice tool.

What I ended up with is the following.

TG_HOST=<ip>
TG_PORT=<port>
TG_BUCKET=<bucket>
TG_USERNAME=<user>
TG_PASSWORD=<password>

while IFS= read -r LINE; do
  DATA=`echo "$LINE" | grep '(Temp)' | grep '(Humidity)' | grep '(Battery)'`
  if [ "$DATA" == "" ]; then continue; fi

  DEVICE=`echo $DATA | awk '{print $2}' | tr -d '[]'`
  TEMPERATURE=`echo $DATA | awk '{print $4}' | tr -dc '0-9.'`
  HUMIDITY=`echo $DATA | awk '{print $6}' | tr -dc '0-9.'`
  BATTERY=`echo $DATA | awk '{print $8}' | tr -dc '0-9.'`

  printf "%s %5s°C %4s%% %3s%%\n" $DEVICE $TEMPERATURE $HUMIDITY $BATTERY
  CONTENT="temp,device=$DEVICE temp=${TEMPERATURE},humidity=${HUMIDITY},battery=${BATTERY} `date +%s`"$'\n'
  CONTENT_LEN=$(echo -en ${CONTENT} | wc -c)
  echo -ne "POST /api/v2/write?u=$TG_USERNAME&p=$TG_PASSWORD&bucket=${TG_BUCKET}&precision=s HTTP/1.0\r\nHost: $TG_HOST\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: ${CONTENT_LEN}\r\n\r\n${CONTENT}" | nc -w 15 $TG_HOST $TG_PORT
done < <(/usr/local/bin/goveebttemplogger --passive)

This script goes over goveebttemplogger output and extracts device MAC address and its data. That data is then packed into Telegrafs line format and simply posted into nc as raw HTTP output. Not more difficult than wget or curl.

Wrapping this into a service so it runs in the background is an exercise left to the reader.

Decoding Classless Static Route Option

It has been 7 years since I made Classless Static Route Option Calculator. This was to solve my itch with Mikrotik DHCP but I later expanded it to cover other use cases. Calculator is simple: you put it networks and you get hex output. What else would you want?

Well, how about decoding existing entries? Calculator below takes hex output and gives you networks it represents.



Encrypted Kubuntu 24.10 with ZFS and Hibernation

For a while now I have published manual installation steps for pretty much any Ubuntu release since 18.10. Since, with Plasma 6, I switched to KDE and Kubuntu, I am abandoning that series. This guide will be the first one with manual installation steps for Kubuntu.

First, the big question: Why manual? Well, I view two things as absolutely mandatory for my Linux desktop: ZFS and encryption. One that I really like to have is hibernation. Default installation uses ZFS’ native encryption which, while nice and fast, doesn’t encrypt metadata. I am not saying that ZFS encryption is bad - I am just saying I am not necessarily comfortable with it. And swap size is way too small for hibernation to work. Thus, I like to do it all manually.

That said, if GUI installation leaves the system in state you like, there is no reason to follow this guide. Save yourself a bit of time.

As previously noted, this guide is for Kubuntu - an Ubuntu variant using KDE and not Gnome. While I was Gnome user for quite a long time, it was more of a stockholm syndrom rather than love. With KDE Plasma 6, I found something I actually like. As for the guide, Ubuntu and Kubuntu have a huge overlap and thus most of the steps are actually the same up to GUI install. It should be easy enough to look into an old Ubuntu guide and adjust the steps. I will probably still update Ubuntu steps for long-term releases (last one was 24.04)

With prologue done, what am I actually trying to achieve? Well, I want a minimal Kubuntu installation for my Framework 13 laptop that supports ZFS and hibernation. Both data and hibernation files are to be protected by LUKS encryption.

Finally, let’s go over all the steps to make it happen.

The first step is to boot into the USB installation and use “Try Ubuntu” option. Once on the a desktop, we want to open a terminal, and, since all further commands are going to need root access, we can start with that.

sudo -i

Then we add a few packets that come by default with Ubuntu install but are extras on Kubuntu.

apt update
apt install -y gdisk zfsutils-linux

Next step should be setting up a few variables - disk, hostname, and username. This way we can use them going forward and avoid accidental mistakes. Just make sure to replace these values with ones appropriate for your system.

DISK=/dev/disk/by-id/<disk>
HOST=<host>
USERNAME=<user>

For partition, I like to have 4 of them. The first two partitions are unencrypted and in charge of booting (boot + EFI). While I love encryption, I almost never encrypt the boot partitions in order to make my life easier as you cannot seamlessly integrate the boot partition password prompt with the later password prompt. Thus encrypted boot would require you to type the password twice (or thrice if you decide to use native ZFS encryption on top of that).

Third partition is swap that we need for hibernation support. I will use 64GB here because my laptop can have up to 96GB. While swap only requires about 40% of RAM to be backed by swap, having a few gigs extra will not hurt.

The last partition is the largest and will contain all user data.

One extra calculation is needed to figure out 4K aligned sector count.

LASTDISKSECTOR=$(( `blockdev --getsz $DISK` / 2048 * 2048 - 1 ))

And then we can create all the partitions needed:

blkdiscard -f $DISK 2>/dev/null
sgdisk --zap-all                               $DISK
sgdisk -n1:1M:+255M          -t1:EF00 -c1:EFI  $DISK
sgdisk -n2:0:+1792M          -t2:8300 -c2:Boot $DISK
sgdisk -n3:0:+64G            -t3:8200 -c3:Swap $DISK
sgdisk -n4:0:$LASTDISKSECTOR -t4:8309 -c4:LUKS $DISK
sgdisk --print                                 $DISK

To ease commands used later, here we can get partition UUIDs. While we can use disk names directly (as I often did before), using partition UUIDs helps if we ever clone disk to another physical drive.

PART1=`blkid -s PARTUUID -o value $DISK-part1`
PART2=`blkid -s PARTUUID -o value $DISK-part2`
PART3=`blkid -s PARTUUID -o value $DISK-part3`
PART4=`blkid -s PARTUUID -o value $DISK-part4`

The next step is to setup all LUKS partitions. If you paid attention, that means we need to repeat formatting a total of 2 times. Unless you want to deal with multiple password prompts, make sure to use the same password for both:

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i /dev/disk/by-partuuid/$PART4
cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i /dev/disk/by-partuuid/$PART3

Since creating encrypted partitions doesn’t mount them, we do need this as a separate step. I like to name my LUKS devices based on partition names so we can recognize them more easily:

cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    /dev/disk/by-partuuid/$PART4 $PART4
cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    /dev/disk/by-partuuid/$PART3 $PART3

Finally, we can set up our ZFS pool with an optional step of setting quota to roughly 85% of disk capacity. Since we’re using LUKS, there’s no need to setup any ZFS keys. Name of the pool will match name of the host and it will contain several datasets to start with. Most of my stuff goes to either Data dataset for general use or to VirtualBox dataset for virtual machines. Consider this just a suggestion and a good starting point, adjust as needed.

zpool create \
    -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O quota=3200G \
    -O canmount=off -O mountpoint=none -R /mnt/install \
    ${HOST^} /dev/mapper/$PART4
zfs create \
    -o reservation=100G \
    -o 28433:snapshot=72 \
    -o devices=on \
    -o canmount=noauto -o mountpoint=/ \
    ${HOST^}/System
zfs mount ${HOST^}/System
zfs create \
    -o 28433:snapshot=240 \
    -o canmount=noauto -o mountpoint=/home \
    ${HOST^}/Home
zfs mount ${HOST^}/Home
zfs set canmount=on ${HOST^}/Home
zfs create \
    -o 28433:snapshot=360 \
    -o canmount=noauto -o mountpoint=/Data \
    ${HOST^}/Data
zfs set canmount=on ${HOST^}/Data
zfs create \
    -o recordsize=32K \
    -o 28433:snapshot=72 \
    -o canmount=noauto -o mountpoint=/VirtualBox \
    ${HOST^}/VirtualBox
zfs set canmount=on ${HOST^}/VirtualBox
zfs set devices=off ${HOST^}

With ZFS done, we might as well setup boot, EFI, and swap partitions too:

yes | mkfs.ext4 /dev/disk/by-partuuid/$PART2
mkdir /mnt/install/boot/
mount /dev/disk/by-partuuid/$PART2 /mnt/install/boot/
mkfs.msdos -F 32 -n EFI -i 4d65646f /dev/disk/by-partuuid/$PART1
mkdir /mnt/install/boot/efi/
mount /dev/disk/by-partuuid/$PART1 /mnt/install/boot/efi/
mkswap /dev/mapper/$PART3

At this time, I also often disable IPv6 as I’ve noticed that on some misconfigured IPv6 networks it takes ages to download packages. This step is both temporary (i.e., IPv6 is disabled only during installation) and fully optional:

sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
sysctl -w net.ipv6.conf.lo.disable_ipv6=1

To start the fun we need to debootstrap our OS. As of this step, you must be connected to the Internet:

apt update
apt dist-upgrade --yes
apt install --yes debootstrap
debootstrap --components=main,restricted,universe,multiverse \
    oracular /mnt/install/

We can use our live system to update a few files on our new installation:

echo $HOST > /mnt/install/etc/hostname
sed "s/kubuntu/$HOST/" /etc/hosts > /mnt/install/etc/hosts
cp /etc/netplan/*.yaml /mnt/install/etc/netplan/

At last, we’re ready to chroot into our new system.

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys
chroot /mnt/install /usr/bin/env \
    PART1=$PART1 PART2=$PART2 PART3=$PART3 PART4=$PART4 \
    HOST=$HOST USERNAME=$USERNAME USERID=$USERID \
    bash --login

With our newly installed system running, let’s not forget to set up locale and time zone:

locale-gen --purge "en_US.UTF-8"
update-locale LANG=en_US.UTF-8 LANGUAGE=en_US
dpkg-reconfigure --frontend noninteractive locales
dpkg-reconfigure tzdata

To download some stuff later, wget is useful.

apt install -y wget

In order for decryption to work, we do need to set up crypttab so our encrypted partitions:

echo -n > /etc/crypttab
echo "$PART3 PARTUUID=$PART3 none luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
echo "$PART4 PARTUUID=$PART4 none luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab

cat /etc/crypttab

To mount all those partitions, we also need some fstab entries too. ZFS entries are not strictly needed. I just like to add them in order to hide our LUKS encrypted ZFS from the file manager (habit from Nautilus days):

echo -n > /etc/fstab
echo "PARTUUID=$PART2    /boot     ext4 noatime,nofail,x-systemd.device-timeout=3s 0 1" >> /etc/fstab
echo "PARTUUID=$PART1    /boot/efi vfat noatime,nofail,x-systemd.device-timeout=3s 0 1" >> /etc/fstab
echo "/dev/mapper/$PART3 none      swap sw,nofail                                  0 0" >> /etc/fstab
echo "/dev/mapper/$PART4 none      auto nofail,nosuid,nodev,noauto                 0 0" >> /etc/fstab

cat /etc/fstab

Now we’re ready to onboard the latest Linux kernel. Since this is not a LTS release, generic kernel will do.

apt update
apt install --yes --no-install-recommends linux-image-generic linux-headers-generic

On systems with a lot of RAM, I like to adjust memory settings a bit. This is inconsequential in the grand scheme of things, but I like to do it anyway. Think of it as wearing “lucky” socks:

echo "vm.swappiness=10" >> /etc/sysctl.conf
echo "vm.min_free_kbytes=1048576" >> /etc/sysctl.conf
cat /etc/sysctl.conf

Now we can create the boot environment:

apt install --yes zfs-initramfs cryptsetup keyutils plymouth-theme-spinner
update-initramfs -c -k all

And then, we can get grub going. Do note we also set up booting from swap (needed for hibernation) here too.

apt install --yes grub-efi-amd64-signed shim-signed

sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
    mem_sleep_default=deep \
    RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/$PART3)\"/" \
    /etc/default/grub

update-grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi \
    --bootloader-id=Ubuntu --recheck --no-floppy

cat /boot/grub/grub.cfg  | grep 'linux' | grep 'ZFS' | head -1 | xargs | cut -d' ' -f3-

I don’t like snap so I preemptively banish it from ever being installed:

apt remove --yes snapd 2>/dev/null

echo 'Package: snapd'    > /etc/apt/preferences.d/snapd
echo 'Pin: release *'   >> /etc/apt/preferences.d/snapd
echo 'Pin-Priority: -1' >> /etc/apt/preferences.d/snapd

apt update

And now, finally, we can install our minimal desktop environment:

apt install --yes kde-plasma-desktop man-db

Since I have a framework laptop, I like to allow trim operation on its external USB drives.

cat << EOF | tee /etc/udev/rules.d/42-framework-storage.rules
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="13fe", ATTRS{idProduct}=="6500", ATTR{provisioning_mode}="unmap"
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0005", ATTR{provisioning_mode}="unmap"
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0010", ATTR{provisioning_mode}="unmap"
EOF

In order to have hibernation to work properly, we need to disable a few devices. Fortunately, we can create script to do it for us every time system goes to sleep.

cat << EOF | tee /usr/lib/systemd/system-sleep/framework
#!/bin/sh
case \$1 in
  pre)
    for DRIVER_LINK in \$(find /sys/devices/ -name "driver" -print); do
      DEVICE_PATH=\$(dirname \$DRIVER_LINK)
      if [ ! -f "\$DEVICE_PATH/power/wakeup" ]; then continue; fi
      DRIVER=\$( basename \$(readlink -f \$DRIVER_LINK) )
      if [ "\$DRIVER" = "i2c_hid_acpi" ] || [ "\$DRIVER" = "xhci_hcd" ]; then
        echo disabled > \$DEVICE_PATH/power/wakeup
     fi
    done
  ;;
esac
EOF

sudo chmod +x /usr/lib/systemd/system-sleep/framework

For hibernation, I like to change sleep settings so that hibernation kicks in automatically after 13 minutes of sleep.

sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/'                           /etc/systemd/sleep.conf
sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/'                   /etc/systemd/sleep.conf
sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' /etc/systemd/sleep.conf
sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=13min/'               /etc/systemd/sleep.conf

For that we also need to do a minor lid switch configuration adjustment. I also like to set my power button as a hibernation trigger.

apt install -y pm-utils
sed -i 's/.*HandlePowerKey=.*/HandlePowerKey=hibernate/'                          /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/'           /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=ignore/' /etc/systemd/logind.conf
sed -i 's/.*HoldoffTimeoutSec=.*/HoldoffTimeoutSec=13s/'                          /etc/systemd/logind.conf

Since Firefox is a snapd package (and we banished it), we can install it manually.

add-apt-repository --yes ppa:mozillateam/ppa

cat << 'EOF' | sed 's/^    //' | tee /etc/apt/preferences.d/mozillateamppa
    Package: firefox*
    Pin: release o=LP-PPA-mozillateam
    Pin-Priority: 501
EOF

apt update && apt install --yes firefox

Chrome aficionados, can install it too:

pushd /tmp
wget --inet4-only https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome-stable_current_amd64.deb
popd

Lastly, we need to have a user too.

adduser --disabled-password --gecos '' -u $USERID $USERNAME
usermod -a -G adm,cdrom,dialout,dip,lpadmin,plugdev,sudo,tty $USERNAME
echo "$USERNAME ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME
passwd $USERNAME

It took a while, but we can finally exit our debootstraped environment:

exit

Let’s clean all mounted partitions and get ZFS ready for next boot:

zfs set devices=off ${HOST^}/System

sync
umount /mnt/install/boot/efi
umount /mnt/install/boot
mount | grep -v zfs | tac | awk '/\/mnt/ {print $3}' | xargs -i{} umount -lf {}
zpool export -a

After reboot, we should be done and our new system should boot with a password prompt.

reboot

Once system has booted, we can tell it to use local BIOS clock. It helps with dual-boot and I actually prefer to have local time in BIOS.

sudo timedatectl set-local-rtc 1 --adjust-system-clock

Of course, we can end with a hibernation test. If everything went correctly, this should work nicely.

sudo systemctl hibernate

Just the Hibernation Steps, Please

For a while now I use Ubuntu family of Linux on my Framework. While the exact flavor might change (Kubuntu rules! :)), two things remain the same. ZFS and hibernation. If you want to install system from scratch, you can find post for many different Ubuntu releases. What I don’t have is a separate post with just a hibernation steps. So, here it is.

First, let’s discuss prerequisites. You MUST have a separate swap partition if you want to hibernate to a file on ZFS. My setup is usually something like this:

PartitionTypeSize (MB)Description
EFIEF00FAT32255Small EFI partition
Boot8300EXT41,792Linux boot partition
Swap8200-65,536Swap, minimum 40% of RAM
(else)8309(whatever)one or more partitions for system and data

If you don’t have a separate partition, you can stop reading now and find somebody smarter. And no, the swap partition doesn’t need to be same size or larger than the amount of RAM you have. Minimum is actually 40% of your RAM (controlled by image_size parameter). Now, I’ve been guilty of having the swap the same size as RAM too. But that is not a requirement.

You also must have the Secure Boot disabled. If you don’t have it disabled, it will all seemingly work but system will never restore.

Steps I am giving here are for encrypted swap. I personally think that you should NEVER have unencrypted data on disk. NEVER. However, I am aware that most of people don’t care and see password entry as a chore. In one of the next posts, I will add instructions for non-encrypted swap too. So, if you are “one of those”, stay tuned.

Order of operations is quite fungible but, for the purpose of this guide, I will start with config files. There are two and whether you will modify them both depends on what you want to achieve.

In sleep.conf, I adjust suspend settings to allow for Suspend-then-Hibernate setup. My preference is to have system hibernate after 13 minutes of sleep but you can change that to your liking.

sudo sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/'                           /etc/systemd/sleep.conf
sudo sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/'                   /etc/systemd/sleep.conf
sudo sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' /etc/systemd/sleep.conf
sudo sed -i 's/.*HibernateMode=.*/HibernateMode=platform shutdown/'           /etc/systemd/sleep.conf
sudo sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=13min/'               /etc/systemd/sleep.conf

Second config file is to reassign power button to hibernation. This is just my preference and, if you want power button to stay as-is, you can omit this step. For this, you need to install pm-utils package, if not already present on your system. Again, these are settings I like so adjust as needed.

sudo apt install -y pm-utils
sudo sed -i 's/.*HandlePowerKey=.*/HandlePowerKey=hibernate/'                          /etc/systemd/logind.conf
sudo sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/'           /etc/systemd/logind.conf
sudo sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=ignore/' /etc/systemd/logind.conf
sudo sed -i 's/.*HoldoffTimeoutSec=.*/HoldoffTimeoutSec=13s/'                          /etc/systemd/logind.conf

With this sorted out, you need to make sure computer doesn’t wake up. This step can be skipped quite often, but not so with Framework. With Framework laptops you need to manually disable wakeup for i2c_hid_acpi and xhci_hcd devices.

These commands will generate script I personally use and allow for its execution upon sleep.

cat << EOF | sudo tee /usr/lib/systemd/system-sleep/framework
#!/bin/sh
case \$1 in
  pre)
    for DRIVER_LINK in \$(find /sys/devices/ -name "driver" -print); do
      DEVICE_PATH=\$(dirname \$DRIVER_LINK)
      if [ ! -f "\$DEVICE_PATH/power/wakeup" ]; then continue; fi
      DRIVER=\$( basename \$(readlink -f \$DRIVER_LINK) )
      if [ "\$DRIVER" = "i2c_hid_acpi" ] || [ "\$DRIVER" = "xhci_hcd" ]; then
        echo disabled > \$DEVICE_PATH/power/wakeup
     fi
    done
  ;;
esac
EOF
sudo chmod +x /usr/lib/systemd/system-sleep/framework

Now, we can setup swap. For this I strongly recommend using variables as to avoid any naming issues. Replacewith your partition (e.g. /dev/disk/by-id/whatever-part3).

PART=<part>
UUID=`sudo blkid -s PARTUUID -o value $PART`

Assuming your swap is not initialized, you need to do so.

sudo cryptsetup luksFormat -q --type luks2 \
  --sector-size 4096 \
  --cipher aes-xts-plain64 --key-size 256 \
  --pbkdf argon2i /dev/disk/by-partuuid/$UUID
sudo cryptsetup luksOpen \
  --persistent --allow-discards \
  --perf-no_write_workqueue --perf-no_read_workqueue \
  /dev/disk/by-partuuid/$UUID $UUID
mkswap /dev/mapper/$UUID

Of course, adding this to both crypttab and fstab is also needed for swap to work properly.

echo "$UUID PARTUUID=$UUID none luks,discard,initramfs,keyscript=decrypt_keyctl" | sudo tee -a /etc/crypttab
echo "/dev/mapper/$UUID none swap sw,nofail 0 0" | sudo tee -a /etc/fstab

The final step is adding swap as RESUME into grub. Note that swap will be identified based on partition UUID.

sudo sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
  rtc_cmos.use_acpi_alarm=1 \
  RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/$UUID)\"/" \
  /etc/default/grub

With these things in place, you should be able to use hibernate. To check, use systemctl.

sudo systemctl hibernate

And yes, you can probably use these steps with any laptop, not just Framework. However, I tested this on Framework and thus will not make other claims. :)