Homebridge 2.0 and Garage Door Opener

Back in 2017 (more than 9 years ago), I created my own home automation garage door opener using a Raspberry Pi Zero. I then wrote a homebridge plugin so that the garage door opener will work with Homebridge and HomeKit.

The times of change has caught up to us. The original version of the plugin that I wrote with Homebridge was designed and coded for version 1.0. As of May 4, 2026, Homebridge released version 2.0. As of this writing, they are now on version 2.2.1.

Unfortunately for me, this upgrade rendered my old plugin not workable with the latest version of Homebridge. I was going to read the migration guide and performed the changes myself, but of course I was too lazy to get around to it.

Today, I simply asked opencode.ai to tackle this problem. I have attached my opencode session at the end of this post.

I am super surprised that opencode using Qwen 3.6 27B LLM local model running on vLLM handled the changes in a single shot. This was truly a one-shot solution, no fuss, no muss.

I still have to learn that AI can do many things for us and not to short sell it for future potential jobs. Live and learn.

Dark Smokey Skies

Yesterday in the Greater Toronto Area, we broke a new temperature high, reaching a scorching 37+ºC.

This morning we woke up to a smoky sky, with barely any sunlight touching the ground. Truly creating an apocalyptic atmosphere. Apparently, the smoke is from Northern Ontario, where it is haunted by many forest fires.

I took my camera and snapped this gloomy photo from our backyard.

Smokey sky and dark morning at 11am on July 15, 2026.

Global Currency

In recent years, I find myself closely following geopolitical topics, such as global conflicts, trade disputes and wars, and international trade. I want to explore not just the what and how, but also the why certain things happen and the parties’ motivation behind their actions.

The journey of trying to understand the above helps to frame my own mind so that I can properly make my own judgements on how effective a government is in managing its resources and assets, to either positively or negatively impact the lives of its constituents, the quality of life of an average citizen.

I have discovered certain YouTube channels that in my view continue to educate me on these matters. On this post, I like to share two YouTube videos that I find very educational.

The first one describes the underlying financial mechanism that allows global trade of goods can be settled and respective currencies can be transferred and the excess stored.

The Hidden Machine Keeping the Dollar Alive Is Breaking

The second one is a discussion with Louis-Vincent Gave (CEO, Gavekal). They explore today’s conflict in the Strait of Hormuz and how it impacts oil prices, and the secondary and tertiary impacts to global economy. LVG has been a favourite of mind. He runs a consulting and investment company head quartered in Hong Kong and has a very informative view on the financial activities inside China and its neighbours.

Louis-Vincent Gave: Inside China’s Plan to Kill the Dollar’s Dominance

If you share similar interests, enjoy the above videos.

Journey to Ubuntu 24.04 LTS Ended in Another Rescue

My NAS is currently running Ubuntu 22.04.5 LTS. I have tried in the past to perform a do-release-upgrade, and ended up with a system that will not boot.

Since then, I have moved many services away from the NAS. I thought I should give it one more try, and I did just that yesterday. Unfortunately the result ended up the same, resulting another rescue.

I thought I should document the rescue process here again.

# Wipe the root fs
mkfs.ext4 /dev/nvme1n1p2

# Restore from backup
mount /dev/nvme1n1p2 /mnt
mount /dev/backup_partition /mntb
rsync -aAXv /mntb/ /mnt/

# Ensure the root file system new UUID is the same in /etc/fstab
vi /mnt/etc/fstab

# chroot to install the boot partition
mount /dev/nvme1n1p1 /mnt/boot/efi
for i in /dev /dev/pts /proc /sys /run; do mount -B $i /mnt$i; done
mount -t efivarfs efivarfs /mnt/sys/firmware/efi/efivars 
chroot /mnt

# Identify your EFI partition again just in case (e.g., /boot/efi)
sudo grub-install

# Below is more forceful but mostly optional and unnecessary
# grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=GRUB --removable --recheck

sudo update-grub
exit

# Exit and reboot
umount -l /mnt
reboot

By now I have become an expert in rescuing failed upgrades with Ubuntu.

