src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

2023-02-01.html (31677B)


      1 <div class="wide64">
      2   <a href="/static/media/1920/dwrz_20221023T155810_edit.jpg" >
      3     <img class="img-center"
      4          src="/static/media/720/dwrz_20221023T155810_edit.jpg">
      5   </a>
      6   <p>
      7     I live in a ground floor apartment, and want to keep an eye on the space and
      8     the cats when I am away. A search for security camera systems that offered
      9     timelapse recording, livestream viewing, and instrusion notifications was
     10     unsatisfactory. Most consumer systems were either too expensive, had security
     11     shortcomings, or lacked sufficient user control. As a result, I endend up
     12     assembling a system that meets most of my needs.
     13   </p>
     14   <p>
     15     The solution I've settled on uses three
     16     <a href="https://www.raspberrypi.org/"> Raspberry Pi</a>'s, each acting as a
     17     server connected to a <a href="https://www.webcamerausb.com/">generic fisheye
     18       USB camera</a>. A mix of open-source software and scripts provides a
     19     password protected livestream served over HTTPS, timelapse recording, motion
     20     and object detection, notifications, and remote storage.
     21   </p>
     22   <p>
     23     The cameras come with limitations and vulnerabilties, some shared with
     24     consumer solutions, others unique to a home-brewed setup. But for my needs,
     25     they have worked well, and I have appreciate their modularity, the ability
     26     to repurpose hardware, and full control over the system and the data that it
     27     generates.
     28   </p>
     29   <p>
     30     I was surprised by how quickly I could stand up a system of such disparate
     31     parts — in terms of hardware and software — while writing little code of my
     32     own. Putting these cameras together seemed to confirm some of the
     33     <a href="https://en.wikipedia.org/wiki/Unix_philosophy#Origin">UNIX</a>
     34     principles. It's been possible to connect components with just a few scripts
     35     as glue.
     36   </p>
     37   <p>
     38     Any code referenced on this page should be available here:
     39     <a href="https://code.dwrz.net/vigil/">https://code.dwrz.net/vigil/</a>.
     40     I don't intend to keep code on this page up to date; it should only be used
     41     for example and inspiration.
     42   </p>
     43   <p>
     44     What follows is a rough guide covering the basic components of the system.
     45     It is not intended to be a step-by-step guide, though there is a chance it
     46     might work as one.
     47   </p>
     48   <h2>Hardware</h2>
     49   <a href="/static/media/1920/dwrz_20221022T213439.jpg" >
     50     <img class="img-center" src="/static/media/720/dwrz_20221022T213439.jpg">
     51   </a>
     52   <p>
     53     I've opted for the following:
     54     <ul>
     55       <li>
     56         <a href="https://www.raspberrypi.com/products/raspberry-pi-400/">Raspberry
     57 	  Pi 400</a> — easier to source and repurpose for my use cases. I would
     58         have preferred a smaller device with more open hardware and USB ports,
     59         but it was hard to find anything of comparable price.
     60       </li>
     61       <li>
     62         <a
     63 	  href="https://www.westerndigital.com/products/usb-flash-drives/sandisk-extreme-pro-usb-3-2">
     64 	  Sandisk SSD Flash Drive</a> — improves performance and reliability
     65         compared to running the Raspberry Pi off of a MicroSD card.
     66       </li>
     67       <li>
     68         <a
     69 	  href="https://www.amazon.com/s?me=AVRDPNYMU6GNM&marketplaceID=ATVPDKIKX0DER">
     70 	  ELP 3.6mm FHD 180° IR Fisheye Camera</a> — can cover an entire room and
     71         see in the dark.
     72       </li>
     73       <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>
     74     </ul>
     75   </p>
     76   <h2>Server</h2>
     77   <img class="img-center" src="/static/media/vigil-diagram.svg">
     78   <p>
     79     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.
     80   </p>
     81   <p>
     82     You should be mindful of the security of the servers themselves. This is a
     83     <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>.
     84     Your personal circumstances will dictate the balance of features and security.
     85   </p>
     86 
     87   <aside class="bordered br-yellow">
     88     <p>
     89       When installing the operating system, you'll have to decide whether to
     90       encrypt the filesystem (or a partition). Encryption makes it harder to
     91       extract data if someone is able to gain physical access to the
     92       storage device. However, entering the decryption passwords requires you to
     93       be physically present at the machine. This can be a problem if, e.g., a
     94       transient power outage happens while you are away from the cameras; they
     95       would cease to function until you are on premises again. It's possible to
     96       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,
     97       to enter the decryption password. But I've opted to use the servers without
     98       encryption, and transfer photos and videos off of the servers as soon as
     99       possible. The downside is that any secrets on the device can be
    100       compromised, requiring remediation in the event of a breach.
    101     </p>
    102   </aside>
    103   <p>
    104     Once you are up and running with Raspberry Pi OS, ensure you are
    105     using the latest software and security updates:
    106   </p>
    107   <pre><code>$ sudo apt-get -y update && sudo apt-get -y dist-upgrade</code></pre>
    108   <p>
    109     Install any dependencies necessary to get work done, e.g.:
    110   </p>
    111   <pre><code>$ sudo apt-get install curl git mg</code></pre>
    112   <p>
    113     Consider enabling <a href="https://wiki.debian.org/UnattendedUpgrades">unattended
    114     security upgrades</a>:
    115   </p>
    116   <pre><code>$ sudo apt-get install unattended-upgrades apt-listchanges</code></pre>
    117   <p>
    118     To receive email reports for unattended upgrades, a
    119     <a href="https://en.wikipedia.org/wiki/Message_transfer_agent">
    120       Message Transfer Agent
    121     </a> (MTA) and the <code>mailx</code> command are required. This guide
    122     assumes the use of <code><a href="https://marlam.de/msmtp/">msmtp</a></code>:
    123   </p>
    124   <pre><code>$ sudo apt-get install bsd-mailx msmtp msmtp-mta</code></pre>
    125   <p>
    126     Setup the <code>msmtp</code>
    127     <a href="https://marlam.de/msmtp/msmtp.html#Configuration-files">configuration</a>
    128     file for the <code>root</code> user:
    129   </p>
    130   <pre><code>$ sudo mg /root/.msmtprc</code></pre>
    131   <pre><code>defaults
    132 auth            on
    133 tls             on
    134 tls_trust_file  /etc/ssl/certs/ca-certificates.crt
    135 logfile         /root/.msmtp.log
    136 
    137 account         gmail
    138 host            smtp.gmail.com
    139 port            587
    140 from            user@example.com
    141 user            user@example.com
    142 password        ${PASSWORD}
    143 # Alternatively, a command may be used to retrieve the password:
    144 # passwordeval   pass google/gmail/app
    145 # See: https://marlam.de/msmtp/msmtp.html#passwordeval.
    146 
    147 account default : gmail</code></pre>
    148   <p>
    149     This configuration assumes a Gmail or Google Workspace account; you will
    150     need to specify appropriate settings for your own mail provider. If you are
    151     using Gmail or Google Workspaces, you will need to set up an "app password"
    152     for programmatic access.
    153   </p>
    154   <p>
    155     Test that <code>msmtp</code> is working:
    156   </p>
    157   <pre><code>echo "Test" | mailx -s "Test" user@example.com</code></pre>
    158   <p>
    159     Edit the <code>unattended-upgrades</code> configuration to send email
    160     notifications:
    161   </p>
    162   <pre><code>$ sudo mg /etc/apt/apt.conf.d/50unattended-upgrades</code></pre>
    163   <pre><code>Unattended-Upgrade::Mail "user@example.com";</code></pre>
    164   <h2>Networking</h2>
    165   <p>
    166     I assign a static IP address for each of
    167     my servers. Most consumer routers allow for this in their web interface; I
    168     have something like the following in my router's <code>/etc/dhcpd.conf</code>:
    169     <pre><code>host kitchen {
    170 	fixed-address 10.0.1.101;
    171 	hardware ethernet de:ad:be:ef:8d:8e;
    172 }</code></pre>
    173   </p>
    174   <aside class="bordered br-yellow">
    175     <p>
    176       If you are using <code>network-manager</code> with a WiFi connection,
    177       you'll need to disable MAC address randomization, or your router / DHCP
    178       server will not be able to consistently match to the device.
    179     </p>
    180     <br>
    181     <pre><code>$ sudo mg /etc/NetworkManager/conf.d/wifi_rand_mac.conf</code></pre>
    182     <br>
    183     <pre><code>[device]
    184 wifi.scan-rand-mac-address=no</code></pre>
    185   </aside>
    186   <p>
    187     Set up a firewall; I block all ports by default, and at most leave three ports
    188     open: one for <code>SSH</code>, one for the camera livestream (e.g.,
    189     <code>3000</code>), and optionally one for the web interface (e.g.,
    190     <code>8080</code>).</p>
    191   <p>
    192     If you intend to share the livestream over the internet, you'll need a relay
    193     or a port forward from your router. Depending on your network setup, you may
    194     need to enable hairpin NAT or split-horizon DNS to access the servers by their
    195     domain name when on the local network.
    196   </p>
    197   <aside class="bordered br-yellow">
    198     <p>
    199       I don't recommend allowing for remote access to web interface; and would
    200       advise limiting access to the livestream if possible. <code>motion</code> is
    201       not written in a memory safe language; all security cameras are a
    202       double-edged sword that can compromise your own privacy.
    203     </p>
    204   </aside>
    205   <h2>SSH</h2>
    206   <p>Create an SSH keypair:
    207   </p>
    208   <pre><code>ssh-keygen -t ed25519</code></pre>
    209   <p>
    210     Then, copy the public key to the server:
    211   </p>
    212   <pre><code>ssh-copy-id -i ${SSH_KEY_PATH} username@10.0.1.101 </code></pre>
    213   <p>Add the server to your SSH config:
    214   </p>
    215   <pre><code>Host kitchen
    216 	Hostname 10.0.1.101
    217 	IdentityFile ~/.ssh/keys/kitchen</code></pre>
    218   <p>
    219     On the server, disable root login and password authentication; enable public
    220     key authentication.
    221   </p>
    222   <pre><code>$ sudo mg /etc/ssh/sshd_config</code></pre>
    223   <pre><code>PermitRootLogin no
    224 PubkeyAuthentication yes
    225 PasswordAuthentication no</code></pre>
    226   <p>
    227     Restart the SSH daemon:
    228   </p>
    229   <pre><code>$ systemctl restart sshd</code></pre>
    230   <h2>Motion</h2>
    231   <p>
    232     <code><a href="https://motion-project.github.io/">motion</a></code> provides
    233     the core features of the system — multiple cameras, live streams, web
    234     control, motion detection, saving images and movies, timelapse, and event
    235     triggers. Install it:
    236   </p>
    237   <pre><code>$ sudo apt-get install motion</code></pre>
    238   <p>
    239     On Raspberry Pi OS, the installation will create a
    240     <code>motion</code> user, homed at <code>/var/lib/motion/</code>. The
    241     configuration file for <code>motion</code> is located at
    242     <code>/etc/motion/motion.conf</code>; the <code>systemd</code> unit file is
    243     <code>/usr/lib/systemd/system/motion.service</code>.
    244   </p>
    245   <p>
    246     <code>motion</code>'s behavior is controlled via its configuration file. The
    247     <a
    248       href="https://motion-project.github.io/motion_config.html">documentation</a>
    249     covers the settings, and should be reviewed; situation will dictate which
    250     values to use. I discuss configuration below, after setting up the other
    251     components of the system.
    252   </p>
    253   <h2>Object Detection</h2>
    254   <a href="/static/media/1920/dwrz_20221026_1.jpg" >
    255     <img class="img-center" src="/static/media/720/dwrz_20221026_1.jpg">
    256   </a>
    257   <p>
    258     <code><a href="https://github.com/WongKinYiu/yolov7">yolov7</a></code>
    259     provides the object detection functionality. I use the "tiny" weights for
    260     faster processing on a Raspberry Pi. On a Raspberry Pi 400, I typically see
    261     inferences complete under a second.
    262   </p>
    263   <pre><code>$ sudo apt-get install git pip
    264 $ sudo -u motion bash
    265 $ git clone https://github.com/WongKinYiu/yolov7.git
    266 $ cd yolov7/
    267 $ pip install -r requirements.txt
    268 $ wget https://github.com/WongKinYiu/yolov7/releases/download/v0.1/yolov7-tiny.pt
    269 $ mkdir -p /tmp/yolov7/</code></pre>
    270   <p>
    271     <a href="https://aws.amazon.com/rekognition/">AWS Rekognition</a> can be used
    272     as an alternative to <code>yolov7</code>. I had better — and cheaper —
    273     results with <code>yolov7</code>. However, if you encounter any issues with
    274     the <code>yolov7</code> installation, AWS offers a convenient fallback.
    275   </p>
    276   <p>
    277     To use Rekognition, you will need to setup an AWS account, install the
    278     <code>aws</code> CLI, and ideally, create an IAM user with permissions
    279     restricted to the Rekognition service. You will also need to
    280     install <code><a href="https://stedolan.github.io/jq/">jq</a></code> to
    281     parse the JSON response from AWS.
    282   </p>
    283   <pre><code>$ sudo apt-get install jq
    284 $ pip3 install --system awscli
    285 $ sudo -u motion bash
    286 $ aws configure</code></pre>
    287   <h2>Notifications</h2>
    288   <a href="/static/media/1920/dwrz_20230124T165950_edit.jpg" >
    289     <img class="img-center" src="/static/media/720/dwrz_20230124T165950_edit.jpg">
    290   </a>
    291   <p>
    292     I've set things up so that notifications are only sent when two smartphones
    293     are not reachable on the network. The upside is less notifications (though
    294     they'll sometimes come through if the device goes to sleep). If you go this
    295     route, you'll need need to set up static IP addresses for your devices, and
    296     remember to take them with you.
    297   </p>
    298   <p>
    299     I send SMS notifications by emailing my mobile phone number, setting the
    300     recipient to something like <code>1234567890@msg.fi.google.com</code>. That
    301     option may not be available depending on your mobile service provider. A
    302     fallback would be to send notifications to an email address, or to use a
    303     service like <a href="https://www.twilio.com/">Twilio</a> or
    304     <a href="https://aws.amazon.com/sns/">AWS SNS</a>.
    305   </p>
    306   <p>
    307     I include the object-detected camera snapshot in notifications. While it's
    308     not too difficult to write a script or simple program to compose
    309     <a href="https://en.wikipedia.org/wiki/MIME">MIME</a>
    310     emails, it's easier to just install
    311     <code><a href="http://www.mutt.org/">mutt</a></code>.
    312   </p>
    313   <pre><code>$ sudo apt-get install mutt</code></pre>
    314   <p>
    315     Configure <code>msmtp</code> and <code>mutt</code> for the <code>motion</code>
    316     user:
    317   </p>
    318   <pre><code>$ sudo -u motion bash
    319 $ cd ~
    320 $ mg .msmtprc</code></pre>
    321   <pre><code>defaults
    322 auth            on
    323 tls             on
    324 tls_trust_file  /etc/ssl/certs/ca-certificates.crt
    325 logfile         ~/.cache/msmtp.log
    326 
    327 account         gmail
    328 host            smtp.gmail.com
    329 port            587
    330 from            user@example.com
    331 user            user@example.com
    332 password        ${PASSWORD}
    333 
    334 account default : gmail</code></pre>
    335   <pre><code>$ chmod 600 .msmtprc
    336 $ mg .muttrc</code></pre>
    337   <pre><code>set sendmail="/usr/bin/msmtp"
    338 set use_from=yes
    339 set from=user@example.com</code></pre>
    340   <h2>Exporting Data</h2>
    341   <p>
    342     I've configured the system to delete snapshots after sending the
    343     corresponding notification. This makes it harder to retrieve data if someone
    344     gains access to the server. However, since I want to be able to review past
    345     snapshots and timelapse footage, I backup the data off the servers.
    346   </p>
    347   <p>
    348     There are several options — <code>rsync</code> or <code>scp</code> files to
    349     a remote server, perhaps one with an encrypted drive. Additionally, or
    350     alternatively, the files can be backed up to the cloud, to a service like
    351     <a href="https://aws.amazon.com/s3/">AWS S3</a> or <a
    352 						         href="https://www.backblaze.com/b2/cloud-storage.html">Backblaze B2</a>.
    353   </p>
    354   <p>
    355     For <code>b2</code>, I took the following steps:
    356     <ol>
    357       <li>Create an account.</li>
    358       <li>Create a bucket.</li>
    359       <li>
    360         Setup lifecycle rules on the bucket to delete files after a certain
    361         number of days.
    362       </li>
    363       <li>Create an application key.</li>
    364     </ol>
    365   </p>
    366   <p>
    367     On the servers, I install and configure the <code>b2</code> CLI.
    368   </p>
    369   <pre><code>$ sudo apt-get install backblaze-b2
    370 $ sudo -u motion bash
    371 $ backblaze-b2 authorize-account</code></pre>
    372   <h2>DDNS</h2>
    373   <p>
    374     To make the camera stream available remotely and conveniently — without a
    375     <code>VPN</code>, port forwarding, or dealing with IP addresses — I use
    376     subdomains to reach my cameras. You will need your own domain for similar
    377     functionality.
    378   </p>
    379   <p>
    380     I don't have a static IP from my ISP, so I use Dynamic DNS to keep my
    381     subdomain records updated. A <code>systemd</code> timer regularly runs a
    382     script to update the <code>A</code> and/or <code>AAAA</code> records for the
    383     server's subdomain.
    384   </p>
    385   <p>
    386     The public IP of the server is retrieved with a DNS lookup, using
    387     <code>dig</code>. On Raspberry Pi OS, you'll need to install the
    388     <code>dnsutils</code> package:
    389   </p>
    390   <pre><code>$ sudo apt-get intsall dnsutils</code></pre>
    391   <p>
    392     How you update your records will depend on your registrar. I use
    393     <a href="https://aws.amazon.com/route53/">AWS Route53</a>, and a simple Go
    394     program I wrote called <code>
    395       <a href="https://code.dwrz.net/src/file/cmd/r53/main.go.html">
    396         r53</a></code>, which wraps around the
    397       <a href="https://github.com/aws/aws-sdk-go-v2/">AWS Go SDK</a> and
    398       <code>dig</code>.
    399   </p>
    400   <p>
    401     A script is probably easier to install. The following isn't as full featured
    402     as <code>r53</code>, but it doesn't require compiling and installing a Go
    403     binary:
    404   </p>
    405   <pre><code>#!/usr/bin/env bash
    406 
    407 readonly HZ="${AWS_HOSTED_ZONE}"
    408 readonly DOMAIN="${HOSTNAME}"
    409 
    410 err() {
    411   echo "[$(date -u +'%Y-%m-%dT%H:%M:%S%:z')]: $*" >&2
    412 }
    413 
    414 main() {
    415   if ! [[ -x "$(command -v aws)" ]]; then
    416     err "aws cli not installed"; exit 1
    417   fi
    418 
    419   # Get the IP address.
    420   ip="$(dig -4 +short myip.opendns.com @resolver1.opendns.com)"
    421   if [[ -z "${ip}" ]]; then
    422     err "failed to get ip address"; exit 2
    423   fi
    424   printf "ip: %s\n" "${ip}"
    425 
    426   # Update the domains.
    427   update='{
    428   "Comment": "DDNS",
    429   "Changes": [
    430    {
    431      "Action": "UPSERT",
    432      "ResourceRecordSet": {
    433        "Name": "'"${DOMAIN}"'",
    434        "Type": "A",
    435        "TTL": 300,
    436        "ResourceRecords": [{ "Value": "'"${ip}"'" }]
    437      }
    438    }
    439  ]
    440 }'
    441 
    442   printf "requesting update for %s\n" "${DOMAIN}"
    443   aws route53 change-resource-record-sets \
    444       --hosted-zone-id "${HZ}" \
    445       --change-batch "${update}"
    446 }
    447 
    448 main "$@"</code></pre>
    449   <p>
    450     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.
    451   </p>
    452   <pre><code>$ pip3 install --system awscli
    453 $ sudo su
    454 # aws configure</pre></code>
    455   <p>
    456     <code>scp</code> the script or the <code>r53</code> binary to the server,
    457     then move it and set appropriate permissions:
    458   </p>
    459   <pre><code>$ scp r53 user@server
    460 $ sudo mv r53 /usr/local/bin/
    461 $ sudo chmod 755 /usr/local/bin/r53</code></pre>
    462   <p>
    463     Test the command to ensure that it works:
    464   </p>
    465   <pre><code>$ r53 $HOSTNAME</code></pre>
    466   <p>
    467     These are the <code>systemd</code> unit and timer files — install them at
    468     <code>/usr/lib/systemd/system/</code>:
    469   </p>
    470   <pre><code>[Unit]
    471 Description=DDNS
    472 RefuseManualStart=no
    473 RefuseManualStop=yes
    474 
    475 [Service]
    476 Type=oneshot
    477 ExecStart=ddns
    478 
    479 [Install]
    480 WantedBy=timers.target</code></pre>
    481 
    482   <pre><code>[Unit]
    483 Description=DDNS
    484 RefuseManualStart=no
    485 RefuseManualStop=no
    486 
    487 [Timer]
    488 OnBootSec=1min
    489 OnCalendar=*-*-* *:*/5:00
    490 Persistent=true
    491 RandomizedDelaySec=15
    492 Unit=ddns.service
    493 
    494 [Install]
    495 WantedBy=default.target</code></pre>
    496   <p>
    497     Then, enable the timer:
    498   </p>
    499   <pre><code>$ systemctl enable --now ddns.timer</code></pre>
    500   <a href="/static/media/1920/dwrz_20221018T191948_edit.jpg" >
    501     <img class="img-center" src="/static/media/720/dwrz_20221018T191948_edit.jpg">
    502   </a>
    503   <h2>TLS Certificates</h2>
    504   <p>
    505     <code>motion</code> will need TLS certificates to encrypt the livestream,
    506     webcontrol, and authentication for each. We can get free certificates from <a
    507 									         href="https://letsencrypt.org/">Let's Encrypt</a>, using <code><a
    508 																		  href="https://certbot.eff.org/">certbot</a></code>. The following assumes a
    509     <code>
    510       <a href="https://letsencrypt.org/docs/challenge-types/#dns-01-challenge">
    511         dns-01
    512       </a>
    513     </code> challenge with Route53.
    514   </p>
    515   <p>
    516     Install <code>certbot</code> and the <code>python3-certbot-dns-route53</code>
    517     plugin:
    518   </p>
    519   <pre><code>$ sudo apt-get install certbot python3-certbot-dns-route53</code></pre>
    520   <p>
    521     Run <code>certbot</code> to generate certificates:
    522   </p>
    523   <pre><code>$ sudo certbot certonly \
    524     --agree-tos \
    525     --email user@example.com \
    526     --non-interactive \
    527     --quiet \
    528     --verbose \
    529     --dns-route53 \
    530     -d ${DOMAIN}</code></pre>
    531   <p>
    532     Add the <code>motion</code> user to the <code>ssl-cert</code> group.
    533 
    534     Then, change group ownership for access.
    535   </p>
    536   <pre><code>$ chown -R root:ssl-cert letsencrypt/archive/${DOMAIN}
    537 $ chown -R root:ssl-cert letsencrypt/live/${DOMAIN}
    538 $ chmod 440 letsencrypt/archive/${DOMAIN}/privkey1.pem</code></pre>
    539   <a href="/static/media/1920/dwrz_20230104T104635_edit.jpg" >
    540     <img class="img-center" src="/static/media/720/dwrz_20230104T104635_edit.jpg">
    541   </a>
    542   <h2>Motion Scripts</h2>
    543   <p>
    544     We're nearly there. Three scripts are used to tie functionality together;
    545     they should be copied over to <code>/var/lib/motion</code> and made
    546     executable by the <code>motion</code> user. I use a
    547     <a href="https://code.dwrz.net/vigil/file/setup.html">script</a> to make
    548     installing the scripts a little easier.
    549   </p>
    550   <p>
    551     An <code>alert</code> script is called when motion is detected; it sends a
    552     notification via email.
    553   </p>
    554   <pre><code>#!/usr/bin/env bash
    555 
    556 # Devices to check -- if populated and up, no notifications are sent.
    557 readonly DEVICES=()
    558 readonly RECIPIENT="${NOTIFICATION_RECIPIENT}"
    559 
    560 check_devices() {
    561   for device in "${DEVICES[@]}"; do
    562     if ping -c 1 -w 1 "${device}" &> "/dev/null"; then
    563       return 0
    564     fi
    565   done
    566 
    567   return 255
    568 }
    569 
    570 main() {
    571   # If devices are present, don't notify.
    572   if (( "${#DEVICES[@]}" )); then
    573     if check_devices; then
    574       exit 0
    575     fi
    576   fi
    577 
    578   echo "${HOSTNAME}: motion detected at $(date '+%Y-%m-%dT%H:%M:%S%:z')." | \
    579     mutt -s "${HOSTNAME}: Motion Detected" \
    580 	 -- "${RECIPIENT}"
    581 }
    582 
    583 main "$@"</code></pre>
    584   <p>
    585     The <code>sync</code> script is used to backup timelapse videos:
    586   </p>
    587   <pre><code>#!/usr/bin/env bash
    588 
    589 readonly BUCKET="${B2_BUCKET}"
    590 
    591 main() {
    592   local filepath="$1"
    593   local name
    594   name="$(basename "${filepath}")"
    595 
    596   backblaze-b2 upload-file \
    597 	       --threads 2 \
    598 	       "${BUCKET}" \
    599 	       "${HOME}/timelapse/${name}" \
    600 	       "${HOSTNAME}/timelapse/${name}"
    601 
    602   # Delete outdated files.
    603   # This assumes the timelapse is created on an hourly basis.
    604   rm -f "$1"
    605   find "${HOME}/timelapse/" -mmin +60 -delete
    606 }
    607 
    608 main "$@"</code></pre>
    609   <p>
    610     The <code>notify</code> script sends notifications, and backs up the
    611     snapshots:
    612   </p>
    613   <pre><code>#!/usr/bin/env bash
    614 
    615 # Backblaze B2 Bucket
    616 readonly BUCKET="${B2_BUCKET}"
    617 
    618 # Devices to check -- if populated and up, no notifications are sent.
    619 readonly DEVICES=()
    620 
    621 # COCO Labels
    622 readonly LABEL_PERSON=0
    623 readonly LABEL_CAT=15
    624 
    625 # Lockfile to ensure that only one instance of the script is running.
    626 readonly LOCKFILE="/tmp/motion-notify.lock.d"
    627 
    628 # yolov7 working directory.
    629 readonly PROJECT="/tmp/yolov7"
    630 
    631 # Notification recipient.
    632 readonly RECIPIENT="${NOTIFICATION_RECIPIENT}"
    633 
    634 acquire_lock () {
    635   while true; do
    636     if mkdir "${LOCKFILE}"; then
    637       break;
    638     fi
    639     sleep 1
    640   done
    641 }
    642 
    643 check_devices() {
    644   for device in "${DEVICES[@]}"; do
    645     if ping -c 1 -w 1 "${device}" &> "/dev/null"; then
    646       return 0
    647     fi
    648   done
    649 
    650   return 255
    651 }
    652 
    653 detect_objects() {
    654   local filepath="$1"
    655 
    656   python "${HOME}/yolov7/detect.py" \
    657 	 --exist-ok \
    658 	 --no-trace \
    659 	 --save-txt \
    660 	 --project "${PROJECT}" \
    661 	 --name "motion" \
    662 	 --weights "${HOME}/yolov7/yolov7-tiny.pt" \
    663 	 --source "${filepath}"
    664 }
    665 
    666 notify() {
    667   local name="$1"
    668 
    669   echo "${HOSTNAME} at $(date '+%Y-%m-%dT%H:%M:%S%:z')" | \
    670     mutt -a "${PROJECT}/motion/${name}.jpg" \
    671 	 -s "${HOSTNAME}: Motion Detected" \
    672 	 -- "${RECIPIENT}"
    673 }
    674 
    675 upload() {
    676   local name="$1"
    677 
    678   backblaze-b2 upload-file \
    679 	       --threads 2 \
    680 	       "${BUCKET}" \
    681 	       "${PROJECT}/motion/${name}.jpg" \
    682 	       "${HOSTNAME}/photo/${name}.jpg"
    683 }
    684 
    685 delete_outdated() {
    686   local filepath="$1"
    687 
    688   acquire_lock
    689 
    690   rm -f "$1"
    691   rm -f "${PROJECT}/motion/${name}.jpg"
    692   find "${HOME}/photo/" -mmin +5 -delete
    693   find "${PROJECT}/motion/" -iname "*.jpg" -mmin +5 -delete
    694   find "${PROJECT}/motion/labels/" -mmin +5 -delete
    695 
    696   release_lock
    697 }
    698 
    699 release_lock () {
    700   rmdir "${LOCKFILE}"
    701 }
    702 
    703 main() {
    704   local filepath="$1"
    705   local name
    706   name="$(basename "${filepath}" .jpg)"
    707 
    708   # If devices are present, don't notify.
    709   if (( "${#DEVICES[@]}" )); then
    710     if check_devices; then
    711       delete_outdated "${filepath}" "${name}"
    712       exit 0
    713     fi
    714   fi
    715 
    716   detect_objects "${filepath}"
    717 
    718   # Send a notification if we match any labels.
    719   labels="$(awk '{print $1}' "${PROJECT}/motion/labels/${name}.txt")"
    720   if echo "${labels}" | grep -qw "${LABEL_PERSON}\|${LABEL_CAT}"; then
    721     notify "${name}"
    722   fi
    723 
    724   upload "${name}"
    725 
    726   delete_outdated "${filepath}" "${name}"
    727 }
    728 
    729 main "$@"</code></pre>
    730   <p>
    731     With AWS Rekognition, you'll need to adapt the script. The following will
    732     handle uploading the image to AWS, and check if the labels are actionable:
    733   </p>
    734   <pre><code>labels="$(env aws rekognition detect-labels \
    735 --min-confidence 90 \
    736 --image-bytes fileb://"${filepath}" \
    737 | jq -j '.Labels | .[] | "\n",.Name," ",.Confidence')"
    738 
    739 if grep --quiet "Human\|Cat" <<< "${labels}"; then
    740   echo "${HOSTNAME} at $(date '+%Y-%m-%dT%H:%M:%S%:z')" | \
    741     mutt -a "${filepath}" \
    742 	 -s "${HOSTNAME}: Motion Detected" \
    743 	 -- "${RECIPIENT}"
    744 fi</code></pre>
    745   <h2>Motion Config</h2>
    746   <p>
    747     The last step is to configure <code>motion</code> to:
    748     <ul>
    749       <li>Take snapshots on motion detection</li>
    750       <li>Capture a timelapse — one photo per second, one file per hour, synced to the Backblaze B2</li>
    751       <li>Serve webcontrol on port 8080 over HTTPS</li>
    752       <li>Livestream on port 3000 over HTTPS</li>
    753       <li>Notify on motion detection and send object-detected snapshots</li>
    754       <li>Keep minimal amounts of data on the local drive</li>
    755     </ul>
    756   </p>
    757   <pre><code># GENERAL
    758 daemon off
    759 target_dir ${MOTION_DIR}
    760 log_file ${MOTION_LOG_FILE}
    761 
    762 # IMAGE PROCESSING
    763 despeckle_filter EedDl
    764 framerate 24
    765 text_scale 2
    766 text_changes on
    767 text_left %$
    768 text_right %Y-%m-%d-T%H:%M:%S %q
    769 
    770 # MOTION DETECTION
    771 event_gap 1
    772 threshold 2000
    773 
    774 # MOVIES
    775 movie_output off
    776 movie_filename /video/%Y-%m-%dT%H:%M:%S-%v
    777 
    778 # PICTURES
    779 picture_output first
    780 picture_filename /photo/%Y-%m-%dT%H-%M-%S_%q
    781 
    782 # TIMELAPSE
    783 timelapse_interval 1
    784 timelapse_mode hourly
    785 timelapse_fps 60
    786 timelapse_codec mpg
    787 timelapse_filename /timelapse/%Y-%m-%d-%H-%M-%S
    788 
    789 # WEBCONTROL
    790 webcontrol_auth_method 2
    791 webcontrol_authentication ${MOTION_USER}:${MOTION_PASSWORD}
    792 webcontrol_port ${PORT_CONTROL}
    793 webcontrol_localhost off
    794 webcontrol_cert ${TLS_CERT}
    795 webcontrol_key ${TLS_KEY}
    796 webcontrol_parms 0
    797 webcontrol_tls on
    798 
    799 # LIVE STREAM
    800 stream_port ${PORT_STREAM}
    801 stream_localhost off
    802 stream_quality 25
    803 stream_motion on
    804 stream_maxrate 24
    805 stream_auth_method 2
    806 stream_authentication ${MOTION_USER}:${MOTION_PASSWORD}
    807 stream_preview_method 0
    808 stream_tls on
    809 
    810 # SCRIPTS
    811 on_motion_detected ${MOTION_DIR}/alert
    812 on_movie_end ${MOTION_DIR}/sync %f
    813 on_picture_save ${MOTION_DIR}/notify %f
    814 
    815 # CAMERA
    816 camera_name ${CAMERA_NAME}
    817 videodevice /dev/video0
    818 height 1080
    819 width 1920</code></pre>
    820   </p>
    821   <p>Restart <code>motion</code> to use the updated configuration:
    822   </p>
    823   <pre><code>$ systemctl restart motion</code></pre>
    824   <h2>Camera Management</h2>
    825   <p>
    826     I use a simple script to manage the cameras. This examples allows control
    827     over three servers (one of which has two cameras):
    828   </p>
    829   <pre><code>#!/usr/bin/env bash
    830 
    831 readonly WEBCONTROL_PORT="${PORT_CONTROL}"
    832 
    833 readonly cameras=(
    834   "https://${HOST0}:${WEBCONTROL_PORT}/0"
    835 #  "https://${HOST0}:${WEBCONTROL_PORT}/1"
    836 #  "https://${HOST1}:${WEBCONTROL_PORT}/${CAMERA0}"
    837 #  "https://${HOST2}:${WEBCONTROL_PORT}/${CAMERA0}"
    838 )
    839 readonly auth=(
    840   "${MOTION_USER}:${MOTION_PASSWORD}"
    841 #  "${USER0}:${PW0}"
    842 #  "${USER1}:${PW1}"
    843 #  "${USER2}:${PW2}"
    844 )
    845 
    846 err() {
    847   echo "[$(date -u +'%Y-%m-%dT%H:%M:%S%:z')]: $*" >&2
    848 }
    849 
    850 main() {
    851   local url="detection/status"
    852 
    853   case "$1" in
    854     "capture"|"c") url="detection/snapshot" ;;
    855     "pause"|"p") url="detection/pause" ;;
    856     "start"|"s") url="detection/start" ;;
    857     "status"|"") url="detection/status" ;;
    858     *) err "unrecognized command: $1"; exit 1
    859   esac
    860 
    861   for i in "${!cameras[@]}"; do
    862     curl --digest --user "${auth[i]}" "${cameras[i]}/${url}"
    863   done
    864 }
    865 
    866 main "$@"</code></pre>
    867   <h2>Next Steps</h2>
    868   <a href="/static/media/1920/dwrz_20220304T224024_edit.jpg" >
    869     <img class="img-center" src="/static/media/720/dwrz_20220304T224024_edit.jpg">
    870   </a>
    871   <p>
    872     As with all software, this project is a work-in-progress, at times
    873     abandoned, and never completed. There are a few ideas I am exploring as I
    874     continue to prototype the system:
    875     <ul>
    876       <li>
    877         The most urgent task is to make it easier to set up a new server and to
    878         keep configuration consistent across servers. I'm working on minimizing
    879         some of the duplicative work. Another option is to use containers.
    880       </li>
    881       <li>
    882         Use <a href="https://www.openbsd.org/">OpenBSD</a> instead of Raspberry
    883         Pi OS, replacing <code>msmtp</code> with <code>OpenSTMTPD</code>,
    884         <code>acme-client</code> for <code>letsencrypt</code>. The main concern
    885         here is whether wireless and <code>yolov7</code> are sufficiently
    886         performant.
    887       </li>
    888       <li>
    889         Use an in-memory filesystem and forgo the SSD, which might drop ~$30
    890         from the cost of the system.
    891       </li>
    892       <li>
    893         Use different hardware — wireless or PoE security cameras with an RTSP
    894         stream and motion running on a single server.
    895       </li>
    896       <li>
    897         Improve object detection with <code>yolov7</code> by training the model.
    898       </li>
    899       <li>
    900         Develop my own Go service to replace or wrap around <code>motion</code>,
    901         or replace the bash scripts with Go programs.
    902       </li>
    903       <li>
    904         Add features, like two-way communication.
    905       </li>
    906       <li>
    907         Use <code>yolov7</code> to monitor the camera stream directly, and forgo
    908         the motion detection step.
    909       </li>
    910     </ul>
    911   </p>
    912   <p>
    913     The final thanks must go to the open-source contributors who have made this
    914     approach possible, from the operating system all the way up to the
    915     interpreters.
    916   </p>
    917 </div>