commit 1c45044e0a0f854c8a4f22e7f005346d22115bbb
parent f2812a4cd3a2196f76167ff5c9ec233f9f3072af
Author: dwrz <dwrz@dwrz.net>
Date: Thu, 2 Feb 2023 14:15:39 +0000
Add 2023-02-01 entry
Diffstat:
2 files changed, 1030 insertions(+), 0 deletions(-)
diff --git a/cmd/web/site/entry/static/2023-02-01/2023-02-01.html b/cmd/web/site/entry/static/2023-02-01/2023-02-01.html
@@ -0,0 +1,1024 @@
+<a href="/static/media/1920/dwrz_20221023T155810_edit.jpg" >
+ <img class="img-center" src="/static/media/720/dwrz_20221023T155810_edit.jpg">
+</a>
+
+<p>
+ I live in a ground floor apartment, and want to keep an eye on the space and
+ the cats when I am away. A search for security camera systems that offered
+ timelapse recording, livestream viewing, and instrusion notifications was
+ unsatisfactory. Most consumer systems were either too expensive, had security
+ shortcomings, or lacked sufficient user control. As a result, I endend up
+ assembling a system that meets most of my needs.
+</p>
+
+<p>
+ The solution I've settled on uses three
+ <a href="https://www.raspberrypi.org/"> Raspberry Pi</a>'s, each acting as a
+ server connected to a <a href="https://www.webcamerausb.com/">generic fisheye
+ USB camera</a>. A mix of open-source software and scripts provides a
+ password protected livestream served over HTTPS, timelapse recording, motion
+ and object detection, notifications, and remote storage.
+</p>
+
+<p>
+ The cameras come with limitations and vulnerabilties, some shared with
+ consumer solutions, others unique to a home-brewed setup. But for my needs,
+ they have worked well, and I have appreciate their modularity, the ability to
+ repurpose hardware, and full control over the system and the data that it
+ generates.
+</p>
+
+<p>
+ I was surprised by how quickly I could stand up a system of such disparate
+ parts — in terms of hardware and software — while writing little code of my
+ own. Putting these cameras together seemed to confirm some of the
+ <a href="https://en.wikipedia.org/wiki/Unix_philosophy#Origin">UNIX</a>
+ principles. It's been possible to connect components with just a few scripts
+ as glue.
+</p>
+
+<p>
+ Any code referenced on this page should be available here:
+ <a href="https://code.dwrz.net/vigil/">https://code.dwrz.net/vigil/</a>.
+ I don't intend to keep code on this page up to date; it should only be used
+ for example and inspiration.
+</p>
+
+<hr>
+
+<p>
+ What follows is a rough guide covering the basic components of the system.
+ It is not intended to be a step-by-step guide, though there is a chance it
+ might work as one.
+</p>
+
+<h2>Hardware</h2>
+
+<a href="/static/media/1920/dwrz_20221022T213439.jpg" >
+ <img class="img-center" src="/static/media/720/dwrz_20221022T213439.jpg">
+</a>
+
+<p>
+ I've opted for the following:
+ <ul>
+ <li>
+ <a href="https://www.raspberrypi.com/products/raspberry-pi-400/">Raspberry
+ Pi 400</a> — easier to source and repurpose for my use cases. I would
+ have preferred a smaller device with more open hardware and USB ports,
+ but it was hard to find anything of comparable price.
+ </li>
+ <li>
+ <a
+ href="https://www.westerndigital.com/products/usb-flash-drives/sandisk-extreme-pro-usb-3-2">
+ Sandisk SSD Flash Drive</a> — improves performance and reliability
+ compared to running the Raspberry Pi off of a MicroSD card.
+ </li>
+ <li>
+ <a
+ href="https://www.amazon.com/s?me=AVRDPNYMU6GNM&marketplaceID=ATVPDKIKX0DER">
+ ELP 3.6mm FHD 180° IR Fisheye Camera</a> — can cover an entire room and
+ see in the dark.
+ </li>
+ <li><a href="https://www.amazon.com/stores/ULIBERMAGNET/page/22395264-96B4-4270-9CA4-518AED93106E">ULIBERMAGNET</a> Tripod Ball Head with Magnetic Base — used to hold and position the cameras.</li>
+ </ul>
+</p>
+
+<h2>Server</h2>
+
+<img class="img-center" src="/static/media/vigil-diagram.svg">
+
+<p>
+ I've used the default operating system for Raspberry Pi's, <a href="https://www.raspberrypi.com/software/">Raspberry Pi OS</a>, and this guide assumes that context. I won't cover the operating system installation and setup of an administrative user — documentation is available elsewhere.
+</p>
+
+<p>
+ You should be mindful of the security of the servers themselves. This is a
+ <a href="https://www.theverge.com/2022/11/30/23486753/anker-eufy-security-camera-cloud-private-encryption-authentication-storage" title="Anker’s Eufy lied to us about the security of its security cameras">problem</a> that even <a href="https://www.bloomberg.com/news/articles/2021-03-09/hackers-expose-tesla-jails-in-breach-of-150-000-security-cams" title="Hackers Breach Thousands of Security Cameras, Exposing Tesla, Jails, Hospitals">commercial</a> <a href="https://www.washingtonpost.com/technology/2019/04/23/how-nest-designed-keep-intruders-out-peoples-homes-effectively-allowed-hackers-get/" title="How Nest, designed to keep intruders out of people’s homes, effectively allowed hackers to get in">offerings</a> <a href="https://www.consumerreports.org/home-security-cameras/wyze-didnt-completely-fix-security-camera-flaws-for-3-years-a3726294358/" title="Wyze Didn't Completely Fix Flaws in Security Cameras for 3 Years">have not handled well</a>.
+ Your personal circumstances will dictate the balance of features and security.
+</p>
+
+<aside class="bordered br-yellow">
+ <p>
+ When installing the operating system, you'll have to decide whether to
+ encrypt the filesystem (or a partition). Encryption makes it harder to
+ extract data if someone is able to gain physical access to the
+ storage device. However, entering the decryption passwords requires you to
+ be physically present at the machine. This can be a problem if, e.g., a
+ transient power outage happens while you are away from the cameras; they
+ would cease to function until you are on premises again. It's possible to
+ use something like <code><a href="https://wiki.archlinux.org/title/Dm-crypt/Specialties#Remote_unlocking_(hooks:_netconf,_dropbear,_tinyssh,_ppp)">dropbear</a></code> for boot-time SSH,
+ to enter the decryption password. But I've opted to use the servers without
+ encryption, and transfer photos and videos off of the servers as soon as
+ possible. The downside is that any secrets on the device can be
+ compromised, requiring remediation in the event of a breach.
+ </p>
+</aside>
+
+<p>
+ Once you are up and running with Raspberry Pi OS, ensure you are
+ using the latest software and security updates:
+ <pre><code>$ sudo apt-get -y update && sudo apt-get -y dist-upgrade</code></pre>
+</p>
+
+<p>
+ Install any dependencies necessary to get work done, e.g.:
+ <pre><code>$ sudo apt-get install curl git mg</code></pre>
+</p>
+
+<p>
+ Consider enabling <a href="https://wiki.debian.org/UnattendedUpgrades">unattended
+ security upgrades</a>:
+
+ <pre><code>$ sudo apt-get install unattended-upgrades apt-listchanges </code></pre>
+</p>
+
+<p>
+ To receive email reports for unattended upgrades, a
+ <a href="https://en.wikipedia.org/wiki/Message_transfer_agent">
+ Message Transfer Agent
+ </a> (MTA) and the <code>mailx</code> command are required. This guide
+ assumes the use of <code><a href="https://marlam.de/msmtp/">msmtp</a></code>:
+
+ <pre><code>$ sudo apt-get install bsd-mailx msmtp msmtp-mta</code></pre>
+</p>
+
+<p>
+ Setup the <code>msmtp</code>
+ <a href="https://marlam.de/msmtp/msmtp.html#Configuration-files">configuration</a>
+ file for the <code>root</code> user:
+
+ <pre><code>$ sudo mg /root/.msmtprc</code></pre>
+
+ <pre><code>defaults
+auth on
+tls on
+tls_trust_file /etc/ssl/certs/ca-certificates.crt
+logfile /root/.msmtp.log
+
+account gmail
+host smtp.gmail.com
+port 587
+from user@example.com
+user user@example.com
+password ${PASSWORD}
+# Alternatively, a command may be used to retrieve the password:
+# passwordeval pass google/gmail/app
+# See: https://marlam.de/msmtp/msmtp.html#passwordeval.
+
+account default : gmail</code></pre>
+</p>
+<p>
+ This configuration assumes a Gmail or Google Workspace account; you will
+ need to specify appropriate settings for your own mail provider. If you are
+ using Gmail or Google Workspaces, you will need to set up an "app password"
+ for programmatic access.
+</p>
+
+<p>
+ Test that <code>msmtp</code> is working:
+
+ <pre><code>echo "Test" | mailx -s "Test" user@example.com</code></pre>
+</p>
+
+<p>
+ Edit the <code>unattended-upgrades</code> configuration to send email
+ notifications:
+
+ <pre><code>$ sudo mg /etc/apt/apt.conf.d/50unattended-upgrades</code></pre>
+
+ <pre><code>Unattended-Upgrade::Mail "user@example.com";</code></pre>
+</p>
+
+<h2>Networking</h2>
+<p>
+ I assign a static IP address for each of
+ my servers. Most consumer routers allow for this in their web interface; I
+ have something like the following in my router's <code>/etc/dhcpd.conf</code>:
+ <pre><code>host kitchen {
+ fixed-address 10.0.1.101;
+ hardware ethernet de:ad:be:ef:8d:8e;
+}</code></pre>
+
+ <aside class="bordered br-yellow">
+ <p>
+ If you are using <code>network-manager</code> with a WiFi connection,
+ you'll need to disable MAC address randomization, or your router / DHCP
+ server will not be able to consistently match to the device.
+
+ <pre><code>$ sudo mg /etc/NetworkManager/conf.d/wifi_rand_mac.conf</code></pre>
+
+ <pre><code>[device]
+wifi.scan-rand-mac-address=no</code></pre>
+ </p>
+ </aside>
+</p>
+
+<p>
+ Set up a firewall; I block all ports by default, and at most leave three ports
+ open: one for <code>SSH</code>, one for the camera livestream (e.g.,
+ <code>3000</code>), and optionally one for the web interface (e.g.,
+ <code>8080</code>).</p>
+
+<p>
+ If you intend to share the livestream over the internet, you'll need a relay
+ or a port forward from your router. Depending on your network setup, you may
+ need to enable hairpin NAT or split-horizon DNS to access the servers by their
+ domain name when on the local network.
+</p>
+
+<aside class="bordered br-yellow">
+ <p>
+ I don't recommend allowing for remote access to web interface; and would
+ advise limiting access to the livestream if possible. <code>motion</code> is
+ not written in a memory safe language; all security cameras are a
+ double-edged sword that can compromise your own privacy.
+ </p>
+</aside>
+
+<h2>SSH</h2>
+
+<p>Create an SSH keypair:
+ <pre><code>ssh-keygen -t ed25519</code></pre>
+</p>
+
+<p>
+ Then, copy the public key to the server:
+ <pre><code>ssh-copy-id -i ${SSH_KEY_PATH} username@10.0.1.101 </code></pre>
+</p>
+
+<p>Add the server to your SSH config:
+ <pre><code>Host kitchen
+ Hostname 10.0.1.101
+ IdentityFile ~/.ssh/keys/kitchen</code></pre>
+</p>
+
+<p>
+ On the server, disable root login and password authentication; enable public
+ key authentication.
+
+ <pre><code>$ sudo mg /etc/ssh/sshd_config</code></pre>
+
+ <pre><code>PermitRootLogin no
+PubkeyAuthentication yes
+PasswordAuthentication no</code></pre>
+
+</p>
+<p>
+ Restart the SSH daemon:
+ <pre><code>$ systemctl restart sshd</code></pre>
+</p>
+
+<h2>Motion</h2>
+<p>
+ <code><a href="https://motion-project.github.io/">motion</a></code> provides
+ the core features of the system — multiple cameras, live streams, web
+ control, motion detection, saving images and movies, timelapse, and event
+ triggers. Install it:
+ <pre><code>$ sudo apt-get install motion</code></pre>
+</p>
+
+<p>
+ On Raspberry Pi OS, the installation will create a
+ <code>motion</code> user, homed at <code>/var/lib/motion/</code>. The
+ configuration file for <code>motion</code> is located at
+ <code>/etc/motion/motion.conf</code>; the <code>systemd</code> unit file is
+ <code>/usr/lib/systemd/system/motion.service</code>.
+</p>
+
+<p>
+ <code>motion</code>'s behavior is controlled via its configuration file. The
+ <a
+ href="https://motion-project.github.io/motion_config.html">documentation</a>
+ covers the settings, and should be reviewed; situation will dictate which
+ values to use. I discuss configuration below, after setting up the other
+ components of the system.
+</p>
+
+<h2>Object Detection</h2>
+
+<a href="/static/media/1920/dwrz_20221026_1.jpg" >
+ <img class="img-center" src="/static/media/720/dwrz_20221026_1.jpg">
+</a>
+
+<p>
+ <code><a href="https://github.com/WongKinYiu/yolov7">yolov7</a></code>
+ provides the object detection functionality. I use the "tiny" weights for
+ faster processing on a Raspberry Pi. On a Raspberry Pi 400, I typically see
+ inferences complete under a second.
+
+ <pre><code>$ sudo apt-get install git pip
+$ sudo -u motion bash
+$ git clone https://github.com/WongKinYiu/yolov7.git
+$ cd yolov7/
+$ pip install -r requirements.txt
+$ wget https://github.com/WongKinYiu/yolov7/releases/download/v0.1/yolov7-tiny.pt
+$ mkdir -p /tmp/yolov7/</code></pre>
+</p>
+
+<p>
+ <a href="https://aws.amazon.com/rekognition/">AWS Rekognition</a> can be used
+ as an alternative to <code>yolov7</code>. I had better — and cheaper —
+ results with <code>yolov7</code>. However, if you encounter any issues with
+ the <code>yolov7</code> installation, AWS offers a convenient fallback.
+</p>
+
+<p>
+ To use Rekognition, you will need to setup an AWS account, install the
+ <code>aws</code> CLI, and ideally, create an IAM user with permissions
+ restricted to the Rekognition service. You will also need to
+ install <code><a href="https://stedolan.github.io/jq/">jq</a></code> to parse
+ the JSON response from AWS.
+
+ <pre><code>$ sudo apt-get install jq
+$ pip3 install --system awscli
+$ sudo -u motion bash
+$ aws configure</code></pre>
+</p>
+
+<h2>Notifications</h2>
+
+<a href="/static/media/1920/dwrz_20230124T165950_edit.jpg" >
+ <img class="img-center" src="/static/media/720/dwrz_20230124T165950_edit.jpg">
+</a>
+
+<p>
+ I've set things up so that notifications are only sent when two smartphones
+ are not reachable on the network. The upside is less notifications (though
+ they'll sometimes come through if the device goes to sleep). If you go this
+ route, you'll need need to set up static IP addresses for your devices, and
+ remember to take them with you.
+</p>
+
+<p>
+ I send SMS notifications by emailing my mobile phone number, setting the
+ recipient to something like <code>1234567890@msg.fi.google.com</code>. That
+ option may not be available depending on your mobile service provider. A
+ fallback would be to send notifications to an email address, or to use a
+ service like <a href="https://www.twilio.com/">Twilio</a> or
+ <a href="https://aws.amazon.com/sns/">AWS SNS</a>.
+</p>
+
+<p>
+ I include the object-detected camera snapshot in notifications. While it's
+ not too difficult to write a script or simple program to compose
+ <a href="https://en.wikipedia.org/wiki/MIME">MIME</a>
+ emails, it's easier to just install
+ <code><a href="http://www.mutt.org/">mutt</a></code>.
+
+ <pre><code>$ sudo apt-get install mutt</code></pre>
+</p>
+
+<p>
+ Configure <code>msmtp</code> and <code>mutt</code> for the <code>motion</code>
+ user:
+
+ <pre><code>$ sudo -u motion bash
+$ cd ~
+$ mg .msmtprc</code></pre>
+ <pre><code>defaults
+auth on
+tls on
+tls_trust_file /etc/ssl/certs/ca-certificates.crt
+logfile ~/.cache/msmtp.log
+
+account gmail
+host smtp.gmail.com
+port 587
+from user@example.com
+user user@example.com
+password ${PASSWORD}
+
+account default : gmail</code></pre>
+ <pre><code>$ chmod 600 .msmtprc
+$ mg .muttrc</code></pre>
+ <pre><code>set sendmail="/usr/bin/msmtp"
+set use_from=yes
+set from=user@example.com</code></pre>
+</p>
+
+<h2>Exporting Data</h2>
+
+<p>
+ I've configured the system to delete snapshots after sending the
+ corresponding notification. This makes it harder to retrieve data if someone
+ gains access to the server. However, since I want to be able to review past
+ snapshots and timelapse footage, I backup the data off the servers.
+</p>
+
+<p>
+ There are several options — <code>rsync</code> or <code>scp</code> files to
+ a remote server, perhaps one with an encrypted drive. Additionally, or
+ alternatively, the files can be backed up to the cloud, to a service like
+ <a href="https://aws.amazon.com/s3/">AWS S3</a> or <a
+ href="https://www.backblaze.com/b2/cloud-storage.html">Backblaze B2</a>.
+</p>
+
+<p>
+ For <code>b2</code>, I took the following steps:
+ <ol>
+ <li>Create an account.</li>
+ <li>Create a bucket.</li>
+ <li>
+ Setup lifecycle rules on the bucket to delete files after a certain
+ number of days.
+ </li>
+ <li>Create an application key.</li>
+ </ol>
+</p>
+<p>
+ On the servers, I install and configure the <code>b2</code> CLI.
+
+ <pre><code>$ sudo apt-get install backblaze-b2
+$ sudo -u motion bash
+$ backblaze-b2 authorize-account</code></pre>
+</p>
+
+<h2>DDNS</h2>
+<p>
+ To make the camera stream available remotely and conveniently — without a
+ <code>VPN</code>, port forwarding, or dealing with IP addresses — I use
+ subdomains to reach my cameras. You will need your own domain for similar
+ functionality.
+</p>
+
+<p>
+ I don't have a static IP from my ISP, so I use Dynamic DNS to keep my
+ subdomain records updated. A <code>systemd</code> timer regularly runs a
+ script to update the <code>A</code> and/or <code>AAAA</code> records for the
+ server's subdomain.
+</p>
+
+<p>
+ The public IP of the server is retrieved with a DNS lookup, using
+ <code>dig</code>. On Raspberry Pi OS, you'll need to install the
+ <code>dnsutils</code> package:
+
+ <pre><code>$ sudo apt-get intsall dnsutils</code></pre>
+</p>
+
+<p>
+ How you update your records will depend on your registrar. I use
+ <a href="https://aws.amazon.com/route53/">AWS Route53</a>, and a simple Go
+ program I wrote called <code>
+ <a href="https://code.dwrz.net/src/file/cmd/r53/main.go.html">
+ r53</a></code>, which wraps around the
+ <a href="https://github.com/aws/aws-sdk-go-v2/">AWS Go SDK</a> and
+ <code>dig</code>.
+</p>
+
+<p>
+ A script is probably easier to install. The following isn't as full featured
+ as <code>r53</code>, but it doesn't require compiling and installing a Go
+ binary:
+ <pre><code>#!/usr/bin/env bash
+
+readonly HZ="${AWS_HOSTED_ZONE}"
+readonly DOMAIN="${HOSTNAME}"
+
+err() {
+ echo "[$(date -u +'%Y-%m-%dT%H:%M:%S%:z')]: $*" >&2
+}
+
+main() {
+ if ! [[ -x "$(command -v aws)" ]]; then
+ err "aws cli not installed"; exit 1
+ fi
+
+ # Get the IP address.
+ ip="$(dig -4 +short myip.opendns.com @resolver1.opendns.com)"
+ if [[ -z "${ip}" ]]; then
+ err "failed to get ip address"; exit 2
+ fi
+ printf "ip: %s\n" "${ip}"
+
+ # Update the domains.
+ update='{
+ "Comment": "DDNS",
+ "Changes": [
+ {
+ "Action": "UPSERT",
+ "ResourceRecordSet": {
+ "Name": "'"${DOMAIN}"'",
+ "Type": "A",
+ "TTL": 300,
+ "ResourceRecords": [{ "Value": "'"${ip}"'" }]
+ }
+ }
+ ]
+}'
+
+ printf "requesting update for %s\n" "${DOMAIN}"
+ aws route53 change-resource-record-sets \
+ --hosted-zone-id "${HZ}" \
+ --change-batch "${update}"
+}
+
+main "$@"</code></pre>
+</p>
+
+<p>
+ If you are using AWS Route53, you'll need to install and setup the <code>aws</code> CLI for whichever user will run the DDNS service. Again, it's best to create an AWS IAM user with permissions limited to Route53.
+
+ <pre><code>$ pip3 install --system awscli
+$ sudo su
+# aws configure</pre></code>
+</p>
+
+<p>
+ <code>scp</code> the script or the <code>r53</code> binary to the server,
+ then move it and set appropriate permissions:
+ <pre><code>$ scp r53 user@server
+$ sudo mv r53 /usr/local/bin/
+$ sudo chmod 755 /usr/local/bin/r53</code></pre>
+</p>
+
+<p>
+ Test the command to ensure that it works:
+ <pre><code>$ r53 $HOSTNAME</code></pre>
+</p>
+
+<p>
+ These are the <code>systemd</code> unit and timer files — install them at
+ <code>/usr/lib/systemd/system/</code>:
+ <pre><code>[Unit]
+Description=DDNS
+RefuseManualStart=no
+RefuseManualStop=yes
+
+[Service]
+Type=oneshot
+ExecStart=ddns
+
+[Install]
+WantedBy=timers.target</code></pre>
+
+ <pre><code>[Unit]
+Description=DDNS
+RefuseManualStart=no
+RefuseManualStop=no
+
+[Timer]
+OnBootSec=1min
+OnCalendar=*-*-* *:*/5:00
+Persistent=true
+RandomizedDelaySec=15
+Unit=ddns.service
+
+[Install]
+WantedBy=default.target</code></pre>
+</p>
+<p>
+ Then, enable the timer:
+ <pre><code>$ systemctl enable --now ddns.timer</code></pre>
+</p>
+
+<a href="/static/media/1920/dwrz_20221018T191948_edit.jpg" >
+ <img class="img-center" src="/static/media/720/dwrz_20221018T191948_edit.jpg">
+</a>
+
+<h2>TLS Certificates</h2>
+
+<p>
+ <code>motion</code> will need TLS certificates to encrypt the livestream,
+ webcontrol, and authentication for each. We can get free certificates from <a
+ href="https://letsencrypt.org/">Let's Encrypt</a>, using <code><a
+ href="https://certbot.eff.org/">certbot</a></code>. The following assumes a
+ <code>
+ <a href="https://letsencrypt.org/docs/challenge-types/#dns-01-challenge">
+ dns-01
+ </a>
+ </code> challenge with Route53.
+</p>
+
+
+<p>
+ Install <code>certbot</code> and the <code>python3-certbot-dns-route53</code>
+ plugin:
+
+ <pre><code>$ sudo apt-get install certbot python3-certbot-dns-route53</code></pre>
+</p>
+
+<p>
+ Run <code>certbot</code> to generate certificates:
+
+ <pre><code>$ sudo certbot certonly \
+ --agree-tos \
+ --email user@example.com \
+ --non-interactive \
+ --quiet \
+ --verbose \
+ --dns-route53 \
+ -d ${DOMAIN}</code></pre>
+</p>
+
+<p>
+ Add the <code>motion</code> user to the <code>ssl-cert</code> group.
+
+ Then, change group ownership for access.
+
+ <pre><code>$ chown -R root:ssl-cert letsencrypt/archive/${DOMAIN}
+$ chown -R root:ssl-cert letsencrypt/live/${DOMAIN}
+$ chmod 440 letsencrypt/archive/${DOMAIN}/privkey1.pem</code></pre>
+</p>
+
+<a href="/static/media/1920/dwrz_20230104T104635_edit.jpg" >
+ <img class="img-center" src="/static/media/720/dwrz_20230104T104635_edit.jpg">
+</a>
+
+<h2>Motion Scripts</h2>
+
+<p>
+ We're nearly there. Three scripts are used to tie functionality together;
+ they should be copied over to <code>/var/lib/motion</code> and made
+ executable by the <code>motion</code> user. I use a
+ <a href="https://code.dwrz.net/vigil/file/setup.html">script</a> to make
+ installing the scripts a little easier.
+</p>
+
+<p>
+ An <code>alert</code> script is called when motion is detected; it sends a
+ notification via email.
+ <pre><code>#!/usr/bin/env bash
+
+# Devices to check -- if populated and up, no notifications are sent.
+readonly DEVICES=()
+readonly RECIPIENT="${NOTIFICATION_RECIPIENT}"
+
+check_devices() {
+ for device in "${DEVICES[@]}"; do
+ if ping -c 1 -w 1 "${device}" &> "/dev/null"; then
+ return 0
+ fi
+ done
+
+ return 255
+}
+
+main() {
+ # If devices are present, don't notify.
+ if (( "${#DEVICES[@]}" )); then
+ if check_devices; then
+ exit 0
+ fi
+ fi
+
+ echo "${HOSTNAME}: motion detected at $(date '+%Y-%m-%dT%H:%M:%S%:z')." | \
+ mutt -s "${HOSTNAME}: Motion Detected" \
+ -- "${RECIPIENT}"
+}
+
+main "$@"</code></pre>
+</p>
+
+<p>
+ The <code>sync</code> script is used to backup timelapse videos:
+ <pre><code>#!/usr/bin/env bash
+
+readonly BUCKET="${B2_BUCKET}"
+
+main() {
+ local filepath="$1"
+ local name
+ name="$(basename "${filepath}")"
+
+ backblaze-b2 upload-file \
+ --threads 2 \
+ "${BUCKET}" \
+ "${HOME}/timelapse/${name}" \
+ "${HOSTNAME}/timelapse/${name}"
+
+ # Delete outdated files.
+ # This assumes the timelapse is created on an hourly basis.
+ rm -f "$1"
+ find "${HOME}/timelapse/" -mmin +60 -delete
+}
+
+main "$@"</code></pre>
+</p>
+
+<p>
+ The <code>notify</code> script sends notifications, and backs up the
+ snapshots:
+
+ <pre><code>#!/usr/bin/env bash
+
+# Backblaze B2 Bucket
+readonly BUCKET="${B2_BUCKET}"
+
+# Devices to check -- if populated and up, no notifications are sent.
+readonly DEVICES=()
+
+# COCO Labels
+readonly LABEL_PERSON=0
+readonly LABEL_CAT=15
+
+# Lockfile to ensure that only one instance of the script is running.
+readonly LOCKFILE="/tmp/motion-notify.lock.d"
+
+# yolov7 working directory.
+readonly PROJECT="/tmp/yolov7"
+
+# Notification recipient.
+readonly RECIPIENT="${NOTIFICATION_RECIPIENT}"
+
+acquire_lock () {
+ while true; do
+ if mkdir "${LOCKFILE}"; then
+ break;
+ fi
+ sleep 1
+ done
+}
+
+check_devices() {
+ for device in "${DEVICES[@]}"; do
+ if ping -c 1 -w 1 "${device}" &> "/dev/null"; then
+ return 0
+ fi
+ done
+
+ return 255
+}
+
+detect_objects() {
+ local filepath="$1"
+
+ python "${HOME}/yolov7/detect.py" \
+ --exist-ok \
+ --no-trace \
+ --save-txt \
+ --project "${PROJECT}" \
+ --name "motion" \
+ --weights "${HOME}/yolov7/yolov7-tiny.pt" \
+ --source "${filepath}"
+}
+
+notify() {
+ local name="$1"
+
+ echo "${HOSTNAME} at $(date '+%Y-%m-%dT%H:%M:%S%:z')" | \
+ mutt -a "${PROJECT}/motion/${name}.jpg" \
+ -s "${HOSTNAME}: Motion Detected" \
+ -- "${RECIPIENT}"
+}
+
+upload() {
+ local name="$1"
+
+ backblaze-b2 upload-file \
+ --threads 2 \
+ "${BUCKET}" \
+ "${PROJECT}/motion/${name}.jpg" \
+ "${HOSTNAME}/photo/${name}.jpg"
+}
+
+delete_outdated() {
+ local filepath="$1"
+
+ acquire_lock
+
+ rm -f "$1"
+ rm -f "${PROJECT}/motion/${name}.jpg"
+ find "${HOME}/photo/" -mmin +5 -delete
+ find "${PROJECT}/motion/" -iname "*.jpg" -mmin +5 -delete
+ find "${PROJECT}/motion/labels/" -mmin +5 -delete
+
+ release_lock
+}
+
+release_lock () {
+ rmdir "${LOCKFILE}"
+}
+
+main() {
+ local filepath="$1"
+ local name
+ name="$(basename "${filepath}" .jpg)"
+
+ # If devices are present, don't notify.
+ if (( "${#DEVICES[@]}" )); then
+ if check_devices; then
+ delete_outdated "${filepath}" "${name}"
+ exit 0
+ fi
+ fi
+
+ detect_objects "${filepath}"
+
+ # Send a notification if we match any labels.
+ labels="$(awk '{print $1}' "${PROJECT}/motion/labels/${name}.txt")"
+ if echo "${labels}" | grep -qw "${LABEL_PERSON}\|${LABEL_CAT}"; then
+ notify "${name}"
+ fi
+
+ upload "${name}"
+
+ delete_outdated "${filepath}" "${name}"
+}
+
+main "$@"</code></pre>
+
+</p>
+
+<p>
+ With AWS Rekognition, you'll need to adapt the script. The following will
+ handle uploading the image to AWS, and check if the labels are actionable:
+
+ <pre><code>labels="$(env aws rekognition detect-labels \
+--min-confidence 90 \
+--image-bytes fileb://"${filepath}" \
+| jq -j '.Labels | .[] | "\n",.Name," ",.Confidence')"
+
+if grep --quiet "Human\|Cat" <<< "${labels}"; then
+ echo "${HOSTNAME} at $(date '+%Y-%m-%dT%H:%M:%S%:z')" | \
+ mutt -a "${filepath}" \
+ -s "${HOSTNAME}: Motion Detected" \
+ -- "${RECIPIENT}"
+fi</code></pre>
+</p>
+
+<h2>Motion Config</h2>
+
+<p>
+ The last step is to configure <code>motion</code> to:
+
+ <ul>
+ <li>Take snapshots on motion detection</li>
+ <li>Capture a timelapse — one photo per second, one file per hour, synced to the Backblaze B2</li>
+ <li>Serve webcontrol on port 8080 over HTTPS</li>
+ <li>Livestream on port 3000 over HTTPS</li>
+ <li>Notify on motion detection and send object-detected snapshots</li>
+ <li>Keep minimal amounts of data on the local drive</li>
+ </ul>
+</p>
+
+<p>
+ <pre><code># GENERAL
+daemon off
+target_dir ${MOTION_DIR}
+log_file ${MOTION_LOG_FILE}
+
+# IMAGE PROCESSING
+despeckle_filter EedDl
+framerate 24
+text_scale 2
+text_changes on
+text_left %$
+text_right %Y-%m-%d-T%H:%M:%S %q
+
+# MOTION DETECTION
+event_gap 1
+threshold 2000
+
+# MOVIES
+movie_output off
+movie_filename /video/%Y-%m-%dT%H:%M:%S-%v
+
+# PICTURES
+picture_output first
+picture_filename /photo/%Y-%m-%dT%H-%M-%S_%q
+
+# TIMELAPSE
+timelapse_interval 1
+timelapse_mode hourly
+timelapse_fps 60
+timelapse_codec mpg
+timelapse_filename /timelapse/%Y-%m-%d-%H-%M-%S
+
+# WEBCONTROL
+webcontrol_auth_method 2
+webcontrol_authentication ${MOTION_USER}:${MOTION_PASSWORD}
+webcontrol_port ${PORT_CONTROL}
+webcontrol_localhost off
+webcontrol_cert ${TLS_CERT}
+webcontrol_key ${TLS_KEY}
+webcontrol_parms 0
+webcontrol_tls on
+
+# LIVE STREAM
+stream_port ${PORT_STREAM}
+stream_localhost off
+stream_quality 25
+stream_motion on
+stream_maxrate 24
+stream_auth_method 2
+stream_authentication ${MOTION_USER}:${MOTION_PASSWORD}
+stream_preview_method 0
+stream_tls on
+
+# SCRIPTS
+on_motion_detected ${MOTION_DIR}/alert
+on_movie_end ${MOTION_DIR}/sync %f
+on_picture_save ${MOTION_DIR}/notify %f
+
+# CAMERA
+camera_name ${CAMERA_NAME}
+videodevice /dev/video0
+height 1080
+width 1920</code></pre>
+</p>
+
+<p>Restart <code>motion</code> to use the updated configuration:
+ <pre><code>$ systemctl restart motion</code></pre>
+</p>
+
+<h2>Camera Management</h2>
+
+<p>
+ I use a simple script to manage the cameras. This examples allows control
+ over three servers (one of which has two cameras):
+
+ <pre><code>#!/usr/bin/env bash
+
+readonly WEBCONTROL_PORT="${PORT_CONTROL}"
+
+readonly cameras=(
+ "https://${HOST0}:${WEBCONTROL_PORT}/0"
+# "https://${HOST0}:${WEBCONTROL_PORT}/1"
+# "https://${HOST1}:${WEBCONTROL_PORT}/${CAMERA0}"
+# "https://${HOST2}:${WEBCONTROL_PORT}/${CAMERA0}"
+)
+readonly auth=(
+ "${MOTION_USER}:${MOTION_PASSWORD}"
+# "${USER0}:${PW0}"
+# "${USER1}:${PW1}"
+# "${USER2}:${PW2}"
+)
+
+err() {
+ echo "[$(date -u +'%Y-%m-%dT%H:%M:%S%:z')]: $*" >&2
+}
+
+main() {
+ local url="detection/status"
+
+ case "$1" in
+ "capture"|"c") url="detection/snapshot" ;;
+ "pause"|"p") url="detection/pause" ;;
+ "start"|"s") url="detection/start" ;;
+ "status"|"") url="detection/status" ;;
+ *) err "unrecognized command: $1"; exit 1
+ esac
+
+ for i in "${!cameras[@]}"; do
+ curl --digest --user "${auth[i]}" "${cameras[i]}/${url}"
+ done
+}
+
+main "$@"</code></pre>
+</p>
+
+<h2>Next Steps</h2>
+
+<a href="/static/media/1920/dwrz_20220304T224024_edit.jpg" >
+ <img class="img-center" src="/static/media/720/dwrz_20220304T224024_edit.jpg">
+</a>
+
+<p>
+ As with all software, this project is a work-in-progress, at times
+ abandoned, and never completed. There are a few ideas I am exploring as I
+ continue to prototype the system:
+
+ <ul>
+ <li>
+ The most urgent task is to make it easier to set up a new server and to
+ keep configuration consistent across servers. I'm working on minimizing
+ some of the duplicative work. Another option is to use containers.
+ </li>
+ <li>
+ Use <a href="https://www.openbsd.org/">OpenBSD</a> instead of Raspberry
+ Pi OS, replacing <code>msmtp</code> with <code>OpenSTMTPD</code>,
+ <code>acme-client</code> for <code>letsencrypt</code>. The main concern
+ here is whether wireless and <code>yolov7</code> are sufficiently
+ performant.
+ </li>
+ <li>
+ Use an in-memory filesystem and forgo the SSD, which might drop ~$30
+ from the cost of the system.
+ </li>
+ <li>
+ Use different hardware — wireless or PoE security cameras with an RTSP
+ stream and motion running on a single server.
+ </li>
+ <li>
+ Improve object detection with <code>yolov7</code> by training the model.
+ </li>
+ <li>
+ Develop my own Go service to replace or wrap around <code>motion</code>,
+ or replace the bash scripts with Go programs.
+ </li>
+ <li>
+ Add features, like two-way communication.
+ </li>
+ <li>
+ Use <code>yolov7</code> to monitor the camera stream directly, and forgo
+ the motion detection step.
+ </li>
+ </ul>
+</p>
+
+<p>
+ The final thanks must go to the open-source contributors who have made this
+ approach possible, from the operating system all the way up to the
+ interpreters.
+</p>
diff --git a/cmd/web/site/entry/static/2023-02-01/metadata.json b/cmd/web/site/entry/static/2023-02-01/metadata.json
@@ -0,0 +1,6 @@
+{
+ "cover": "dwrz_20221023T155810_edit.jpg",
+ "date": "2023-02-01T00:00:00Z",
+ "published": true,
+ "title": "Security Cameras"
+}