I have upgraded my TUF GAMING B550-PLUS motherboard to version 3636. This is a recently released BIOS from ASUS in January of 2026. My previous version of the BIOS was from 2024.

I will give myself another breather, say about a week, before attempting to try again.

Resetting SolarEdge Inverters

In a previous post, one of our two SolarEdge inverters encountered an error and one quick fix is to reset the inverters. This year we had a similar issue.

Three days ago, our solar system encountered a grid voltage issue. Our XWPro inverter was in AC PassThru mode causing the SolarEdge inverters to detect the same grid issue. Our solar system is AC coupled. With XWPro handling grid-tied net metering, and battery charging and discharging, and SolarEdge for solar energy generation.

AC Qualification Limit Exceeded

This grid event cause both SolarEdge inverters to go into a “Grid Profile Limit” mode where its AC output was limited to around 100W. When I reset both inverters through the main breaker panel, one recovered while the other continue with the limited output behaviour. To fix the second one, I had to perform a hard reset on the inverter. Below are the steps needed.

Main Breaker Panel
SolarEdge Inverter Control Positions

First I had to switch off the inverter at position A, and then turn off the DC disconnect at position B. I then had to switch off the breaker on the main panel.

The important part is to wait 5 to 10 minutes to wait for the inverter to discharge for the full reset to happen.

Once the time has passed, perform the action in reverse. Turn back on the breaker, the DC disconnect (B), and then finally turn back on the inverter (A).

Luckily after this hard reset procedure, the second SolarEdge inverter has been fully restored with normal operation.

Home Automation Garage Door Opener on Life Support

More than nine years ago, I created a remote garage door opener that connected to my HomeKit setup. This has proven to be a budget-friendly and super handy device, as I am able to control my garage door from anywhere in the world. I came up with this solution before WiFi-based remote garage door openers were commercialized.

However, recently the Raspberry Pi Zero W started to randomly lose WiFi network connection, and I have to reboot it all the time. Of course, this is very frustrating. Since the device is plugged into a ceiling plug, the same socket that is used for the actual garage door opener, it is quite inconvenient to cycle the device. I typically had to restart the whole garage by resetting the breaker on the main electrical panel.

I have some extra ESP32-S3 SuperMini boards on the side that I was going to replace the PiZero W with. I bought these from Pinduoduo (拼多多) when I was in China last year. Due to my laziness, I did not get around to it. Something else happened that allowed me to find another workaround.

About three and a half years ago, I purchased the VOCOlinc HomeKit Smart Plugs from Amazon. I used these to remotely control some fans in the house. One of these was recently freed up. I can then plug the adapter used to power the Pi Zero into the Smart Plug. Now I have a remote way to remotely power cycle the Pi Zero. A remote device to control the power of another remote device! Not only can I cycle the Pi Zero remotely, I can also programmatically determine when to cycle the device.

The Smart Plug is setup with my HomeKit environment and I recently learned that on a Mac, you can use the Shortcut App to toggle an accessory or scene with HomeKit.

I also found out that once I have a Shortcut, I can invoke it using the shortcuts command line command.

Using this shortcut concept, I can create a periodic cron job that effectively check the connectivity of the Pi Zero every 15 minutes. If I am unable to connect, I can effectively remote restart the Pi Zero. The script is listed below:

#!/usr/bin/env zsh
#
# This script is meant to be run as root

logger "cyclePizero.sh: INFO test connectivity to pizero.localdomain"
if ! ping -q -c 1 pizero.localdomain >/dev/null; then
        logger "cyclePizero.sh: ERROR unable to ping pizero.localdomain"
        logger "cyclePizero.sh: INFO restarting the resolved daemon"
        systemctl restart systemd-resolved.service
        logger "cyclePizero.sh: INFO cycling pizero.localdomain"
        ssh bigbird -n 'shortcuts run "Toggle Garage Opener"'
        sleep 3
        ssh bigbird -n 'shortcuts run "Toggle Garage Opener"'
        logger "cyclePizero.sh: INFO cycling completed"
else
        logger "cyclePizero.sh: INFO pizero.localdomain ping successfully"
fi

