Streaming QR code detection in Home Assistant

A while back, I set up keyfob access using Home Assistant and ESPHome. It required bodging an RFID reader onto my doorbell, and after some time its 3D printed enclosure has yellowed. Today, out of the blue, an idea: ditch the keyfobs and scanner for QR codes.

Important: Ignore the configuration suggested in this post if you’re trying to install this add-on in Home Assistant itself. This is about an earlier prototype. Follow the readme on GitHub instead.

Scroll to the bottom if you just want the GitHub link.

The plan was simple. First, eat the doorbell’s RTSP stream. Feed it into OpenCV or something and detect QR codes. Then, throw the codes at the Home Assistant API.

It ended up being easier than I thought.

Here’s the code for the initial proof of concept. Yep, that’s all of it. It took about 15 minutes to write and test.

import sys
import re
from datetime import datetime, timedelta

import cv2
import homeassistant_api as ha
import yaml
try:
    from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
    from yaml import Loader, Dumper

DEBOUNCE_PERIOD = timedelta(seconds=5)
TAG_ID_PATTERN = re.compile('https://www.home-assistant.io/tag/([0-9a-f-]+)')

def main(config):
    with open(config, 'r') as fh:
        config = yaml.load(fh, Loader=Loader)['ha-cam-tag']
    
    stream = cv2.VideoCapture(config['camera']['stream'])
    detector = cv2.QRCodeDetector()

    last_time, last_tag = None, None

    with ha.Client(config['homeassistant']['uri'], config['homeassistant']['auth-token']) as client:
        while stream.isOpened():
            _, frame = stream.read()
            if data := detector.detectAndDecode(frame)[0]:
                if m := TAG_ID_PATTERN.match(data):
                    cur_time, tag_id = datetime.now(), m.group(1)
                    if last_tag != tag_id or last_time < cur_time - DEBOUNCE_PERIOD:
                        client.fire_event("tag_scanned", tag_id=tag_id, device_id=config['camera']['device-id'])
                        last_time, last_tag = cur_time, tag_id

    stream.release()
    cv2.destroyAllWindows()
    return 0
        
if __name__ == "__main__":
    sys.exit(main(*sys.argv[1:]))

The final version has the detector running in a separate thread (this helps with latency), and some boilerplate to make it easier to run, but the core concept is the same.

This requires just three packages: pyyaml, opencv-python, and homeassistant_api.

It eats a config file that looks like this:

ha-cam-tag:
  homeassistant:
    uri: https://homeassistant.example.com/api/
    auth-token: "secret-token"
  camera:
    device-id: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
    stream: rtsp://10.1.20.xxx:7447/xxxxxxxxxxxxxxxx

I’m using the high resolution RTSP stream on my Unifi doorbell. The medium level worked too, but was a bit less reliable.

I’ve got this running in a separate container from Home Assistant on my Proxmox box for now, but I want to port it to a proper extension eventually.

Here’s the repo: https://github.com/tmick0/ha-cam-tag