Note that I also sometimes have to restart the name resolution service, system-resolved. This is another reason sometimes HomeKit fails to communicate with the Pi Zero.

Hopefully this patch will work until I finally have time to replace it with the ESP32.

Target Sports Canada

Today we had fun at our first shooting range in Canada. A friend of ours was kind enough to arrange a group outing at Target Sports Canada. They offered an unlicensed shooting experience of groups between 2 to 5 people.

The whole experience was about 2 hours. The registration was very simple. After about 30 minutes of orientation, we got prepped with glasses and ear protection and went into the shooting range.

The range was safe and organized. We ended up shooting a rifle, a shotgun, a 9mm hand gun and a 45 Colt 1911. I cannot recall the other models. We had about 10 rounds each, and it was fun to experience the different model of the guns.

I found the hand guns to be most enjoyable. The shotgun’s kickback was something to experience. Overall I think the entire group had loads of fun including my wife who tagged along for the trip.

Here is a short video of our experience:

Our first experience at the shooting range

WebAuthn with Email Implementation

Over the past few years I have developed several services that can be accessed using a web site. Many if not all of these sites require authentication. In the past I have typically adopted a typical user id and password technique, and more recently an email based authentication along with the user’s external IP address, so that they do not need to be burdened with remembering the password.

When my iPhone started to adopt the WebAuthn passkey solutions I wanted to make use of this convenient solution for my sites as well. As you can see from the chart below, the adoption across the different platforms and devices are now universal.

Compatibility List

I went about to develop my own identity provider server using the python WebAuthn package. Why did I develop my own solution and instead of using one of the open source solution? I wanted to learn how this works, and what better way to do it than implementing my own version. I also wanted to customize it based on a list of authorized email with the ability to track and manage the access.

This was also the first solution where I used AI to help me vibe code the browser side of the solution. It used the navigator.credentials object to do most of the heavy lifting. The AI generated code at the time is fraught with many errors and bad assumptions which I had fix manually. This was more than a year ago, so I am sure things have improved by now.

In the end, I deployed this custom identity service on auth.lufamily.ca. This custom service also handled the email authentication flow, which goes something like this:

No Email Sent Yes Goto Site Already Registered? Register with Email Read Email and Click on Welcome Login with Email Enter Site

There are no passwords with the above approach. All the users need to remember are the email addresses that they used to register with the site access. The login and registration page looks like this:

Registration and Login Page

For access provisioning, I simply use a JSON file to bind the email address to the allowed web sites. Below is an example:

{
  "jdoe@gmail.com": [
    {
      "user": "John",
      "site": "https://site1.lufamily.ca"
    }
  ],
  "jane.doe@icloud.com": [
    {
      "user": "Jane",
      "site": "https://site1.lufamily.ca"
    },
    {
      "user": "Jane",
      "site": "https://site2.lufamily.ca"
    }
  ]
}

When the user registers, they will receive an email looking like:

Sample onboarding or registration email

In the beginning, I wrote custom code on my web site to use my identity service. However I found out I can write an Apache Lua script to check for token provisioning and invoking of the identity service. I needed some other Lua packages to write my script so I had to figure out which version of Lua is my Apache2 using.

ldd /usr/lib/apache2/modules/mod_lua.so                                                                                                
        linux-vdso.so.1 (0x00007ffd141ea000)
        liblua5.3.so.0 => /lib/x86_64-linux-gnu/liblua5.3.so.0 (0x0000729ff6d47000)
        libcrypt.so.1 => /lib/x86_64-linux-gnu/libcrypt.so.1 (0x0000729ff6d0d000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000729ff6a00000)
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x0000729ff6919000)
        /lib64/ld-linux-x86-64.so.2 (0x0000729ff6dbe000)

Once I found out that I was using version 5.3, I needed to enable mod_lua with Apache 2:

sudo a2enmod lua

Followed by the installation of three packages that I needed for my script:

sudo luarocks --lua-version 5.3 install lua-cjson
sudo luarocks --lua-version 5.3 install luasec
sudo luarocks --lua-version 5.3 install luasocket

These three packages allow me to process JSON data structures, and communicate with my identity server (auth.lufamily.ca). This way I can add authentication to any sites that I host with Apache2 web server with a virtual host configuration that looks like this:

<VirtualHost *:80>

  <Location / >
    LuaCodeCache forever
    LuaHookAccessChecker /path/to/checkAuthLuFamily.lua handle

    RewriteEngine On

    RewriteCond %{ENV:token} ^$
    RewriteRule ^ https://auth.lufamily.ca/register/%{ENV:cbsite}" [L,R=302]

    Header set Set-Cookie "token=%{token}e; Max-Age=1800; Path=/; HttpOnly; Secure; SameSite=Strict" env=token
  </Location>

</VirtualHost>

The checkAuthLuFamily.lua script is used to check if a token is provided either as an HTTP GET parameter, Authorization Bearer value, or a secure, http-only cookie. The token is actually a JWT token containing user specific attributes derived from the JSON file earlier. This token is provisioned when authentication is successful. If the token is missing, then this means the user has yet to be authenticated so we automatically redirect them to the registration page. If the token is valid, then the script will store a new refreshed token with extended expiry (another 30 minutes) into the environment variable which we use to reset the cookie. Any future requests to the same site will preserve the cookie/token.

I love this flexibility. This means I can add authentication to any site that I host with Apache2 without changing the code or modifying the site. This also means that I can develop future sites and services without having to worry about authentication.

I have not provided the source code here, because I am still testing it, but I wanted to document the concept and the approach, so that I can refer to my own creation in the future.

Replacing Fail Drive in Existing VDEV

In a previous post, I discussed creating a brand new VDEV with new drives to replace an existing VDEV. However, there is another approach that I chose to use in a very recent event for my NAS (Network Attached Storage) hard drive when it started to encounter write errors and later checksum errors.

The output of zpool status -v

The affected VDEV is mirror-4. Since there are 16 hard drives involved in this storage pool, I had to find out which hard drive is having the issue. I had to perform the following command line operations to obtain the serial numbers of the drives within the VDEV.

Shell commands to get the Serial Number.

It was the WD60EFRX drive that failed. This is a WD60EFRX Western Digital Red 6TB 5400RPM drive. I was curious to see how old is the drive, so I used the smartctl utility to find out the number of powered on hours that this particular drive endured.

The 4.2 years (37033 / 24 / 365 = 4.2) is well over the 3 years warranty promised by Western Digital, so I took this unfortunate opportunity to get two new Seagate IronWolf Pro 12TB Enterprise NAS Internal HDD Hard Drive. The idea is not just to replace the drive with issue but also to expand the pool, and get an extra 6TB drive from the existing mirror that is still good, and use it as part of my offline backup strategy.

Once the new drives arrived and connected to the system, I simply performed an attach command to add them to the mirror VDEV.

commands to attach the new drive

After attaching the new drives, the zfs pool begins to automatically resilver. The above image was taken several hours after the attachment, and we are now waiting for the last drive to complete its resilvering. Since one of the new drive has already completed its resilvering, this means we have regained full redundancy.

After the resilvering is completed, I will then detach both old drives from the mirror using the detach command.

zpool detach vault /dev/disk/by-id/wwn-0x50014ee2b9f82b35-part1
zpool detach vault /dev/disk/by-id/wwn-0x50014ee2b96dac7c-part1

The first drive will be chucked into the garbage bin, and the second drive will be used for offline backup. Before I use the second drive for offline backup, I need to remove all zfs information and meta data from the drive to avoid any unintentional future conflicts. We do this using the labelclear command like below.

zpool labelclear /dev/disk/by-id/wwn-0x50014ee2b96dac7c

For extra safety, we can also destroy the old partition by using parted and relabeling the disk and create a new partition table. If the above command fails, we can use the dd command to just zero out the first few blocks of the drive.

dd if=/dev/zero of=/dev/disk/by-id/wwn-0x50014ee2b96dac7c bs=1M count=100

In summary, this is the general strategy moving forward. When a drive on my NAS pool starts to fail (before actual failure), I take the opportunity to replace all the drives in the entire mirror with higher capacity drives, and use the remaining good one to serve as offline backup.