<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>The one thing necessary</title><link>https://eamonnsullivan.co.uk/</link><description>Musings about life and code</description><generator>Hugo</generator><language>en-GB</language><atom:link href="https://eamonnsullivan.co.uk/" rel="self" type="application/rss+xml"/><item><title>Putting email in its place with Emacs and Mu4e</title><link>https://eamonnsullivan.co.uk/posts-output/email-setup/2025-12-3-putting-email-in-its-place/</link><pubDate>Wed, 03 Dec 2025 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/email-setup/2025-12-3-putting-email-in-its-place/</guid><description>&lt;p&gt;I had a few free hours recently and decided to have a go at a long-simmering idea: To improve how I manage multiple email accounts. As someone who struggles mightily to cope with distractions (I have the attention span of a gnat), I was hoping to consolidate several separate interfaces into one, simpler, less distracting, more focused client.&lt;/p&gt;
&lt;p&gt;Sadly, I wasn&amp;rsquo;t &lt;em&gt;quite&lt;/em&gt; successful &amp;ndash; my work emails remain out of reach. But I learned a lot and ended up in at least a &lt;em&gt;better&lt;/em&gt; place.&lt;/p&gt;
&lt;p&gt;There are already many guides on the Internet around what I&amp;rsquo;ve done here, but all of the information I needed was a bit scattered, especially around using &lt;a href="https://oauth.net/2/"&gt;OAuth 2.0&lt;/a&gt;. I thought it was worth writing up for posterity, even if just for me.&lt;/p&gt;
&lt;h2 id="why"&gt;Why?&lt;/h2&gt;
&lt;p&gt;I manage my schedule, notes and to-dos in Emacs, via &lt;a href="https://orgmode.org/"&gt;Org Mode&lt;/a&gt; and &lt;a href="https://www.orgroam.com/"&gt;Org Roam&lt;/a&gt;. I get some ribbing from (much younger) VS Code-using colleagues for that &amp;ndash; using Emacs in 2025 is a bit like wearing a bow tie &amp;ndash; but I&amp;rsquo;ve whittled and shaped Emacs into a sharp, well-honed tool over the last 30 years or so. It fits my brain nicely. Having my email directly available &amp;ndash; instead of jumping to a completely different interface or, worse, the Distracto Land™ of the browser &amp;ndash; when I&amp;rsquo;m organising things or taking notes just makes sense for me.&lt;/p&gt;
&lt;p&gt;I also wanted this to specifically work offline. I&amp;rsquo;m writing this right now on the train from London to Manchester and, despite the spotty-at-best connection, I have access to all my emails.&lt;/p&gt;
&lt;p&gt;Why Mu4e? There are other email clients for Emacs, including &lt;a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Gnus.html"&gt;Gnus&lt;/a&gt;, which is built-in, and multiple back ends and indexers like &lt;a href="https://notmuchmail.org/"&gt;Notmuch&lt;/a&gt;. I settled on Mu4e (a combination of an indexer call &lt;code&gt;mu&lt;/code&gt; and an Emacs package), because I liked its nicely compact and customisable interface, where I can easily set up quick (two character) shortcuts to my most common email searches.&lt;/p&gt;
&lt;p&gt;However, most of what I&amp;rsquo;m going to go through here would be applicable to many other offline email clients (Emacs or not), so you might find it useful, even if you don&amp;rsquo;t agree with me on Mu4e.&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/email-setup/mu4e-main.png"
alt="A text-based email client with links to separate inboxes and accounts, running in Emacs."
style="width:100%"/&gt;
&lt;figcaption&gt;&lt;em&gt;The main Mu4e page, where you can set up bookmarks to quickly check multiple accounts..&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/email-setup/message-view.png"
alt="A text-based email client, displaying an email for a mailing list, in Emacs."
style="width:100%"/&gt;
&lt;figcaption&gt;&lt;em&gt;The message view.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="gathering-the-emails"&gt;Gathering the Emails&lt;/h2&gt;
&lt;p&gt;I started this whole process with my personal Gmail account, and then moved on to the emails I help manage as a volunteer for a local charity (a chapter of the Saint Vincent de Paul society). I mostly work in its &lt;a href="https://svpsouthruislip.org.uk/pages-output/foodbank/"&gt;food bank&lt;/a&gt;, but I also act as an unofficial IT guy and provide a couple of client-facing emails (&lt;code&gt;info@&lt;/code&gt; and &lt;code&gt;foodbank@&lt;/code&gt;), which are currently implemented with Amazon&amp;rsquo;s Simple Email Service and proxied through another Gmail account.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m considering another email provider, &lt;a href="https://fastmail.com"&gt;Fastmail&lt;/a&gt;, for managing those charity emails. I&amp;rsquo;m currently in the 30-day free trial, so I threw in that one as well.&lt;/p&gt;
&lt;p&gt;For all of the emails, I&amp;rsquo;ll use the &lt;a href="https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol"&gt;Internet Message Access Protocol (IMAP)&lt;/a&gt; to download the messages and save them locally in a standard format called &lt;a href="https://en.wikipedia.org/wiki/Maildir"&gt;Maildir&lt;/a&gt; for searching and viewing.&lt;/p&gt;
&lt;p&gt;There are a couple of options for doing this. I tried &lt;a href="https://isync.sourceforge.io/mbsync.html"&gt;mbsync&lt;/a&gt; first, but ran into issues with Gmail (the downloads kept stopping early with an error). &lt;a href="https://github.com/OfflineIMAP/offlineimap3"&gt;OfflineIMAP&lt;/a&gt; was slower, but worked better for me.&lt;/p&gt;
&lt;p&gt;On the Mac, you can install OfflineIMAP via brew: &lt;code&gt;brew install offlineimap&lt;/code&gt;. On Linux, you can also use brew, but probably best to use something like &lt;code&gt;pipx&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="setting-up-oauth-20-for-google"&gt;Setting up OAuth 2.0 for Google&lt;/h3&gt;
&lt;p&gt;OfflineIMAP needs to know how to log in to your provider and retrieve the emails. OfflineIMAP&amp;rsquo;s deep integration with Python (which you can call directly from the configuration file) and packages such as &lt;a href="https://pypi.org/project/keyring/"&gt;keyring&lt;/a&gt; and &lt;a href="https://googleapis.dev/python/google-auth/latest/index.html"&gt;google-auth&lt;/a&gt; make this &lt;em&gt;almost&lt;/em&gt; straightforward. (I&amp;rsquo;ll mention some issues later). It allows me to keep any login details out of my config files and just call functions to retrieve them at runtime, which is better for security.&lt;/p&gt;
&lt;p&gt;Most of the guides on the Web steer you toward &amp;ldquo;app passwords&amp;rdquo;, which was the previous way of securely providing access to your Gmail account from third party tools. Fastmail, as we&amp;rsquo;ll see, still uses these. Google, however, has deprecated them. They &lt;em&gt;seemed&lt;/em&gt; to work at first, but they fail (get rejected) fairly randomly. I&amp;rsquo;m just guessing, but I think this is Google&amp;rsquo;s way of gently nudging us on to more modern authentication.&lt;/p&gt;
&lt;p&gt;I put most of the code I wrote for this step in a separate repo &amp;ndash; &lt;a href="https://github.com/eamonnsullivan/oauth2-tools"&gt;eamonnsullivan/oauth2-tools&lt;/a&gt;. The code is very heavily inspired by &lt;a href="https://linsnotes.com/posts/sending-email-from-raspberry-pi-using-msmtp-with-gmail-oauth2/"&gt;Kuibin Lin&amp;rsquo;s blog post&lt;/a&gt; for setting up a Raspberry Pi to use Gmail as a relay (which I also needed to do). The main changes I did was to use &lt;a href="https://github.com/astral-sh/uv"&gt;uv&lt;/a&gt; for the package manager and some refactoring to handle multiple gmail accounts.&lt;/p&gt;
&lt;p&gt;But, first, I needed to create an OAuth client in Google Cloud:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go to Google&amp;rsquo;s &lt;a href="https://console.cloud.google.com/"&gt;cloud console&lt;/a&gt;, and create a new project.&lt;/li&gt;
&lt;li&gt;Navigate to API &amp;amp; Services, then to OAuth consent screen, click Get Started.&lt;/li&gt;
&lt;li&gt;Fill out the form. The important bit is to choose &amp;lsquo;External&amp;rsquo; for User Type&lt;/li&gt;
&lt;li&gt;Create a new OAuth client (API &amp;amp; Services -&amp;gt; Credentials -&amp;gt; Create Credentials -&amp;gt; OAuth client ID).&lt;/li&gt;
&lt;li&gt;For Application Type choose &amp;lsquo;Desktop App.&amp;rsquo;&lt;/li&gt;
&lt;li&gt;When created, download the client secret (JSON) file.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I needed to do this for both Gmail accounts.&lt;/p&gt;
&lt;p&gt;Using that file, &lt;a href="https://github.com/eamonnsullivan/oauth2-tools?tab=readme-ov-file#usage"&gt;run the &lt;code&gt;authorise.py&lt;/code&gt; script&lt;/a&gt; to create a &lt;code&gt;credentials.json&lt;/code&gt; file for each account. The next couple of scripts, &lt;code&gt;get_token.py&lt;/code&gt; and &lt;code&gt;get_auth_creds.py&lt;/code&gt; just use that &lt;code&gt;credentials.json&lt;/code&gt; file to get the fields I&amp;rsquo;ll need, updating any expired token automatically. I&amp;rsquo;ve put these in my local bin directory (&lt;code&gt;~/bin/&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s important to protect these JSON files like passwords, so at a bare minimum, make sure only you have access to the file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;0600&lt;/span&gt; *.json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If they are lost, stolen or you accidentally check them into Github (don&amp;rsquo;t laugh), you can disable the client in the Google Cloud console and create a new one.&lt;/p&gt;
&lt;h3 id="creating-an-app-password-for-fastmail"&gt;Creating an app password for Fastmail&lt;/h3&gt;
&lt;p&gt;Fastmail still uses app passwords. They also provide OAuth2, but you need to contact them to become a &amp;ldquo;partner&amp;rdquo; or something or other (at the time of writing). I&amp;rsquo;ll just use the app password during my free trial period.&lt;/p&gt;
&lt;p&gt;In Fastmail&amp;rsquo;s &lt;a href="https://app.fastmail.com/settings/security"&gt;security settings&lt;/a&gt;, click on &amp;ldquo;Manage app passwords and access&amp;rdquo; to create a new one. I then saved it to Keychain on my Mac.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;security add-generic-password -s offlineimap -a me@mydomain.com -w
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Or, on Linux, something like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;secret-tool store --label=&amp;#34;offlineimap&amp;#34; password value
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In both cases, I was prompted to enter the password.&lt;/p&gt;
&lt;p&gt;Once it&amp;rsquo;s stored in the local secrets system, I can retrieve the passwords with a script. I use something like this, using uv&amp;rsquo;s &lt;a href="https://docs.astral.sh/uv/guides/scripts/"&gt;support for standalone scripts&lt;/a&gt;, and saved in my local &lt;code&gt;~/bin&lt;/code&gt; directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="ch"&gt;#!/usr/bin/env -S uv run --script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# /// script&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# requires-python = &amp;#34;&amp;gt;=3.14&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# dependencies = [&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# &amp;#34;keyring&amp;#34;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ///&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;keyring&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_password_from_keychain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; Retrieve the password for the given account from the macOS Keychain or Gnome Keyring.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; Args:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; account (str): The name of the account for which to retrieve the password.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; Returns:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; str: The retrieved password.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;keyring&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;offlineimap&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="my-offlineimap_helperpy"&gt;My offlineimap_helper.py&lt;/h3&gt;
&lt;p&gt;I put my OfflineIMAP Python &amp;ldquo;helper&amp;rdquo; code in &lt;code&gt;~/bin/offlineimap_helper.py&lt;/code&gt;. One issue I ran into is that OfflineIMAP can&amp;rsquo;t seem to handle any dependencies outside of the standard library. If I tried to use the function above in my helper library, I&amp;rsquo;d get an error:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;ERROR: No module named &amp;#39;keyring&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To be honest, that could be because I&amp;rsquo;m a &lt;em&gt;very&lt;/em&gt; rusty Python programmer and I&amp;rsquo;m missing some trick with virtual environments or something, but I couldn&amp;rsquo;t figure it out. Instead, my &lt;code&gt;~/bin/offlineimap_helper.py&lt;/code&gt; file actually &lt;em&gt;runs&lt;/em&gt; these scripts and gets the values it needs from standard out. I did it this way, rather than just opening the credentials file directly (it&amp;rsquo;s just JSON) because I want to keep a layer of abstraction in place, for when I (probably) add other OAuth providers. That&amp;rsquo;s probably overthinking it!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;check_output&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_password_from_keychain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; Retrieve the password for the given account from macOS Keychain.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;security&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;find-generic-password&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;-s&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;offlineimap&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;-a&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;-w&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_client_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; Retrieve the OAuth2 client id for the given credentials file.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;get_auth_creds&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;client_id&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_client_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;Retrieve the OAuth2 client secret from the given credentials.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;get_auth_creds&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;client_secret&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_refresh_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; Retrieve the OAuth2 refresh token from the given credentials.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;check_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;get_auth_creds&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;refresh_token&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="saving-the-emails"&gt;Saving the emails&lt;/h2&gt;
&lt;p&gt;We now have all the tools we need to retrieve the emails with OfflineIMAP.&lt;/p&gt;
&lt;p&gt;I created the directories where I wanted my two Gmail accounts and one Fastmail account to go to.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p ~/.maildir/gmail1 ~/.maildir/gmail2 ~/.maildir/fastmail
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;OfflineIMAP also needs to be pointed toward the root certificates to use secure connections. I found it easier to just copy the root certificates to a file in &lt;code&gt;.maildir&lt;/code&gt;. I followed &lt;a href="https://macowners.club/posts/email-emacs-mu4e-macos/"&gt;this guide&lt;/a&gt; to export them from Keychain on MacOS and save them as a pem file in &lt;code&gt;~/.maildir/certificates&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My current configuration for OfflineIMAP (which I put in &lt;code&gt;~/.offlineimaprc&lt;/code&gt;) looks &lt;em&gt;something&lt;/em&gt; like this (I&amp;rsquo;ve anonymised it), for the three accounts:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[general]
accounts = Gmail1,Gmail2,Fastmail
# The implementation code for get_client_id,
# get_client_secret,
# get_password_from_keychain and
# get_refresh_token used below.
pythonfile = ~/bin/offlineimap_helper.py
[Account Gmail1]
localrepository = Local1
remoterepository = Remote1
synclabels = yes
[Repository Local1]
type = GmailMaildir
localfolders = ~/.maildir/gmail1/
[Repository Remote1]
type = Gmail
auth_mechanisms = XOAUTH2
oauth2_client_id_eval = get_client_id(&amp;#34;/path/to/gmail1/credentials.json&amp;#34;)
oauth2_client_secret_eval = get_client_secret(&amp;#34;/path/to/gmail1/credentials.json&amp;#34;)
oauth2_request_url = https://oauth2.googleapis.com/token
oauth2_refresh_token_eval = get_refresh_token(&amp;#34;/path/to/gmail1/credentials.json&amp;#34;)
remotehost = imap.gmail.com
remoteuser = user.name1@gmail.com
ssl = yes
sslcacertfile = ~/.maildir/certificates/root-certificates.pem
[Account Gmail2]
localrepository = Local2
remoterepository = Remote2
synclabels = yes
# I don&amp;#39;t need every email for this one, just the last couple of months
maxage = 60
[Repository Local2]
type = GmailMaildir
localfolders = ~/.maildir/gmail2/
[Repository Remote2]
type = Gmail
auth_mechanisms = XOAUTH2
oauth2_client_id_eval = get_client_id(&amp;#34;/path/to/gmail2/credentials.json&amp;#34;)
oauth2_client_secret_eval = get_client_secret(&amp;#34;/path/to/gmail2/credentials.json&amp;#34;)
oauth2_request_url = https://oauth2.googleapis.com/token
oauth2_refresh_token_eval = get_refresh_token(&amp;#34;/path/to/gmail2/credentials.json&amp;#34;)
remotehost = imap.gmail.com
remoteuser = user.name2@gmail.com
ssl = yes
sslcacertfile = ~/.maildir/certificates/root-certificates.pem
[Account Fastmail]
localrepository = LocalFastmail
remoterepository = RemoteFastmail
synclabels = yes
[Repository LocalFastmail]
type = Maildir
localfolders = ~/.maildir/fastmail/
[Repository RemoteFastmail]
type = IMAP
# I&amp;#39;m not sure which auth mechanism Fastmail is using,
# so here are some options. It&amp;#39;ll try each until
# one works.
auth_mechanisms = CRAM-MD5, PLAIN, LOGIN
remotehost = imap.fastmail.com
remoteport = 993
remoteuser = me@mydomain.com
remotepasseval = get_password_from_keychain(&amp;#34;me@mydomain.com&amp;#34;)
ssl = yes
sslcacertfile = ~/.maildir/certificates/root-certificates.pem
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I then just ran &lt;code&gt;offlineimap&lt;/code&gt; to start downloading my email. For my primary personal Gmail account, this took days (many, many gigabytes of emails dating back to 2004), but for my secondary one (where I&amp;rsquo;m only downloading a couple of months) and new Fastmail account, it was quick.&lt;/p&gt;
&lt;h2 id="sending-email"&gt;Sending email&lt;/h2&gt;
&lt;p&gt;To send, most providers (usually) provide an &lt;a href="https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol"&gt;SMTP&lt;/a&gt; service for this. I&amp;rsquo;m using &lt;a href="https://marlam.de/msmtp/"&gt;msmtp&lt;/a&gt;, which is among the simplest mail transport agents to set up.&lt;/p&gt;
&lt;p&gt;On the mac, you can &lt;code&gt;brew install msmtp&lt;/code&gt;. On Linux, it&amp;rsquo;s usually available in the standard repositories. On Debian-flavoured distributions (e.g., Ubuntu) run this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo apt-get install msmtp msmtp-mta
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Again, we don&amp;rsquo;t want any plain text credentials in the config, so we&amp;rsquo;ll use the scripts to either retrieve a valid OAuth token or the app password. This is my &lt;code&gt;~/.msmtprc&lt;/code&gt; (anonymised):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;defaults
logfile ~/.maildir/msmtp.log
tls_trust_file ~/.maildir/certificates/root-certificates.pem
account gmail1
auth oauthbearer
host smtp.gmail.com
port 587
protocol smtp
from user.name1@gmail.com
user user.name1@gmail.com
passwordeval &amp;#34;get_token.py -c /path/to/gmail1/credentials.json&amp;#34;
tls on
tls_starttls on
account gmail2
auth oauthbearer
host smtp.gmail.com
port 587
protocol smtp
from user.name2@gmail.com
user user.name2@gmail.com
passwordeval &amp;#34;get_token.py -c /path/to/gmail2/credentials.json&amp;#34;
tls on
tls_starttls on
account fastmail
auth on
host smtp.fastmail.com
port 465
protocol smtp
from me@mydomain.com
user me@mydomain.com
passwordeval &amp;#34;security find-generic-password -s offlineimap -a me@mydomain.com -w&amp;#34;
tls on
# This needs to be off for Fastmail, for some reason
tls_starttls off
account default : gmail1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;get_token.py&lt;/code&gt; script returns the actual login token directly and refreshes it automatically if the token has expired.&lt;/p&gt;
&lt;h2 id="setting-up-mu4e"&gt;Setting up Mu4e&lt;/h2&gt;
&lt;p&gt;The final piece of the puzzle is to use &lt;code&gt;mu&lt;/code&gt; to index all the messages and set up Mu4e to search that index and send email. The &lt;a href="https://www.djcbsoftware.nl/about.html"&gt;creator of Mu4e&lt;/a&gt; has written some &lt;a href="https://www.djcbsoftware.nl/code/mu/mu4e/index.html"&gt;excellent documentation&lt;/a&gt; and you can see my &lt;a href="https://github.com/eamonnsullivan/emacs.d/blob/master/lisp/init-mu4e.el"&gt;complete Emacs config of Mu4e&lt;/a&gt; in Github, so I&amp;rsquo;ll just call out a few bits.&lt;/p&gt;
&lt;p&gt;To install mu, I used &lt;code&gt;brew install mu&lt;/code&gt; on the Mac. I use &lt;a href="https://github.com/radian-software/straight.el"&gt;straight.el&lt;/a&gt; to manage my Emacs configuration, so this got a little complicated (Mu4e needs some special tools to compile), but I could use the brew installed version of Mu4e directly:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-elisp" data-lang="elisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;use-package&lt;/span&gt; &lt;span class="nv"&gt;mu4e&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;:straight&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;:local-repo&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;/opt/homebrew/share/emacs/site-lisp/mu/mu4e&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;:type&lt;/span&gt; &lt;span class="nv"&gt;built-in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Once I downloaded my email, I needed to initialise the index. I did something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mu init --personal-address user.name1@gmail.com &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --personal-address user.name2@gmail.com &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --personal-address me@mydomain.com &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --maildir ~/.maildir
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For a complete example, take a look at my config. I have three different &amp;ldquo;contexts&amp;rdquo; for the different accounts, each with their own settings. I&amp;rsquo;ve also started (barely) customising the experiencing, adding my own actions. This is one that saves a link to the current message and creates a node in Org Roam:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-elisp" data-lang="elisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;eds/get-org-directory&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;The location of my org directory varies by computer.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;file-truename&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;~/Dropbox/org&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;eds/strip-invalid-chars&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Strip characters that don&amp;#39;t work in filenames or github branches&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;replace-regexp-in-string&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;[\\?\\&amp;gt;\\&amp;lt;\\|\\:\\&amp;amp;]&amp;#34;&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;eds/process-title&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Downcase and hyphenate a title case string. Remove characters
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;that don&amp;#39;t work in a filename.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;eds/strip-invalid-chars&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mapconcat&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;identity&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;split-string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;-&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;eds/orgify-msg&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Create a new org-roam node from an email message.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let*&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;link&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;org-store-link&lt;/span&gt; &lt;span class="nv"&gt;msg&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;subject&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;mu4e-message-field&lt;/span&gt; &lt;span class="nv"&gt;msg&lt;/span&gt; &lt;span class="nb"&gt;:subject&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;time-string&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format-time-string&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;%Y%m%dT%H%M%S&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;clean-subject&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;eds/strip-invalid-chars&lt;/span&gt; &lt;span class="nv"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;extension&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;org&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;eds/process-title&lt;/span&gt; &lt;span class="nv"&gt;clean-subject&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;file-base-name&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt; &lt;span class="nv"&gt;time-string&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;-&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;slug&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;.&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;org-roam-directory&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;eds/get-org-directory&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;file-name&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;expand-file-name&lt;/span&gt; &lt;span class="nv"&gt;file-base-name&lt;/span&gt; &lt;span class="nv"&gt;org-roam-directory&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title-line&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;#+title: %s\n&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;startup-line&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;#+startup: content\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;first-heading&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;* %s\n&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;find-file&lt;/span&gt; &lt;span class="nv"&gt;file-name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt; &lt;span class="nv"&gt;title-line&lt;/span&gt; &lt;span class="nv"&gt;startup-line&lt;/span&gt; &lt;span class="nv"&gt;first-heading&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;org-id-get-create&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;end-of-buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;save-buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;org-roam-db-sync&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;add-to-list&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;mu4e-view-actions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Org&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;eds/orgify-msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="no"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;figure&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/email-setup/message-actions.png"
alt="A text-based email client, displaying an email for a mailing list, in Emacs."
style="width:100%"/&gt;
&lt;figcaption&gt;&lt;em&gt;The message "action" menu, with my custom Org action.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="tripped-up-at-the-last-hurdle"&gt;Tripped up at the last hurdle&lt;/h2&gt;
&lt;p&gt;Alas, the email I &lt;em&gt;really&lt;/em&gt; struggle with is for work. My personal and charity emails are important, but low volume. My work emails are important and &lt;em&gt;high&lt;/em&gt; volume. We use Outlook and Microsoft 365, which do provide hooks I need, but my employer has disabled IMAP, SMTP and support for most third-party clients (except Apple Mail and I think whatever the equivalent is on Android). I can&amp;rsquo;t, for example, create the client id and secret I need to use OAuth 2. My employer has done this for undoubtedly good security reason (smaller surface area to worry about, for example), but it&amp;rsquo;s not great for me.&lt;/p&gt;
&lt;p&gt;Still, I&amp;rsquo;ve learned quite a bit, and I&amp;rsquo;ve brought at least my personal and charity emails into the Org system. I can create a link to an email and make a to-do item out of it in a couple of keystrokes. That&amp;rsquo;s progress, and worth doing.&lt;/p&gt;</description></item><item><title>Local TLS the easy way, with Let's Encrypt and Certbot</title><link>https://eamonnsullivan.co.uk/posts-output/ssl-setup/2024-04-01-setting-up-ssl-for-my-local-apps/</link><pubDate>Mon, 01 Apr 2024 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/ssl-setup/2024-04-01-setting-up-ssl-for-my-local-apps/</guid><description>&lt;p&gt;One huge and welcome change in the last decade or so for Web development is that secure connections are ubiquitous. Free certificates are available and most hosting providers make it very easy to obtain, use and renew the certificates automatically. This site, for example, runs on Amazon Web Services and enabling &lt;code&gt;https&lt;/code&gt; took not much more effort than ticking a few checkboxes.&lt;/p&gt;
&lt;p&gt;Browsers, like Chrome, Firefox and Safari, have also played their part, by gently steering users toward secure connections, warning about sending sensitive information over plain text and switching to &lt;code&gt;https&lt;/code&gt; automatically, when available.&lt;/p&gt;
&lt;p&gt;The last bastion for plain old &lt;code&gt;http&lt;/code&gt; connections are web apps running on your local network, like a home wiki. But, so what? This is my local network, right? Yes, it is, but using SSL to encrypt the traffic between your browser and the application is a good, belt-and-braces, defence-in-depth security step. It can protect you from a dodgy device or compromised application, &lt;em&gt;inside&lt;/em&gt; of your network, from listening in on your traffic.&lt;/p&gt;
&lt;h2 id="enabling-local-certs"&gt;Enabling local certs&lt;/h2&gt;
&lt;p&gt;There are a few ways to enable SSL connections to local servers. The easiest way is just use self-signed certificates. The disadvantage is that it requires you to accept these certificates on each browser you want to use.&lt;/p&gt;
&lt;p&gt;Another option, which many larger companies use, is to run your own Certificate Authority. That &lt;em&gt;isn&amp;rsquo;t&lt;/em&gt; easy, and you&amp;rsquo;ll still need to configure all of your browsers to accept your local root certificate. (This can be done with configuration scripting like Puppet or something. But see the &lt;em&gt;isn&amp;rsquo;t easy&lt;/em&gt; part&amp;hellip;)&lt;/p&gt;
&lt;p&gt;Either of those methods would be fine if I were the only user, but that isn&amp;rsquo;t always the case. We have a local, household wiki, which acts as this house&amp;rsquo;s README. I want to make that available to any guests, where they can find info like where the cat food is, when the rubbish is picked up and where the water shut off valve is. Once connected to my WiFi, guests can open the wiki using QR codes posted around the house. I don&amp;rsquo;t want them to have to click through any warnings about insecure connections or have to accept any self-signed certificates.&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/ssl-setup/wiki-snippet.png"
alt="A snippet from a web page with info on the household."
style="width:100%"/&gt;
&lt;figcaption&gt;&lt;em&gt;A portion of the local wiki page for my house.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="local-domain"&gt;Local domain&lt;/h2&gt;
&lt;p&gt;The option I went with was to use a real, &lt;a href="https://letsencrypt.org/"&gt;Let&amp;rsquo;s Encrypt&lt;/a&gt; certificate for a domain I have registered but not used publicly. In my case, I had &lt;code&gt;sullivanhome.org.uk&lt;/code&gt; available.&lt;/p&gt;
&lt;p&gt;The first step was to tell my router that this domain is the &amp;ldquo;local&amp;rdquo; one, which is easy enough to do on most routers, as long as you can get to the admin pages.&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/ssl-setup/local-domain-name.png"
alt="A form for a router, setting the local domain."
style="width:100%"/&gt;
&lt;figcaption&gt;&lt;em&gt;Configuring my router (Asus ZenWiFi AX) to use my registered domain as the "local" domain.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Once this is set up, local computers resolve on that domain.&lt;/p&gt;
&lt;figure&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/ssl-setup/ping.png"
alt="A terminal window running the 'ping' command."
style="width:100%"/&gt;
&lt;figcaption&gt;&lt;em&gt;My router acts as the local DNS, serving computers from the domain you specify.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="certbot"&gt;Certbot&lt;/h2&gt;
&lt;p&gt;The next step is to obtain a wildcard certificate (e.g., one for &amp;ldquo;&lt;code&gt;*.sullivanhome.org.uk&lt;/code&gt;&amp;rdquo;). My applications run across two Raspberry Pis, one called Pineapple and the other called Blueberry. I want one cert that I can apply on both, so that either &lt;code&gt;pineapple.sullivanhome.org.uk&lt;/code&gt; or &lt;code&gt;blueberry.sullivanhome.org.uk&lt;/code&gt; present as valid (with a lock icon and no errors) in the browser.&lt;/p&gt;
&lt;p&gt;Fortunately, this is made extremely easy with &lt;a href="https://certbot.eff.org/"&gt;certbot&lt;/a&gt;. This little tool from the Electronic Frontier Foundation automates the process of obtaining &lt;em&gt;and&lt;/em&gt; periodically renewing the certificate almost entirely.&lt;/p&gt;
&lt;p&gt;On Pineapple (which runs Ubuntu), I installed certbot from the snap store. I also had to change the snap store permissions and install a plugin for the registration provider I&amp;rsquo;m using (Amazon&amp;rsquo;s Route53). There are plugins for a &lt;a href="https://eff-certbot.readthedocs.io/en/latest/using.html#dns-plugins"&gt;wide variety of registrars&lt;/a&gt;.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo snap install --classic certbot
sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-route53
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To get our certificate, I just ran the following.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;certbot certonly --dns-route53 -d sullivanhome.org.uk -d *.sullivanhome.org.uk
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That command needs a bit of explanation: The &lt;code&gt;certonly&lt;/code&gt; argument tells certbot to just download the certs. It can install the certs directly in many applications and web servers (e.g., nginx or apache), but I have several different applications using the same certs, so I&amp;rsquo;ll do that later. The &lt;code&gt;-dns-route53&lt;/code&gt; option tells certbot to use the plugin to temporarily add entries in the DNS to prove that we own the domain. Finally, the &lt;code&gt;-d&lt;/code&gt; option tells certbot what certificates to register.&lt;/p&gt;
&lt;p&gt;Certbot then prompted me for various bits it needs (like my email address), registered me with Let&amp;rsquo;s Encrypt and downloaded the certificates to &lt;code&gt;/etc/letencrypt&lt;/code&gt;. The &lt;code&gt;/etc/letsencrypt/live&lt;/code&gt; directory looks like:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;lrwxrwxrwx 1 root root 43 Jan 4 15:54 cert.pem -&amp;gt; ../../archive/sullivanhome.org.uk/cert1.pem
lrwxrwxrwx 1 root root 44 Jan 4 15:54 chain.pem -&amp;gt; ../../archive/sullivanhome.org.uk/chain1.pem
lrwxrwxrwx 1 root root 48 Jan 4 15:54 fullchain.pem -&amp;gt; ../../archive/sullivanhome.org.uk/fullchain1.pem
lrwxrwxrwx 1 root root 46 Jan 4 15:54 privkey.pem -&amp;gt; ../../archive/sullivanhome.org.uk/privkey1.pem
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="automatic-renewals"&gt;Automatic renewals&lt;/h2&gt;
&lt;p&gt;Notice that the &lt;code&gt;live&lt;/code&gt; certs (the currently active certs) are symlinks. The software also sets up a &lt;code&gt;systemctl&lt;/code&gt; timer service that will renew the certificate before it expires and update those symlinks. The renewal feature allows me to provide a script that gets called on a successful renewal (in &lt;code&gt;/etc/letsencrypt/renewal-hooks/post/post-renew-certs.sh&lt;/code&gt;). I use this script to copy the file (via &lt;code&gt;scp&lt;/code&gt;) from pineapple to blueberry. All of my local web apps and services run in docker, so I also restart the containers on both Raspberry Pis.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Make a backup copy on blueberry&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scp /etc/letsencrypt/archive/sullivanhome.org.uk/* blueberry.local:/etc/letsencrypt/archive/sullivanhome.org.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# This will follow the symlinks and create non-symlinked files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh blueberry.local rm /etc/letsencrypt/live/sullivanhome.org.uk/*
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scp /etc/letsencrypt/live/sullivanhome.org.uk/* blueberry.local:/etc/letsencrypt/live/sullivanhome.org.uk
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# restart the services on blueberry -- this script just navigates to the appropriate directory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# runs docker compose restart (like below)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh blueberry.local /etc/letsencrypt/renewal-hooks/post/post-renew-certs.sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /media/homeassistant/home_assistant/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose restart
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="using-the-certs"&gt;Using the certs&lt;/h2&gt;
&lt;p&gt;Changing the applications so that they present themselves using these certificates varies a lot by application. Node RED, for example, provides a function in its configuration to retrieve the certs, force &lt;code&gt;https&lt;/code&gt; and periodically pick up any changes to the certs. I make the certs available in the docker container in a volume and then:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// This function should return the options object, or a Promise
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// that resolves to the options object
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;fs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/certs/live/sullivanhome.org.uk/privkey.pem&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;fs&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/certs/live/sullivanhome.org.uk/fullchain.pem&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt;/** If the `https` setting is a function, the following setting can be used
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt; * to set how often, in hours, the function will be called. That can be used
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt; * to refresh any certificates.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;httpsRefreshInterval&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt;/** The following property can be used to cause insecure HTTP connections to
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt; * be redirected to HTTPS.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cm"&gt; */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;requireHttps&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="wiki"&gt;Wiki&lt;/h3&gt;
&lt;p&gt;For the home wiki, it&amp;rsquo;s a bit more complicated. It has a database (PostGres) behind it and a front end that &lt;em&gt;does&lt;/em&gt; &lt;a href="https://docs.requarks.io/install/config#https"&gt;support Let&amp;rsquo;s Encrypt certificates&lt;/a&gt;, but in a way that doesn&amp;rsquo;t work well with my setup. So, instead, I installed &lt;a href="https://nginx.org/en/"&gt;nginx&lt;/a&gt; (&lt;code&gt;sudo apt install nginx&lt;/code&gt;) and used what&amp;rsquo;s called a reverse proxy. This sits in front of the non-SSL enabled site and just proxies it, serving it over SSL.&lt;/p&gt;
&lt;h4 id="install-and-configure-nginx"&gt;Install and configure nginx&lt;/h4&gt;
&lt;p&gt;On blueberry, I created a file in &lt;code&gt;/etc/nginx/sites-available&lt;/code&gt; called &lt;code&gt;blueberry.sullivanhome.org.uk&lt;/code&gt;. The contents of that file is as follows:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;server {
listen 80;
listen [::]:80;
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate /etc/letsencrypt/live/sullivanhome.org.uk/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sullivanhome.org.uk/privkey.pem;
server_name blueberry.sullivanhome.org.uk;
location / {
proxy_pass http://localhost:6830;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then make that site &amp;ldquo;available&amp;rdquo; with a symlink and start it:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo ln -s /etc/nginx/sites-available/blueberry.sullivanhome.org.uk /etc/nginx/sites-enabled/blueberry.sullivanhome.org.uk
sudo systemctl reload nginx
# or restart
sudo systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&amp;rsquo;s it. When you go to &lt;a href="https://blueberry.sullivanhome.org.uk"&gt;https://blueberry.sullivanhome.org.uk&lt;/a&gt; in your browser, nginx forwards your request to &lt;code&gt;localhost:6830&lt;/code&gt;, which is where I set the wiki to serve from.&lt;/p&gt;
&lt;h2 id="and-thats-it"&gt;And, that&amp;rsquo;s it&lt;/h2&gt;
&lt;p&gt;This set up has been working well for me now for three auto-renewals of the 90-day cert and happens without any intervention on my part. But if something does go wrong, I have all I need (backups of the certs, etc) to fix things.&lt;/p&gt;
&lt;p&gt;The caveat is that I&amp;rsquo;m not a security expert and this is unlikely to be the best approach for a public app, or for use in a public place (a restaurant&amp;rsquo;s WiFi, maybe) but it works for my purposes.&lt;/p&gt;</description></item><item><title>The extra stuff about Home Assistant</title><link>https://eamonnsullivan.co.uk/posts-output/ha-followup/2024-02-16-ha-extras/</link><pubDate>Fri, 16 Feb 2024 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/ha-followup/2024-02-16-ha-extras/</guid><description>&lt;p&gt;My &lt;a href="https://eamonnsullivan.co.uk/posts-output/home-automation-three-years/2024-02-11-home-assistant-three-years-later/"&gt;last post&lt;/a&gt; was my first in a long time and I&amp;rsquo;d forgotten how hard they are to write. After a couple of days, I got sick of looking at it and just chopped out a whole bunch of detail and posted it. Here&amp;rsquo;s a quick follow-up with some bits I left out.&lt;/p&gt;
&lt;h2 id="zigbee-vs-z-wave"&gt;Zigbee vs Z-Wave&lt;/h2&gt;
&lt;hr&gt;
&lt;figure&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/ha-followup/zigbee.png"
alt="A visualisation of my Zigbee network"
style="width:50%"/&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/ha-followup/z-wave.png"
alt="A visualisation of my Z-Wave network"
style="width:50%"/&gt;
&lt;figcaption&gt;&lt;em&gt;Visualisations of my Zigbee (left) and Z-Wave (right) networks.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;hr&gt;
&lt;p&gt;Zigbee and Z-Wave are low-power, mesh networking protocols aimed at home automation. I lean more toward Zigbee. As mentioned in the article, I started out with several Philips Hue devices (bulbs and motion sensors), which use Zigbee under the hood, and that&amp;rsquo;s probably the main reason why I have more Zigbee devices (38) than Z-Wave (seven), but not the only reason.&lt;/p&gt;
&lt;p&gt;Z-Wave has some advantages over Zigbee. It doesn&amp;rsquo;t use the same radio spectrum as WiFi, so it&amp;rsquo;s less susceptible to interference. The certification process is stricter: Z-Wave devices are much more likely to Just Work™ in my experience than the average Zigbee gadget.&lt;/p&gt;
&lt;p&gt;But on the downside, Z-Wave devices are lot more expensive and there are fewer options to choose from. Also, the Z-Wave protocol is more power-hungry than Zigbee. My one Z-Wave motion sensor in the kitchen goes through its odd-ball CR123A battery every few months, while my Zigbee devices can last two years on their plain double-As.&lt;/p&gt;
&lt;p&gt;So, I voted with my wallet. Yes, the average Aliexpress impulse buy is a gamble, but I can afford to try out more devices. I was considering getting rid of Z-Wave, but its been &lt;em&gt;so&lt;/em&gt; reliable for what I use it for (mostly smart plugs with energy monitoring) that I&amp;rsquo;ll probably keep it. I&amp;rsquo;m encouraged that Nabu Casa, the company behind Home Assistant, has &lt;a href="https://www.home-assistant.io/blog/2024/02/15/nabu-casa-joins-z-wave-alliance/"&gt;joined the Z-Wave Alliance&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="home-assistant-community-store"&gt;Home Assistant Community Store&lt;/h2&gt;
&lt;p&gt;Even though I use docker to run Home Assistant, I can still install the (very unofficial, unsupported) Home Assistant Community Store (HACS) and add third-party plugins. I use a handful of these. I mentioned Node-RED in the last post and that requires a &lt;a href="https://github.com/zachowj/hass-node-red"&gt;plugin&lt;/a&gt;, but I also use a few others that have made my life simpler.&lt;/p&gt;
&lt;p&gt;One that I added last summer is &lt;a href="https://github.com/basnijholt/adaptive-lighting"&gt;Adaptive Lighting&lt;/a&gt;. This adjusts the brightness and temperature of my lights based on the elevation of the sun. My automations used to have lots of code for adjusting the lights depending on the time of day. With Adaptive Lighting, those automations got a lot simpler. I just turn on the light and let the plugin figure out how brightly.&lt;/p&gt;
&lt;p&gt;Another very useful addition is &lt;a href="https://github.com/make-all/tuya-local"&gt;Tuya Local&lt;/a&gt;. The &lt;a href="https://www.fruugo.co.uk/tibet-mini-70w-led-dimmable-ceiling-light-with-built-in-35w-dc-wood-effectwhite/p-76208982-153939774?language=en&amp;amp;ac=bing&amp;amp;msclkid=c1d43b16114f1f7c422b5fcdb57514f7"&gt;ceiling fan&lt;/a&gt; in our bedroom uses the Tuya App, which is a popular cloud-based App for many Chinese devices, but I&amp;rsquo;ve found its cloud services to be very unreliable.&lt;/p&gt;
&lt;p&gt;I used to have a bunch of Tuya-controlled smart plugs, for example, that were &lt;em&gt;extremely&lt;/em&gt; flaky, mostly because Tuya insists on serving me from data centres in Poland, about a thousand miles from London. But the fan was exactly what we wanted, so we gave Tuya another shot.&lt;/p&gt;
&lt;p&gt;It was still terrible. The fan frequently wouldn&amp;rsquo;t turn on or wouldn&amp;rsquo;t turn off. The Tuya Local code talks directly to the fan, bypassing the long trip to Poland. We&amp;rsquo;ve had a few niggles at first, but it&amp;rsquo;s been trouble-free for almost a year now.&lt;/p&gt;
&lt;h2 id="philips-hue"&gt;Philips Hue&lt;/h2&gt;
&lt;p&gt;I mentioned in the previous post that, if I had to do it over again, I might have tried smart switches for the lighting, instead of smart bulbs. But I still really like my Philips Hue bulbs. I like being able to control the colour, temperature and brightness (now via Adaptive Lighting) and the quality is quite good: the light looks nice and all of them are still working.&lt;/p&gt;
&lt;p&gt;The Hue bulbs, especially the full-colour ones, cost the earth, though. I can&amp;rsquo;t afford them. Only some of my bulbs are full-colour ones, and the rest are what are called &amp;ldquo;White ambiance&amp;rdquo;. I&amp;rsquo;ve tried various other Zigbee bulbs, such as those from Ikea, but they just aren&amp;rsquo;t as good.&lt;/p&gt;
&lt;p&gt;If I could get over my fear of fiddling with light switches (gangs and such!), I might try a hybrid solution. The switches would turn on/off the smart bulbs, but I could still control the brightness.&lt;/p&gt;
&lt;h2 id="wled"&gt;WLED&lt;/h2&gt;
&lt;hr&gt;
&lt;figure&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/ha-followup/wled.jpeg"
alt="A pair of shelves with strip lighting"
style="width:100%"/&gt;
&lt;figcaption&gt;&lt;em&gt;I'm getting the hang of using strip lighting, such as these on my living room shelves.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;hr&gt;
&lt;p&gt;Another technology I&amp;rsquo;ve incorporated into the home is &lt;a href="https://kno.wled.ge/"&gt;WLED&lt;/a&gt; lighting. These use WiFi, but locally only, and are supported quite well by Home Assistant. My first attempts at creating using strips were quite rudimentary &amp;ndash; bare strips with ESP32 chips hanging off them. But I&amp;rsquo;m getting the hang of it now. These ones in my living room have proper rails, but they&amp;rsquo;re still using developer board ESP32 chips with clip-on wiring and they&amp;rsquo;re a bit flaky. My next challenge is to get the hang of soldering, which I hope will make them more reliable.&lt;/p&gt;
&lt;p&gt;All for now. I&amp;rsquo;m sure there are bits I&amp;rsquo;m forgetting, but those are the main extra bits I wanted to mention.&lt;/p&gt;</description></item><item><title>Home Assistant: Three years later</title><link>https://eamonnsullivan.co.uk/posts-output/home-automation-three-years/2024-02-11-home-assistant-three-years-later/</link><pubDate>Sun, 11 Feb 2024 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/home-automation-three-years/2024-02-11-home-assistant-three-years-later/</guid><description>&lt;p&gt;Almost three years ago, deep in the pandemic, my kids got me my first Raspberry Pi. That started me on &lt;a href="https://eamonnsullivan.co.uk/posts-output/home-automation/2021-06-21-handy-around-the-home/"&gt;an adventure with Home Assistant&lt;/a&gt;, mostly because it was one of the first search results for &amp;ldquo;what to do with a Raspberry Pi.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m still going, and learned much since then, so it is time for a quick update.&lt;/p&gt;
&lt;hr&gt;
&lt;figure&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation-three-years/mainlights.png"
alt="The first page on my Home Assistant dashboard"
style="width:33%"/&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation-three-years/heating.png"
alt="The heating dashboard"
style="width:33%"/&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation-three-years/harddisks.png"
alt="Hard disk space monitoring"
style="width:33%"/&gt;
&lt;figcaption&gt;&lt;em&gt;Snippets from my Home Assistant dashboard. From lighting, through heating and even some infrastructure monitoring (Pineapple, Blueberry and Watermelon are the Pis in my house).&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;hr&gt;
&lt;h2 id="what-hasnt-changed"&gt;What hasn&amp;rsquo;t changed&lt;/h2&gt;
&lt;p&gt;At the start, the foray into &lt;a href="https://www.home-assistant.io/"&gt;Home Assistant&lt;/a&gt; was tentative, but what attracted me to it then only got more compelling over time. Being able to use devices from many different manufacturers, without getting stuck in any walled gardens, is still hugely important. I don&amp;rsquo;t have to decide between an Apple home, an Amazon home, a Philips home or a Google home. With Home Assistant, I can make &lt;em&gt;my&lt;/em&gt; home out of the best bits from (almost) anyone, using Home Assistant&amp;rsquo;s &lt;a href="https://www.home-assistant.io/integrations/"&gt;integrations&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The software continues to run on the exact same Raspberry Pi 4 the kids gave me, with a few tweaks (see below).&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m also still using &lt;a href="https://nodered.org/"&gt;Node-RED&lt;/a&gt; for most of my automations. I remain more comfortable in Javascript than YAML (which is what Home Assistant&amp;rsquo;s native automations are written in), even though Home Assistant has made many improvements to its built-in automation editor since I started.&lt;/p&gt;
&lt;hr&gt;
&lt;figure&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation-three-years/bedroom-flow2.png"
alt="A Node-RED flow for handling motion in the master bedroom"
style="width:100%"/&gt;
&lt;figcaption&gt;&lt;em&gt;The Node-RED flow for responding to motion in the master bedroom, taking into account the time of day and the day of the week.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;hr&gt;
&lt;p&gt;I&amp;rsquo;m continuing to use the &lt;a href="https://nabucasa.com/"&gt;Home Assistant Cloud&lt;/a&gt; service, which is provided by the company behind Home Assistant and helps fund its development. It allows me to connect to my server securely while out of the house. It&amp;rsquo;s possible (easy, even) to set that up yourself, but I want to contribute in some way.&lt;/p&gt;
&lt;h2 id="what-has-changed"&gt;What has changed&lt;/h2&gt;
&lt;p&gt;The main thing that has changed is what I consider a good automation.&lt;/p&gt;
&lt;p&gt;Three years ago, I was excited about things like flashing and colourful lights, getting notifications and making my speakers say things. I was aiming for something more like &lt;a href="https://en.wikipedia.org/wiki/HAL_9000"&gt;HAL 9000&lt;/a&gt;. My automations were heavy-handed and opinionated, and often annoyed my ever-patient wife and amused the kids, especially when they went wrong.&lt;/p&gt;
&lt;p&gt;Nowadays, I&amp;rsquo;m using a lighter touch. I want the house to do the right thing, the expected thing, at the appropriate time. A good automation should be barely noticeable, almost invisible: The lights should turn on when you enter the room, if it&amp;rsquo;s dark, and turn off again when they are not needed. The heating should come on when we wake up and turn off when we&amp;rsquo;re out of the house or go to bed. Deviations, like keeping a light off or on, should be as simple as pressing a conveniently located button.&lt;/p&gt;
&lt;p&gt;Most of my automations are not huge improvements over the simple light switch on the wall, and could hardly be described as life changing, but a small number are more substantive: We can tell the house &amp;ldquo;good night&amp;rdquo; (via a &lt;a href="https://www.home-assistant.io/voice_control/thirteen-usd-voice-remote/"&gt;cheap, home-made voice control gadget&lt;/a&gt;) and lots of things happen &amp;ndash; some lights turn on, some off, the heating shuts down and the TV turns off. The house then waits for a quiet period (by checking motion sensors) before shutting all the lights and settling into &amp;ldquo;sleep&amp;rdquo; mode. The house does a similar &amp;ldquo;shut down&amp;rdquo; when it notices that no one is home, and &amp;ldquo;wakes&amp;rdquo; up when we return.&lt;/p&gt;
&lt;hr&gt;
&lt;figure&gt;
&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation-three-years/goodnight.png"
alt="A Node-RED flow for shutting down the house at night."
style="width:100%"/&gt;
&lt;figcaption&gt;&lt;em&gt;The Node-RED flow for shutting down the house at the end of the day.&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;hr&gt;
&lt;h3 id="local-focus"&gt;Local focus&lt;/h3&gt;
&lt;p&gt;My taste in devices has also become more discerning. At the beginning, I tried loads of things, from many manufacturers, most of which relied on cloud services. I quickly discovered how unreliable that was. I want to turn my lights off, even when my Internet connection is down.&lt;/p&gt;
&lt;p&gt;In general, if we&amp;rsquo;ve learned anything over the years, it&amp;rsquo;s that relying on the good will of any company is a really bad idea. If you buy an Alexa, a Google Nest, Hive thermostat or some other Internet-connect device, you are essentially renting a future paperweight &amp;ndash; features are withdrawn, companies go out of business and cloud services get shut down.&lt;/p&gt;
&lt;p&gt;Today I buy and use devices that work locally, without an Internet connection. That means using local protocols, like Zigbee or my WiFi. I avoid anything that requires the cloud. It&amp;rsquo;s OK to &lt;em&gt;have&lt;/em&gt; a proprietary app, but I shouldn&amp;rsquo;t be &lt;em&gt;required&lt;/em&gt; to use it for important features. I haven&amp;rsquo;t thrown out some of my past mistakes, like my Ring doorbell, but I won&amp;rsquo;t buy more. I&amp;rsquo;m gradually transitioning, as I can afford to.&lt;/p&gt;
&lt;p&gt;Unfortunately, this approach is antithetical to the interests of most companies, no matter how &amp;ldquo;not evil&amp;rdquo; they seem, so it is a bit of a struggle and will probably remain that way. I have &lt;em&gt;hopes&lt;/em&gt; about &lt;a href="https://www.home-assistant.io/integrations/matter/"&gt;Matter&lt;/a&gt; (a very new, local-focused standard for home automation), and will try it when supporting devices become more widely available, but I&amp;rsquo;m only cautiously optimistic. If it&amp;rsquo;s possible to get an edge and keep the customer tethered, Google, Apple, Amazon, etc., will find a way.&lt;/p&gt;
&lt;h3 id="reliability"&gt;Reliability&lt;/h3&gt;
&lt;p&gt;One important shift in my attitude is that I now consider Home Assistant to be very important, even mission-critical, rather than just an experiment or a hobby. If that Pi gives up the ghost, it would be a real pain. I&amp;rsquo;ve beefed it up a bit: The Home Assistant software now runs on a hard disk (using an internal SSD &lt;a href="https://thepihut.com/products/wd-green-240gb-m-2-internal-ssd?variant=37573574983875"&gt;like this&lt;/a&gt; and a &lt;a href="https://thepihut.com/products/argon-one-m-2-raspberry-pi-4-case?variant=32506446872638"&gt;new case&lt;/a&gt;), but the operating system (Ubuntu) is still running on an SD card that &lt;em&gt;will&lt;/em&gt; crap out at the most inconvenient time.&lt;/p&gt;
&lt;p&gt;I use the very slightly &amp;ldquo;advanced&amp;rdquo; &lt;a href="https://www.home-assistant.io/installation/linux#install-home-assistant-container"&gt;container-based&lt;/a&gt; installation option for Home Assistant, which might help a bit. Originally, I used this method to learn docker and docker compose, but I&amp;rsquo;ve stuck with it because I like the flexibility. I could move services from one Pi to another, swiftly revert to a previous version or (assuming I&amp;rsquo;ve backed up the data volume recently) recover quickly from a hardware failure.&lt;/p&gt;
&lt;p&gt;Still, I&amp;rsquo;ve only started to put some thought into what will replace this server hardware.&lt;/p&gt;
&lt;h3 id="zigbee"&gt;Zigbee&lt;/h3&gt;
&lt;p&gt;At the start, the main way I managed all of my lights and motion sensors was via Home Assistant&amp;rsquo;s &lt;a href="https://www.home-assistant.io/integrations/hue/"&gt;Philips Hue&lt;/a&gt; integration. Under the hood, Philips Hue uses a light-weight, mesh protocol call Zigbee, but with some proprietary features accessible from an app. Almost everything I owned was through this integration. So much so that, other than a couple of buttons and speakers, I could have just used the Hue app to control things. This was Plan B, if Home Assistant didn&amp;rsquo;t work out.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve now switched to a more generic Zigbee system, using the &lt;a href="https://www.home-assistant.io/skyconnect/"&gt;SkyConnect&lt;/a&gt; dongle, and Home Assistant&amp;rsquo;s own &lt;a href="https://www.home-assistant.io/integrations/zha/"&gt;Zigbee Home Automation&lt;/a&gt;, because I wanted to start using devices that don&amp;rsquo;t fit into the Philips Hue worldview, such as thermostats and radiator valves.&lt;/p&gt;
&lt;p&gt;The transition wasn&amp;rsquo;t easy. The SkyConnect was a &lt;em&gt;lot&lt;/em&gt; fiddlier to get working and was more susceptible to network interference than the Hue hub (which had its own power supply and a bigger antenna).&lt;/p&gt;
&lt;p&gt;I fixed the interference problems by buying a long (five-metre), shielded USB extension and moving the SkyConnect far away from the Raspberry Pi (and its USB3 connected hard drive), the WiFi router and other sources of interference. I had to monitor the airwaves in the house and switch Zigbee channels until I found a quiet place in the radio spectrum. Finally, I bought some Zigbee network extenders (like &lt;a href="https://www.amazon.co.uk/gp/product/B09KXTCMSC"&gt;this one&lt;/a&gt;) to increase coverage and reliability.&lt;/p&gt;
&lt;h2 id="what-i-dont-like"&gt;What I don&amp;rsquo;t like&lt;/h2&gt;
&lt;p&gt;Knock on wood, everything in the house seems to be working now, but every big change seems to involve a bit of swearing and moving things around (or channel-changing) until things are stable again. Because of this, I&amp;rsquo;m becoming reluctant to fiddle &amp;ndash; or at least not until I have some time on my hands. I still enjoy the tinkering, but it has to be more carefully considered.&lt;/p&gt;
&lt;p&gt;While Home Assistant is well documented compared with most open-source software, if something goes wrong, it can be difficult to debug. There&amp;rsquo;s a &lt;em&gt;lot&lt;/em&gt; of moving parts! The log messages are hard to decipher, and must be inscrutable to a non-programmer. The forums and Discord chats are friendly, but busy. Unless you catch the attention of someone who knows, your pleas for help quickly sink into the ether. Home Assistant users are a small percentage of most manufacturers customer base, so help is spotty as well from official tech support. I&amp;rsquo;m usually reduced to searching for the error messages in &lt;a href="https://github.com/home-assistant/core/issues"&gt;Github issues&lt;/a&gt;, hoping for a workaround.&lt;/p&gt;
&lt;p&gt;Compared with my day job as a software engineer, I find automating my house a bit like tightrope walking without a net. It&amp;rsquo;s basically impossible to be test-driven, or at least automated tests. Typically, after a big refactor, it&amp;rsquo;ll take me a few days to find and fix simple bugs like copy and paste errors.&lt;/p&gt;
&lt;p&gt;But, overall, my experience has been very positive. Home Assistant is such a large and diverse project that I&amp;rsquo;m still discovering new features almost every day.&lt;/p&gt;
&lt;h2 id="regrets"&gt;Regrets&lt;/h2&gt;
&lt;p&gt;When I look back, there are a few things that I would do differently. I might have leaned more toward smart switches and &lt;a href="https://www.click4electrics.co.uk/1/11677/click-smart-13a-2-gang-zigbee-smart-switched-socket-outlet-cma30036"&gt;integrated wall sockets&lt;/a&gt; than smart bulbs and dongle-like &lt;a href="https://www.amazon.co.uk/Innr-Zigbee-Socket-Philips-SmartThings/dp/B0CFVLK4FL"&gt;plugs&lt;/a&gt;. And I could have saved myself a lot of heartache by avoiding cloud services. But the flexibility of Home Assistant and its thousands of integrations have meant that I can work through those issues and make do with almost anything &amp;ndash; there aren&amp;rsquo;t many dead ends.&lt;/p&gt;
&lt;p&gt;Would I do it all over again? That&amp;rsquo;s a more complicated question. Nothing quite disturbs your peace as sitting in a dark room, or having a light turn on in the middle of the night, because an automation has gone wrong. Hardware, in general, is also crap and I&amp;rsquo;ve spent too many hours trying to fix that faulty light strip, dodgy motion sensor or misbehaving sensor.&lt;/p&gt;
&lt;p&gt;But I&amp;rsquo;ve also quite enjoyed learning about it all. It&amp;rsquo;s been a great hobby, particularly over the pandemic, when I had a lot of time on my hands. I&amp;rsquo;d do it again, and I&amp;rsquo;m looking forward to the next few years.&lt;/p&gt;</description></item><item><title>Home automation adventures</title><link>https://eamonnsullivan.co.uk/posts-output/home-automation/2021-06-21-handy-around-the-home/</link><pubDate>Mon, 21 Jun 2021 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/home-automation/2021-06-21-handy-around-the-home/</guid><description>&lt;p&gt;For the past six weeks or so I&amp;rsquo;ve been playing around with home automation. It&amp;rsquo;s a rather large and complex subject and I&amp;rsquo;ve only just started, but thought it might be worth writing up what I&amp;rsquo;ve found so far.&lt;/p&gt;
&lt;p&gt;The adventure started when the kids gave me a new Raspberry Pi for my birthday near the beginning of May. I installed &lt;a href="https://www.home-assistant.io/"&gt;Home Assistant&lt;/a&gt; on it and, a bit later, &lt;a href="https://nodered.org/"&gt;Node Red&lt;/a&gt;. Then, over the next few weeks, much to my wife&amp;rsquo;s dismay, I purchased 10 smart light bulbs, a few motion sensors and half a dozen smart plugs &amp;ndash; gradually changing the house into a version of &lt;a href="https://en.wikipedia.org/wiki/HAL_9000"&gt;HAL&lt;/a&gt;. Each time she would come home from work, some wall switch or another would stop working as it did&amp;hellip; She&amp;rsquo;s been very patient!&lt;/p&gt;
&lt;h2 id="home-assistant"&gt;Home Assistant&lt;/h2&gt;
&lt;p&gt;The first piece of this, and the cornerstone, is Home Assistant. It is a very active open source project, compatible with (at the time of writing) more than 1,800 devices. Using &lt;a href="https://www.home-assistant.io/integrations/"&gt;&amp;ldquo;integrations&amp;rdquo;&lt;/a&gt;, Home Assistant allows you to tie together incompatible products, from manufacturers who all want to be your sole home automation provider. Rather than going all Apple, or all Google, or all Amazon/Alexa, or having six different apps on your phone, you can you use the best bits and pieces from several different ecosystems. You want to trigger music on two different brands of Wifi speaker, turn on a smart switch in one room when a bulb toggles in another, or tell a Google Home speaker to activate an Apple device? All of this and more are possible.&lt;/p&gt;
&lt;p&gt;And it&amp;rsquo;s not just devices &amp;ndash; I can also tie together APIs, such as Google&amp;rsquo;s Text-to-Speech or The Met Office&amp;rsquo;s weather API.&lt;/p&gt;
&lt;p&gt;Can you do this easily? Well&amp;hellip; It&amp;rsquo;s not too bad, but expect a learning curve. Freedom comes at a cost.&lt;/p&gt;
&lt;p&gt;To install Home Assistant on the Pi, I used the &lt;a href="https://www.home-assistant.io/installation/linux#install-home-assistant-container"&gt;docker container&lt;/a&gt; installation option. This installs the core of the software in a container, but you miss out on some one-click integration with add-ons. You can still use add-ons, but you have to install them manually, using some docker tricks. This wasn&amp;rsquo;t an issue for me, since one of my motivations was to learn those docker tricks. Other options include installing a Home Assistant Operating System or even &lt;a href="https://www.home-assistant.io/blue"&gt;buying&lt;/a&gt; a fully configured box.&lt;/p&gt;
&lt;p&gt;The software exposes a web dashboard, which you can configure with various buttons and switches, to run your house.&lt;/p&gt;
&lt;p&gt;To help fund the open source project, there is also a &lt;a href="https://www.nabucasa.com/"&gt;mobile app&lt;/a&gt;, letting you connect to that local dashboard to control Home Assistant while you are out of the house. It&amp;rsquo;s very optional, but I found it useful enough to subscribe (£5 a month).&lt;/p&gt;
&lt;h2 id="automations"&gt;Automations&lt;/h2&gt;
&lt;p&gt;You can get quite far with just the Home Assistant software, turning on and off lights and switches using various events and inputs. These so-called automations are put together using a visual editor and stored using YAML. For example, I bought a starter set for Philips Hue and configured the lights to turn on when the sun is setting. (To be clear, you can do this with the Philips Hue app, but I don&amp;rsquo;t want several different apps to control my house.)&lt;/p&gt;
&lt;p&gt;The wizard-like visual editor steps you though creating a response to some event. The ultimate outcome is a piece of YAML, looking like this, but many users could probably avoid ever seeing the YAML:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation/sunset-automation.png"
alt="YAML to turn on a couple of lights when the sun is less than 10% from the horizon"
width=500&gt;&lt;/p&gt;
&lt;p&gt;As a software engineer, I think that&amp;rsquo;s pretty readable and would often drop down into Home Assistant&amp;rsquo;s built-in YAML editor to write this stuff directly. You can get more complicated, with nested OR and AND conditions. However, at some point, YAML starts to get hard to read and maintain, or the wizard interface just hasn&amp;rsquo;t thought of some wrinkle that you need. At that point, I turned to Node Red.&lt;/p&gt;
&lt;h2 id="node-red"&gt;Node Red&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://nodered.org/"&gt;Node Red&lt;/a&gt; and Home Assistant are a &lt;a href="https://home-assistant-guide.com/2020/09/27/node-red-vs-home-assistant-why-not-use-both/"&gt;popular combination&lt;/a&gt;. The two together let you write more complex automations, called flows. The flows are assembled using widgets from a palette, connected with lines indicating sequences and branches, in a visual way.&lt;/p&gt;
&lt;p&gt;But underneath it all, it&amp;rsquo;s just NodeJS. The widgets come from NPM packages, one of which lets you write flows that can be triggered from Home Assistant buttons, or respond to its events. It also gives me the ability to drop down to writing code, or write my own widgets, making it nearly infinitely extensible.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example: My simple sunset routine (above) worked pretty well, but it has some compromises. I had set the trigger elevation a bit higher than it needs to be (the sun sets behind our neighbouring houses at an elevation of about 5 percent) because it gets very dark in the house on cloudy days well before that trigger point. So, instead, I use a flow that can trigger on any one of several events &amp;ndash; the (location-adjusted) sun level or the level of light in various rooms, using sensors to measure lights levels.&lt;/p&gt;
&lt;p&gt;This is what the flow looks like in Node Red:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation/node-red-sunset.png" alt="A Node Red flow to turn lights on when one of several conditions are true."&gt;&lt;/p&gt;
&lt;h2 id="docker-compose"&gt;Docker Compose&lt;/h2&gt;
&lt;p&gt;Both Home Assistant and Node Red run on my Raspberry Pi in docker containers. I bring them both up using a &lt;code&gt;single docker-compose.yml&lt;/code&gt; file. This ensures that they are started/restarted/stopped/etc. together and can communicate with each other. Here is my current docker-compose configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;3&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;homeassistant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;homeassistant&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;homeassistant/raspberrypi4-homeassistant:stable&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/home/eamonn/home_assistant:/config&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;network_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;host&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;nodered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nodered&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nodered/node-red&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;1880:1880&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Local path where all Node-RED config will be stored.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;/home/eamonn/home-node-red:/data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;homeassistant&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;TZ&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Europe/London&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;/config&lt;/code&gt; (for Home Assistant) and &lt;code&gt;/data&lt;/code&gt; (for Node Red) point to local directories on the Pi, both of which are backed up to repositories in Github. I can rollback my configuration and flows to a known good state if I do something drastically wrong.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;restart&lt;/code&gt; setting (&lt;code&gt;unless-stopped&lt;/code&gt;) ensures that the two of them restart when the Pi is rebooted.&lt;/p&gt;
&lt;h2 id="lights"&gt;Lights&lt;/h2&gt;
&lt;p&gt;The first thing I tackled was the lights. As mentioned, I started with a Philips Hue hub and two bulbs, but the Philips bulbs are really expensive. They work great, and look good, but I can&amp;rsquo;t afford to light my whole house with these things.&lt;/p&gt;
&lt;p&gt;Fortunately, the Philips Hue is based mostly on a &lt;a href="https://csa-iot.org/"&gt;standard protocol&lt;/a&gt; and I was able to buy eight more from several different (mostly Chinese) manufacturers for less than half the price. I was able to integrate those into the Philips Hue hub, usually with a click or two in the Philips app. Here&amp;rsquo;s &lt;a href="https://www.amazon.co.uk/gp/product/B08SBR73V5"&gt;one example&lt;/a&gt; of a compatible Chinese-manufactured bulb.&lt;/p&gt;
&lt;p&gt;The knock-offs lack one important feature &amp;ndash; the ability to configure what happens when the power goes off and then back on again (like, when the better half tries to turn the wall switch off and on). I guess this isn&amp;rsquo;t part of the standard. This is a useful feature, but not worth the extra money. I just put the Philips ones where the switch-on behaviour is most desirable.&lt;/p&gt;
&lt;p&gt;The bulbs, even the Philips ones, come in several flavours, only some of which support changing colours. I mostly don&amp;rsquo;t use the colours and just need control over the &amp;ldquo;temperature&amp;rdquo; and brightness. I do have a couple of colour-capable bulbs, which are mostly useful for just showing off.&lt;/p&gt;
&lt;h2 id="smart-plugs"&gt;Smart plugs&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation/plug.jpg"
alt="A smart wall plug."
width=250&gt;&lt;/p&gt;
&lt;p&gt;The next bit was to integrate some smart wall plugs. Our sun room (an extension to the back of the house) has no wall switch &amp;ndash; just an overhead light, connected to a mains plug high on one wall, and a floor lamp with four bulbs in the corner. For years, we used a pool cue to turn on the power for the main ceiling light and then had to turn on the lamp&amp;rsquo;s four bulbs. Every. Time. I wanted to be able to turn on and off the overhead light and the floor lamp at the same time, as if connected to the same switch.&lt;/p&gt;
&lt;p&gt;I put a smart bulb in the overhead light, leaving the power on, and then used Node Red to respond to a &amp;ldquo;turn on&amp;rdquo; or &amp;ldquo;turn off&amp;rdquo; event on this bulb and automatically toggle the floor lamp, which I connected to a smart plug. These are two entirely different products, with different APIs and protocols, but Home Assistant and Node Red are able to hide this complexity.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation/sun-room-light.png" alt="Turn on/off floor lamp when a bulb toggles."&gt;&lt;/p&gt;
&lt;p&gt;Another good use of the smart switches was to control the clothes dryer. Our ageing dryer has a dodgy timer, but otherwise works fine. The timer would, very occasionally, fail to turn off the dryer, which is a dangerous fire hazard. What we&amp;rsquo;ve been doing is to put the clothes in and set a timer on our phones. When the alarm goes off, we&amp;rsquo;d have to go outside to the shed to check that the dryer stopped.&lt;/p&gt;
&lt;p&gt;So, what I did was plug the dryer into one of the smart plugs, create a button in Home Assistant, tied to a flow in Node Red, that ensures the power shuts off again after an hour.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation/dryer-button.png" alt="Button to run dryer for an hour"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation/dryer-plug-flow.png" alt="Dryer plug Node Red flow"&gt;&lt;/p&gt;
&lt;p&gt;Notice that last bit about &amp;ldquo;announcing&amp;rdquo; when the dryer is finished &amp;ndash; that&amp;rsquo;s just a little showing off. I use a Home Assistant integration to connect to two speakers in the house, then use Google Text-to-Speech to tell us when the clothes are dry&amp;hellip; Yes, completely unnecessary, but still fun.&lt;/p&gt;
&lt;p&gt;I found &lt;a href="https://www.amazon.co.uk/gp/product/B07MNJ72SF"&gt;these plugs&lt;/a&gt; work well, using the &lt;a href="https://www.home-assistant.io/integrations/tuya/"&gt;Tuya&lt;/a&gt; integration in Home Assistant.&lt;/p&gt;
&lt;h2 id="motion-time-and-presence"&gt;Motion, time and presence&lt;/h2&gt;
&lt;p&gt;Another piece of the puzzle was to turn lights and power on and off in response to events. For example, I want the hallway lights to turn on when my wife comes home from a late shift. I want the bathroom light to turn on when we enter, but to control the brightness depending on the time of day (e.g., very dimly in the middle of the night). And I wanted to turn the power off to certain appliances overnight, such as the laptop chargers.&lt;/p&gt;
&lt;p&gt;For this, I&amp;rsquo;ve tried a couple of different approaches. One was to listen for when our phones connect to the home Wifi. This is good enough to turn the lights off if neither of us are home, but it&amp;rsquo;s slow to react and not good enough to, say, trigger the lights when we enter.&lt;/p&gt;
&lt;p&gt;For that, I needed motion sensors. I&amp;rsquo;ve tried two so far. One is the &lt;a href="https://shellystore.co.uk/product/Shelly-Motion"&gt;Shelly Motion&lt;/a&gt;. This uses a rechargeable battery and connects to your Wifi directly. It also exposes other sensors &amp;ndash; the light and battery level and &amp;ldquo;tampering&amp;rdquo; (i.e., shaking the device). I don&amp;rsquo;t really need the tampering and I only get a light-level event when motion is detected. I&amp;rsquo;m not that happy with it &amp;ndash; it was expensive, a bit fiddly to set up and the events I get from it are intermittent sometimes.&lt;/p&gt;
&lt;p&gt;The second I tried is the &lt;a href="https://www.amazon.co.uk/gp/product/B0748NCMNW"&gt;Philips Hue motion sensor&lt;/a&gt;, which despite being the &amp;ldquo;brand-name&amp;rdquo; product was actually cheaper and connected to the Hue hub instead of directly to Wifi. The batteries (two AAAs) aren&amp;rsquo;t rechargeable, but reportedly work for at least a year (and up to two). In addition to motion, these expose light levels (like the Shelly), but also temperature. That&amp;rsquo;s much more useful than the tampering sensor and means I might be able to hook these up to the heat (or a fan or two) later.&lt;/p&gt;
&lt;p&gt;By strategically placing these around the house, and checking the time of day, I can create some sophisticated flows that intelligently turn on lights (at the appropriate brightness level) and turn them off again when no motion is detected for a while.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation/motion-flow.png" alt="Motion sensor flow"&gt;&lt;/p&gt;
&lt;h2 id="music"&gt;Music&lt;/h2&gt;
&lt;p&gt;The music integration was another response to a household frustration (like the broken dryer switch and pool cue light). Several years ago, I bought a couple of &lt;a href="https://www.bose.co.uk/en_gb/products/speakers/smart_home/soundtouch_family.html"&gt;Bose SoundTouch&lt;/a&gt; speakers. They sound great and I can play the same music, synchronised, though both of them. But the software to control them is truly awful.&lt;/p&gt;
&lt;p&gt;With Home Assistant, and the &lt;a href="https://www.home-assistant.io/integrations/soundtouch/"&gt;Bose SoundTouch integration&lt;/a&gt;, I can just create a couple of buttons that do exactly what I want without having to go anywhere near Bose&amp;rsquo;s software.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation/music-buttons.png" alt="Buttons in Home Assistant to control music"&gt;&lt;/p&gt;
&lt;h2 id="scenes-and-routines"&gt;Scenes and routines&lt;/h2&gt;
&lt;p&gt;Another very nice feature of Home Assistant are &amp;ldquo;scenes&amp;rdquo;, where you can set light levels and toggle smart plugs in several rooms to pre-set levels. For example, we have a &amp;ldquo;watching TV&amp;rdquo; scene that turns off several lights, and then dims others.&lt;/p&gt;
&lt;p&gt;But you can&amp;rsquo;t run scripts from Home Assistant scenes. This is where Node Red comes in. We have an empty (does nothing) scene in Home Assistant called &amp;ldquo;Good night&amp;rdquo;. Node Red is configured to listen to the &amp;ldquo;scene activation&amp;rdquo; event from Home Assistant and do various things, such as shut off most lights, turn the TV off, turn on the bathroom lights, etc. Then, dim the lights in stages until they shutoff altogether in 15 minutes.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation/bedtime.png" alt="Bedtime flow in Node Red"&gt;&lt;/p&gt;
&lt;p&gt;The pink boxes in this flow are examples of calling saved, reusable routines. It&amp;rsquo;s an easy way to reduce the complexity of flows, the same way you&amp;rsquo;d write your own modules or functions in node.&lt;/p&gt;
&lt;h2 id="tying-it-all-together"&gt;Tying it all together&lt;/h2&gt;
&lt;p&gt;At the end of the day, however, it&amp;rsquo;s hard to beat the simplicity of a light switch. Finding your phone or navigating to a web page to turn on the dryer or the lights isn&amp;rsquo;t very convenient. To address that aspect, I got (yet another) Raspberry Pi, and a compatible touch screen, to create something that hopefully bridges some of that gap.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/home-automation/touchscreen.jpeg"
alt="A touchscreen showing Home Assistants dashboard"
width=500&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s been a fun adventure, and I&amp;rsquo;ll probably continue building it out for a while.&lt;/p&gt;</description></item><item><title>A year on</title><link>https://eamonnsullivan.co.uk/posts-output/2021-03-16-a-year-on/</link><pubDate>Tue, 16 Mar 2021 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2021-03-16-a-year-on/</guid><description>&lt;p&gt;A year ago, I was told to &lt;a href="https://eamonnsullivan.co.uk/posts-output/2020-03-16-home-sweet-work/"&gt;vacate the office&lt;/a&gt; and start working from home. I expected it to last several weeks or a few months, at most. I also expected it to be fun. Working from home &lt;em&gt;is&lt;/em&gt; fun. Conducting every meeting, every discussion, every personal interaction of any kind, for months on end, through the virtual keyhole that is Zoom is a bit less so.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve adapted surprisingly well, in some respects. I talk to my children more frequently now than I did before the pandemic, for example, and I&amp;rsquo;ve slowly assembled a very comfortable working environment in one of our spare bedrooms. But I can also count quite a few costs: I&amp;rsquo;ve lost a year in my seven-year-old granddaughter&amp;rsquo;s life. I miss being able to hug my kids, something I now realise I did far too infrequently before the pandemic. (Let that be a lesson: Hug while you can.)&lt;/p&gt;
&lt;p&gt;And the cost has been too high for society. The number of visitors to the food bank I volunteer at has grown more than five fold in the past year, with long queues of desperate people pushed over the edge, forced to choose between eating and keeping the lights on or paying the rent.&lt;/p&gt;
&lt;p&gt;Beyond the obvious changes, though, there has been a more subtle shift, so gradual that I&amp;rsquo;ve almost missed it. My days flow through my various &amp;ldquo;roles&amp;rdquo; more fluidly than before.&lt;/p&gt;
&lt;p&gt;I wake up, go for a jog around the neighbourhood, hop on a stand-up meeting on zoom, then pop a load of laundry in. I work for a bit, then have a tea with my wife, maybe in the garden if the weather&amp;rsquo;s nice. Over lunch, I pop down to the shops and catch up with the neighbour. In the afternoon, I might have to sort something for the food bank, in between bouts of work or meetings. After dinner, I might take another stab at some of the home-automation software I&amp;rsquo;ve been working on, or have a zoom with the family, or answer that work email.&lt;/p&gt;
&lt;p&gt;I am almost certainly &lt;em&gt;more&lt;/em&gt; productive at work, and put in similar (or more) hours, but my various roles are braided through my day.&lt;/p&gt;
&lt;p&gt;I imagine this is how it&amp;rsquo;s like to be a farmer. One minute, you are milking the cows, the next you&amp;rsquo;re fixing the roof on the house, or helping the kids with schoolwork. Life is one big lump, with no sharp lines.&lt;/p&gt;
&lt;p&gt;To be sure, that can be a bad thing: no boundaries between life and work can lead to burnout. But I think that&amp;rsquo;s a danger only if you mix up the priorities. You can see the old assumptions in the post I made on the day (linked above). I took as given the rigid separation between my work-me and me-me, and vowing to keep the boxes separate. But, really, all my roles are important and have their place and time: husband, father, son, software engineer, neighbour, volunteer.&lt;/p&gt;
&lt;p&gt;We just have to learn to relax into that. We are a complicated whole, made up of many personal and economic connections. Without that sharp change of location, with the commute between, that web of roles and links has become more obvious. Maybe the stress we feel is that we fail to embrace all those parts of us.&lt;/p&gt;
&lt;p&gt;Life would be boring, if it was easy. I&amp;rsquo;m glad that it&amp;rsquo;s a year on and I&amp;rsquo;m still trying to get the hang of this. I&amp;rsquo;m still learning.&lt;/p&gt;</description></item><item><title>Walking along with suffering</title><link>https://eamonnsullivan.co.uk/posts-output/2020-12-06-walking-along-with-suffering/</link><pubDate>Sun, 06 Dec 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2020-12-06-walking-along-with-suffering/</guid><description>&lt;p&gt;I&amp;rsquo;ve been volunteering for a couple of years with the &lt;a href="https://www.jrsuk.net/"&gt;Jesuit Refugee Service&lt;/a&gt;, which aims to support refugees, asylum seekers and migrants. I&amp;rsquo;m a volunteer &amp;ldquo;social visitor.&amp;rdquo; Before the lockdown, that meant regularly travelling out to the Immigration Detention Centres near Heathrow, Europe&amp;rsquo;s largest. (There are two centres, next to each other, called Harmondsworth and Colnbrook.)&lt;/p&gt;
&lt;p&gt;Several hundred young men, and a very small number of women, are held here indefinitely, often for several weeks or months, while the courts consider their cases. They are usually alone, with no family or friends to visit them. My job is mostly to just talk with them and treat them as people, not cases to be adjudicated.&lt;/p&gt;
&lt;p&gt;During the pandemic, these visits continue, but by phone.&lt;/p&gt;
&lt;p&gt;The U.K. is a particularly inhospitable place for refugees, sadly. We&amp;rsquo;re in a period of time where attitudes to outsiders of any kind have hardened and nationalism and xenophobia are on the rise. I feel the work is as important as ever.&lt;/p&gt;
&lt;p&gt;This month, I was asked to do the &amp;ldquo;reflection&amp;rdquo; for the JRS&amp;rsquo;s monthly newsletter, on the themes of the Advent season and on its mission of &lt;a href="https://www.jrsuk.net/accompany/"&gt;accompanying&lt;/a&gt; people through this brutal, heartless process. I&amp;rsquo;ll share the thought here, as well.&lt;/p&gt;
&lt;blockquote&gt;
When I first started visiting detainees in Harmondsworth and Colnbrook about two years ago, my aim was "do something" for those at the bottom of our society -- the people jailed indefinitely by an inhumane, degrading immigration and asylum system. I quickly learned how wrong-headed I was.
&lt;p&gt;The people I met needed solicitors who returned their calls, immigration officials who cared, edible food, a safe environment, the company of their loved ones and medical care. I could provide them with none of these. I met them, after being searched and led through several layers of locked doors and guards, with empty hands and pockets. Even the room we shared (back when we could share a room) offered about as much comfort or warmth as a GP&amp;rsquo;s waiting room. I was not a doer of &amp;ldquo;something.&amp;rdquo; I was helpless.&lt;/p&gt;
&lt;p&gt;Instead, in this helplessness, I learned what the Jesuit Refugee Service meant by accompaniment. To walk along with people who have suffered, are suffering and still have much more suffering ahead of them is not something you do. It&amp;rsquo;s something you are. It&amp;rsquo;s about being, not doing. Just being present. Listening to, and seeing them. Recognising them as fellow human beings. It feels like nothing, because it is. I’m not doing anything. God is. My job is to just be with this person in front of me, who has been through more than I can imagine. God works through that, somehow, through nothing I do or say.&lt;/p&gt;
&lt;p&gt;Often, the benefit of visits isn&amp;rsquo;t evident. I leave someone who appears to be just as depressed as when I arrived. But, sometimes, God does give me a glimpse of the fruits. Farhad &lt;a href="#footnote1"&gt;(1)&lt;/a&gt;, who I call regularly at the moment, refers to our conversations (which are almost always about nothing) as &amp;ldquo;talking therapy&amp;rdquo;, while another I met says he was able to sleep best after our visits. Usually when I least expect it, God shows me how He is able to use what I could consider to be nothing. Truly, the yoke &lt;em&gt;is&lt;/em&gt; easy.&lt;/p&gt;
&lt;p&gt;In this season of Advent, we are asked to think about what we bring to Christ in our midst. The asylum seeker, reviled by so many, blamed for so much, is among today&amp;rsquo;s &amp;ldquo;least of my brothers&amp;rdquo;, very much like the homeless, refugee family living in a barn at the centre of the Advent season. I have no gold, frankincense or myrrh, but I have something even more. If you doubt the power of just being present to each other &amp;ndash; even just being merely respectful and decent &amp;ndash; imagine what the world would look like if we all did that? It would be heaven on earth, with no more individual effort than a smile, attention and love. No amount of gold could come close.&lt;/p&gt;
&lt;p&gt;At the start of this journey, I was driven more by anger at the injustice of it all, and the need to fight back. I&amp;rsquo;m still angered by the hostile environment and culture of disbelief that makes so many lives miserable. But I&amp;rsquo;m slowly learning that fighting isn&amp;rsquo;t the answer. Loving is the only response.&lt;/p&gt;
&lt;div id="footnote1"&gt;[1] not his real name.&lt;/div&gt;
&lt;/blockquote&gt;</description></item><item><title>Developers Bin</title><link>https://eamonnsullivan.co.uk/posts-output/2020-11-05-developers-bin/</link><pubDate>Thu, 05 Nov 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2020-11-05-developers-bin/</guid><description>&lt;p&gt;I&amp;rsquo;m fortunate to work for a team that gives its developers time to play &amp;ndash; a day, every other week, that we can use to learn about or try something new. Over the years, a number of useful stuff has come out of those days. Many of them are small utilities that developers have created for themselves &amp;ndash; for example, to automate a task. We&amp;rsquo;ve gathered quite an eclectic collection of these, written in all sorts of languages and frameworks.&lt;/p&gt;
&lt;p&gt;Unfortunately, for the most part, these were just being used by the programmer who wrote them, squirrelled away in some directory in their path (&lt;code&gt;~/bin&lt;/code&gt;). For example, I&amp;rsquo;ve written some command-line utilities in Clojure that I think others would find useful, but we&amp;rsquo;re a Scala and NodeJS shop. I don&amp;rsquo;t want them to have to install Clojure build tools to try them out.&lt;/p&gt;
&lt;p&gt;How do we make it easy for team members to share these utilities with their colleagues?&lt;/p&gt;
&lt;h2 id="private-homebrew-taps"&gt;Private Homebrew Taps&lt;/h2&gt;
&lt;p&gt;The solution was to create our own, private &lt;a href="https://brew.sh/"&gt;Homebrew&lt;/a&gt; repository that I&amp;rsquo;ve dubbed our &amp;ldquo;Developers Bin.&amp;rdquo; I chose Homebrew because most of us use Macs and already &lt;code&gt;brew install&lt;/code&gt; applications. And, although I&amp;rsquo;m on Ubuntu most of the time, brew is &lt;a href="https://docs.brew.sh/Homebrew-on-Linux"&gt;on Linux&lt;/a&gt;, as well.&lt;/p&gt;
&lt;p&gt;Homebrew &amp;ldquo;formulas&amp;rdquo;, if written well, automatically install and configure an application, and ensure that dependencies are installed as well. My colleagues can just &lt;code&gt;brew install &amp;lt;my application&amp;gt;&lt;/code&gt; and not know, nor care, that it was written in Go, Elm, Haskell or Clojure. It just runs.&lt;/p&gt;
&lt;p&gt;One of the first hurdles you&amp;rsquo;ll run into, though, is how to create a private repository. Creating a Homebrew repository that others can &amp;ldquo;tap&amp;rdquo; is simple &amp;ndash; just create a repo on Github of the form &lt;code&gt;&amp;lt;user&amp;gt;/homebrew-&amp;lt;repo-name&amp;gt;&lt;/code&gt;. Users can just &lt;code&gt;brew tap &amp;lt;user&amp;gt;/&amp;lt;repo-name&amp;gt;&lt;/code&gt; and then be able to &lt;code&gt;brew install&lt;/code&gt; anything specified in a &lt;code&gt;Formulas&lt;/code&gt; directory. But if that repo must be private, as in our case, that&amp;rsquo;s a little trickier.&lt;/p&gt;
&lt;p&gt;Fortunately, someone&amp;rsquo;s already &lt;a href="https://lessthanhero.io/post/homebrew-with-private-repo-releases/"&gt;written up&lt;/a&gt; instructions on how to do that, including the crucial &lt;code&gt;custom_download_strategy.rb&lt;/code&gt; that&amp;rsquo;s needed. This bit of Ruby code used to be part of Homebrew, but the maintainers (probably sensibly) didn&amp;rsquo;t want to have to maintain code that they don&amp;rsquo;t use themselves, so it&amp;rsquo;s been ripped out.&lt;/p&gt;
&lt;h2 id="generating-and-updating-formulas"&gt;Generating and Updating Formulas&lt;/h2&gt;
&lt;p&gt;The next hurdle was to make it easy for my colleagues to add formulas to our repository and keep them updated. The strategy I&amp;rsquo;ve settled on is to create a &amp;ldquo;template&amp;rdquo; for each formula that then gets turned into a formula (or updated formula) by a Jenkins job configured to run when code is merged into the main branch.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example template (somewhat redacted) that I used for one of my utilities in Clojure. Notice the placeholders for &lt;code&gt;_RELEASE_VERSION_&lt;/code&gt;, &lt;code&gt;_RELEASE_URL_&lt;/code&gt; and &lt;code&gt;_RELEASE_SHA__&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ruby" data-lang="ruby"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;require_relative&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;../custom_download_strategy.rb&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QuickLogin&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Formula&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Simple command-line interface for logging into one of our services.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;homepage&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://github.com/...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;_RELEASE_VERSION_&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;_RELEASE_URL_&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:using&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;CustomGitHubPrivateRepositoryReleaseDownloadStrategy&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sha256&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;_RELEASE_SHA_&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;bottle&lt;/span&gt; &lt;span class="ss"&gt;:unneeded&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;depends_on&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;openjdk&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;uses_from_macos&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ruby&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:build&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;install&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;system&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;./install.sh&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env_script_all_files&lt;/span&gt; &lt;span class="n"&gt;libexec&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:JAVA_HOME&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;${JAVA_HOME:-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Formula&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;openjdk&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;].&lt;/span&gt;&lt;span class="n"&gt;opt_prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# cosmos-login --help&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It runs my install.sh script, pulls in Openjdk as a dependency and installs the code on the user&amp;rsquo;s path. I&amp;rsquo;ve written a few variations of this template, depending on the language or runtime environment of the application. Our developers can just copy one that &lt;code&gt;depends on &amp;quot;node&amp;quot;&lt;/code&gt; or &lt;code&gt;depends on &amp;quot;python&amp;quot;&lt;/code&gt; or whatever and modify it.&lt;/p&gt;
&lt;p&gt;I then had to write a script (run by Jenkins) to do the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a release in Github.&lt;/li&gt;
&lt;li&gt;Build the code &amp;ndash; generating a JAR file in this case.&lt;/li&gt;
&lt;li&gt;Upload the JAR, and anything else needed, such as the install.sh script, to Github as a tar.gz file, tagged with the release.&lt;/li&gt;
&lt;li&gt;Finally, the script checks out our homebrew repo, uses the template to create (or update an existing) formula and checks the changes in again.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That sounds like a lot, and it is the first time you have to create one, but I&amp;rsquo;ve written one for Clojure, Python, Node and Bash scripts so far. Once I&amp;rsquo;ve written one, it&amp;rsquo;s very easy to modify slightly for the next.&lt;/p&gt;
&lt;p&gt;When a user runs &lt;code&gt;brew install&lt;/code&gt;, for example, or &lt;code&gt;brew upgrade&lt;/code&gt; (if they already have the application installed), Homebrew uses the instructions in the formula to download the artifact (using the SHA to do an integrity check) and install or update the application.&lt;/p&gt;
&lt;p&gt;For the first few utilities, I had to do some of the heavy lifting &amp;ndash; create the scripts and the Jenkins jobs. But once my colleagues started to see our &amp;ldquo;Developers Bin&amp;rdquo; as a valuable shared resource, they&amp;rsquo;ve started adding formulas and templates themselves &amp;ndash; one or two a month.&lt;/p&gt;
&lt;p&gt;We now have a valuable toolbox of utilities that are easy to install, easy to use and make our day-to-day work easier.&lt;/p&gt;</description></item><item><title>Who Am I?</title><link>https://eamonnsullivan.co.uk/posts-output/2020-05-24-who-am-i/</link><pubDate>Sun, 24 May 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2020-05-24-who-am-i/</guid><description>&lt;p&gt;The extended family members are getting together again tonight, on the usual unwieldy, international Zoom conference with too many people on it. In an effort to make it less cacophonous, we&amp;rsquo;re each being asked to come up with something to share &amp;ndash; a piece of music, a poem, or whatever &amp;ndash; that we liked. We&amp;rsquo;ll each get three minutes.&lt;/p&gt;
&lt;p&gt;I like the idea, so I thought I&amp;rsquo;d write my thoughts on my &amp;ldquo;thing&amp;rdquo; beforehand, so that I can make the best of my three minutes.&lt;/p&gt;
&lt;p&gt;My show-and-tell &amp;ndash; Richard Dawkins&amp;rsquo;s 2005 Ted Talk on &amp;ldquo;Why the universe seems so strange.&amp;rdquo; &amp;ndash; is a bit odd. I have, in the past, really disliked Dawkins &amp;ndash; a dogmatic, arrogant man who has lifted the &lt;a href="https://en.wikipedia.org/wiki/Straw_man"&gt;straw man&lt;/a&gt; argument to a new art form.&lt;/p&gt;
&lt;p&gt;But in this talk, those flaws are less evident. It&amp;rsquo;s a humble exploration of the limits of knowledge, and our ability to understand a universe that we can only glimpse in a narrow, distorted way. He starts by quoting biologist J.B.S. Haldane:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;My own suspicion is that the universe is not only queerer than we suppose, but queerer than we can suppose.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The whole thing is about 20 minutes and very well worth a listen. It goes into why we have such a hard time understanding the concepts in the very small (e.g., quantum effects) and very large and fast (the strange quirks of the Theory of Relativity). I&amp;rsquo;ve linked to it, below.&lt;/p&gt;
&lt;p&gt;The part that stuck with me for years, however, was when he described how we are like a wave, comparing us to dunes that retain their shape as the winds march them across the desert. He summarises &lt;a href="https://en.wikipedia.org/wiki/Steve_Grand_(roboticist)"&gt;Steve Grand&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Think of an experience from your childhood, something you remember clearly, something you can see, feel, maybe even smell, as if you were really there. After all, you really were there at the time, weren&amp;rsquo;t you? How else would you remember it? But here is the bombshell: You weren&amp;rsquo;t there. Not a single atom that is in your body today was there when that event took place. Matter flows from place to place and momentarily comes together to be you. Whatever you are, therefore, you are not the stuff of which you are made.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The image is extraordinary: atoms, forged in exploding stars over billions of years, temporarily come together to form you, in this moment in time, and then move on. It has stayed with me ever since. I think about it every time I walk along a beach. We&amp;rsquo;re a wave of organised information, moving through time and space, until in the end we crest and crash into some distant shore.&lt;/p&gt;
&lt;p&gt;And that image has led me to explore what it means to be me. What is the mind, what makes me unique and how am I connected to everyone else? It&amp;rsquo;s led me back to exploring science and reading about physics, something I failed at emphatically in school. I&amp;rsquo;ve been fascinated by the study of our sense of self, the way we think, the limits of our free will and our sense of time.&lt;/p&gt;
&lt;p&gt;Perhaps it would irk Dawkins to learn that his talk also led me &lt;em&gt;back&lt;/em&gt; to religion after I had mostly fallen away. All major world religions are remarkable similar on this point &amp;ndash; the miraculous nature of us and the present moment. The way we are all connected, literally moving through, breathing and having our being in the fabric of the universe, which they call God.&lt;/p&gt;
&lt;p&gt;In the 15 years since I first heard this, I&amp;rsquo;ve also reevaluated my thinking on Dawkins. The straw man he knocks down is the concept of a magical being with a flowing beard, up in the sky somewhere. He&amp;rsquo;s smart enough to know that religions really teach nothing of the sort, which has always irked me, but what he&amp;rsquo;s really critiquing is the small-minded concepts that we&amp;rsquo;ve made into religions.&lt;/p&gt;
&lt;p&gt;If God exists, he is queerer than we suppose, and most definitely queerer than we can suppose. So, in a sense, Dawkins is more truly in touch with that concept than most religious people, and most religions. A modern prophet. Who would have guessed?&lt;/p&gt;
&lt;div style="max-width:854px"&gt;&lt;div style="position:relative;height:0;padding-bottom:56.25%"&gt;&lt;iframe src="https://embed.ted.com/talks/richard_dawkins_why_the_universe_seems_so_strange" width="854" height="480" style="position:absolute;left:0;top:0;width:100%;height:100%" frameborder="0" scrolling="no" allowfullscreen&gt;&lt;/iframe&gt;&lt;/div&gt;&lt;/div&gt;</description></item><item><title>Remote First, Pair Programming and Emacs</title><link>https://eamonnsullivan.co.uk/posts-output/2020-04-25-remote-first-emacs/</link><pubDate>Sat, 25 Apr 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2020-04-25-remote-first-emacs/</guid><description>&lt;p&gt;At my work, we are advocates of &lt;a href="https://en.wikipedia.org/wiki/Pair_programming"&gt;pair programming&lt;/a&gt;. From an organisational perspective, the practice has several advantages. We&amp;rsquo;re often more productive working with someone else &amp;ndash; we keep each other on task and get stuck less often and for shorter periods of time. More importantly, the resulting code is generally better and more robust, in my experience, especially when combined with test- or &lt;a href="https://en.wikipedia.org/wiki/Behavior-driven_development"&gt;behaviour-driven development&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Businesses often object to the practice based on cost. Why pay two programmers to do the work of one? But at the end of the day, you&amp;rsquo;ll have two developers intimately familiar with the code for future modifications and maintenance &amp;ndash; i.e., the bulk of the software&amp;rsquo;s lifespan. Yes, you spend more in the short term, but for a better payoff in the future. You&amp;rsquo;re investing in the team, spreading the knowledge more widely.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve had to make some modifications to the practice, because our team hasn&amp;rsquo;t been co-located for some time. The lockdown has just reinforced those practices. Rather than sitting at the same workstation, passing the keyboard back and forth, we pair remotely in various different ways.&lt;/p&gt;
&lt;h2 id="git-ping-pong"&gt;Git Ping Pong&lt;/h2&gt;
&lt;p&gt;My favourite approach, even when sat next to a colleague, is to ping-pong the work through a shared repository (such as Github). One of us writes a test, which fails, and commits the code. The other, checks out the code and makes the test pass. For a bigger refactoring, I&amp;rsquo;ll try to use the &lt;a href="https://francescocirillo.com/pages/pomodoro-technique"&gt;pomodoro technique&lt;/a&gt;. Each of us types for 25 minutes and then commits the work-in-progress. We take a break between sessions.&lt;/p&gt;
&lt;p&gt;One big advantage of the ping-pong approach is that both of you must be set up to work on the code &amp;ndash; at a minimum, be able to compile and run the tests. Getting to that point is often a big hurdle with complex software. If you can get two programmers to that stage, you&amp;rsquo;ve again increased the robustness and resilience of the team as a whole. Also, at any given time, our work-in-progress is committed and available, making it easier to do things like mix up pairs, do interim code reviews or pursue experimental paths with the confidence of having a known good state behind us.&lt;/p&gt;
&lt;p&gt;Generally, I try to make sure that the person who is &amp;ldquo;driving&amp;rdquo; the work isn&amp;rsquo;t the same as the person typing. So, for example, I&amp;rsquo;ll be telling the person what test to write, and they will be telling me how to get the test to pass. This keeps us both fully engaged. But usually, once we really get going, the roles blur and we mind-meld into a single programmer with twice the brain power. It&amp;rsquo;s quite magical when you hit that stride!&lt;/p&gt;
&lt;h2 id="editor-wars"&gt;Editor Wars&lt;/h2&gt;
&lt;p&gt;As an Emacs user &amp;ndash; and the sole Emacs user on my team &amp;ndash; I face a couple of unique challenges. My pair for the day will often be intimidated by the spartan interface and unfamiliar keyboard shortcuts. There is one Vim user among my colleagues, but most use either IntelliJ IDEA or VS Code. The VS Code users will also sometimes use the &lt;a href="https://code.visualstudio.com/blogs/2017/11/15/live-share"&gt;Live Share&lt;/a&gt; feature, which I agree is a nice way to share a session.&lt;/p&gt;
&lt;p&gt;This can be a source of friction. Two programmers each using their own editor doesn&amp;rsquo;t fit the traditional definition of pair programming, and people do get quite heated on this point of doctrine. Why bother if you don&amp;rsquo;t do it &amp;ldquo;right&amp;rdquo;, they ask? Some programmers also can&amp;rsquo;t understand why I won&amp;rsquo;t just use IntelliJ or VS Code. To them, I&amp;rsquo;m being pig-headed or stubborn, but I feel as crippled in those editors as they probably do in Emacs or Vim.&lt;/p&gt;
&lt;p&gt;Emacs has a couple of other disadvantages for pairing remotely, as well. It doesn&amp;rsquo;t show line numbers by default, for example. You can&amp;rsquo;t easily point to a section of code verbally (&amp;ldquo;Now, change line 23 so that it uses the variable on line 6&amp;rdquo;). I also like to have as much of my code on the screen at once. I turn off all the menus and other distractions and set the font size a bit low. That&amp;rsquo;s not as nice an experience for my remote partner.&lt;/p&gt;
&lt;h2 id="visual-niceties"&gt;Visual Niceties&lt;/h2&gt;
&lt;p&gt;To make the experience of working with me as pleasant as possible, I&amp;rsquo;ve recently tweaked my configuration. I have added a couple of items to my top-level menu (bound to Ctrl-z) to let me turn line numbers on and off and adjust the font size globally. Neither involved writing any new code &amp;ndash; the functionality was already in Emacs. Here&amp;rsquo;s how I set up my main menu, for which I use the &lt;a href="https://github.com/abo-abo/hydra"&gt;hydra&lt;/a&gt; package:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-elisp" data-lang="elisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;global-set-key&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;kbd&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;C-z&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;defhydra&lt;/span&gt; &lt;span class="nv"&gt;hydra-global-menu&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;:color&lt;/span&gt; &lt;span class="nv"&gt;red&lt;/span&gt; &lt;span class="nb"&gt;:hint&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;^Display^ ^Buffers^ ^Actions^
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;^^^^^^^^^-----------------------------------------------------
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;_g_: zoom-in _d_: close all buffers _u_: update all packages
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;_s_: zoom-out _o_: open buffer on desktop _l_: display line numbers
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;_q_: quit this menu _r_: restart emacs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;g&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;default-text-scale-increase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;s&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;default-text-scale-decrease&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;d&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;kill-all-buffers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;l&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;global-display-line-numbers-mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;r&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;stop-and-restart-emacs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;u&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;eds-straight-pull-or-prune&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;o&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;eds/open-buffer-on-desktop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;q&amp;#34;&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The above makes a colour-coded menu appear at the bottom of my screen whenever I type C-z. The first two items (zoom-in and zoom-out) and the one to display line numbers were added specifically for remote working. I can quickly set up a font size that works for both of us and turn on line numbers with just a couple of keystrokes. I&amp;rsquo;ve also been experimenting with different themes, to make the contrast better and easier to read on a small screen over a remote link. My current is &lt;a href="https://github.com/fniessen/emacs-leuven-theme"&gt;Leuven&lt;/a&gt;. For the pomodoro technique, I use &lt;a href="https://github.com/TatriX/pomidor"&gt;pomidor&lt;/a&gt;, which displays a nice, big colour coded history of the time we&amp;rsquo;ve spent in each session.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in some of the other items in that menu, my whole &lt;a href="https://github.com/eamonnsullivan/emacs.d"&gt;configuration&lt;/a&gt; is on Github.&lt;/p&gt;
&lt;h2 id="reducing-friction"&gt;Reducing Friction&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m not going to convince any of my colleagues to pick up Emacs &amp;ndash; I&amp;rsquo;ve long ago given up on religious wars &amp;ndash; but we can meet in the middle, both of us using the environment and editor that makes us as productive as possible.&lt;/p&gt;
&lt;p&gt;In the end, the aim is to get to that mind-meld stage &amp;ndash; getting into the flow. Anything that reduces the friction helps. If we&amp;rsquo;re both comfortable in our working environment, can see and talk to each other easily and can view each other&amp;rsquo;s screens, we&amp;rsquo;re well on our way.&lt;/p&gt;</description></item><item><title>A Sustainable Pace</title><link>https://eamonnsullivan.co.uk/posts-output/sustainable-pace/2020-04-10-a-sustainable-pace/</link><pubDate>Fri, 10 Apr 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/sustainable-pace/2020-04-10-a-sustainable-pace/</guid><description>&lt;p&gt;It&amp;rsquo;s now been a few weeks since many of us in the U.K. have gone into lockdown. We&amp;rsquo;re spending a lot more time at home and our social calendar has suddenly freed up, completely, for the foreseeable future. So why are we so tired?&lt;/p&gt;
&lt;p&gt;When we first moved to the U.K. in 1998 from the Boston, Massachusetts area, my very smart Aunt Kathleen gave my wife and I an abbreviated version of the course she gave to corporate managers who were about to go on overseas assignments. We found this training to be extremely helpful in the first few months in London. It was a guide for handling life-upturning change, precisely what we&amp;rsquo;re going through again right now, and is one of the reasons I was expecting the tiredness.&lt;/p&gt;
&lt;p&gt;So it&amp;rsquo;s a useful exercise to revisit some of that wisdom and see what it could teach us about this period in our lives.&lt;/p&gt;
&lt;h2 id="mood-swings"&gt;Mood swings&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/sustainable-pace/oscillation.png" alt="Damping oscillation graph"&gt;&lt;/p&gt;
&lt;p&gt;One unfortunate side-effect of our radical change in location in 1998 was rapid swings of mood. We&amp;rsquo;d go from, &amp;ldquo;This is great &amp;ndash; it&amp;rsquo;ll be like vacationing all the time!&amp;rdquo; to &amp;ldquo;What the hell have we done with our life?&amp;rdquo; from one day to the next.&lt;/p&gt;
&lt;p&gt;Aunt Kathleen showed us a graph similar to the one above. She said to expect times where things looked unrealistically wonderful, followed by bleak periods. These swings will become less sharp and more gradual over time, she said.&lt;/p&gt;
&lt;p&gt;I like to think of it as a stone thrown into a pond. The waves are high and frequent closer to the event, but become less pronounced the further you move away (in time and space) from the disruption.&lt;/p&gt;
&lt;p&gt;I can&amp;rsquo;t say I had many periods of euphoria about lockdown, but I&amp;rsquo;d also be lying if I said I didn&amp;rsquo;t look forward to working from home for an extended period. Quickly, however, the reality of using my home as my place of work sunk in. The constant video conferencing, the poorer office equipment and less downtime and separation from work got old quickly. So has separation from my family &amp;ndash; all five of my children and my sister in London used to get together very frequently. Now we see each other only on video. My enthusiasm for it all as waned.&lt;/p&gt;
&lt;p&gt;As the weeks drag on, though, I&amp;rsquo;m getting used to things. I still have swings in mood from day to day, but there becoming less sharp, more just the normal ups and downs. I&amp;rsquo;m beginning to appreciate the upsides &amp;ndash; I can wander out to my garden for breaks, and my wife and I can spend more time together. It&amp;rsquo;ll be OK. The waters are calming.&lt;/p&gt;
&lt;h2 id="why-are-we-tired"&gt;Why are we tired&lt;/h2&gt;
&lt;p&gt;Another aspect of radical change that my aunt warned us of was exhaustion. You would think moving from New England to Old England wouldn&amp;rsquo;t be that big of a jump &amp;ndash; and it wasn&amp;rsquo;t. We spoke more or less the same language, for example.&lt;/p&gt;
&lt;p&gt;But there was a constant underlying current of effort &amp;ndash; things were slightly different everywhere we went. The foods we were used to buying in the supermarket weren&amp;rsquo;t there, so we had to find substitutes. The schools were organised completely differently, and we ended up with our children split between two primary schools. Driving on the left, figuring out the Tube, understanding some of the accents, and on and on.&lt;/p&gt;
&lt;p&gt;None of the individual changes were overwhelming. They were just minor adjustments, for the most part. But a whole day of learning new things took its toll. We were exhausted by the time we went to bed each night.&lt;/p&gt;
&lt;p&gt;Similarly, the cognitive load of constant remote working and socialising is draining. We don&amp;rsquo;t notice each adjustment, each individual tweak to our life, yet they suck the energy out of us.&lt;/p&gt;
&lt;p&gt;We have to learn to take it easy on ourselves. We simply cannot keep going at the same pace that we were in the before-time (or BC, Before Coronavirus, as it coming to be known). And that&amp;rsquo;s OK. We&amp;rsquo;re not just working from home, we&amp;rsquo;re trying to keep going through some truly historic upheavals. We need to be kind to ourselves &amp;ndash; go to bed earlier, rest up. Just because we&amp;rsquo;re not going out, for example, doesn&amp;rsquo;t mean we have to fill all that time with learning a new language or new instrument.&lt;/p&gt;
&lt;h2 id="things-that-help"&gt;Things that help&lt;/h2&gt;
&lt;p&gt;The final part of my aunt&amp;rsquo;s training was to suggest things that can help get over the worst effects of change. For example, she suggested drawing a rough map of the neighbourhood (the location of the local shops, schools and church) and sticking it on the fridge, to help us make the mental adjustment to our new environment. A lot of what she suggested, however, was specific to relocating and not as relevant to what we&amp;rsquo;re going through now. So what would help?&lt;/p&gt;
&lt;p&gt;One of the big surprises about the lockdown is what I&amp;rsquo;m missing. It&amp;rsquo;s not what I would have expected. Take commuting &amp;ndash; why would I miss that? I ride a motorcycle to work. Operating 250 kg (550 pounds) of metal, on two wheels, in London traffic &amp;ndash; especially the way we do it around here, using the space between lanes of traffic &amp;ndash; is an activity that fully engages the mind and body. If I want to survive the experience, I have to be awake and aware. I don&amp;rsquo;t have the mental space to be thinking too much about what I want to accomplish today (on the way to work) or regretting what I didn&amp;rsquo;t (on the way home). That commute was a very useful boundary period &amp;ndash; a total separating wall &amp;ndash; between work time and home time.&lt;/p&gt;
&lt;p&gt;It also was a very useful activity for my mental health. I practice mindfulness meditation, have for years and still do it every morning. Motorcycle riding is very different, but it has similar benefits. The first chapter of &lt;a href="https://en.wikipedia.org/wiki/Zen_and_the_Art_of_Motorcycle_Maintenance"&gt;Zen and the Art of Motorcycle Maintenance&lt;/a&gt; describes it well. It&amp;rsquo;s nothing like driving a car. You are &lt;em&gt;in&lt;/em&gt; the environment, the weather literally (and often, here) streams off you and drips down your back. You&amp;rsquo;re not just watching the world go by like it&amp;rsquo;s a TV channel.&lt;/p&gt;
&lt;p&gt;I miss those two periods of my day when I was brought out of my thoughts, into the stark reality of how I&amp;rsquo;m going to safely thread my way through that black cab to my left, and the lorry to my right. I&amp;rsquo;m finding that I need substitutes during this period. They don&amp;rsquo;t have to be death-defying, but they need to be engaging in a way that is completely different than my work.&lt;/p&gt;
&lt;p&gt;Writing this blog is one such activity. It wouldn&amp;rsquo;t have helped at all for when I was a reporter or editor, but it is quite a break from working on software. Another activity is gardening and general DIY around the house. I&amp;rsquo;m absolutely rubbish at that kind of work, so it does occupy me more than most people. I&amp;rsquo;m also find that I need more play. My wife and I have started playing pool more frequently, for example.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m sure as this lockdown drags on that we&amp;rsquo;ll get better and better at coping, and even thriving. I&amp;rsquo;m actually looking forward to the adventure, good and bad. That&amp;rsquo;s what got us out of that transition period when we moved here. We started looking forward more than backwards. Instead of comparing our new life with the old, we just started noticing and appreciating our life, as it is.&lt;/p&gt;
&lt;p&gt;Then and now, we&amp;rsquo;re taking things one day at a time, at a sustainable pace.&lt;/p&gt;</description></item><item><title>A well-honed tool</title><link>https://eamonnsullivan.co.uk/posts-output/2020-04-05-well-honed-tool/</link><pubDate>Sun, 05 Apr 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2020-04-05-well-honed-tool/</guid><description>&lt;p&gt;I took a few minutes last night to add a small feature to Emacs, saving me from some repetitive steps when I&amp;rsquo;m starting a blog post (like this one). It was a tiny bit of code and nothing exceptional in itself, but it reminded me of why I choose to use an editor that&amp;rsquo;s &lt;a href="https://en.wikipedia.org/wiki/GNU_Emacs"&gt;nearly as old as I am&lt;/a&gt;. It is designed for this kind of organic, incremental growth.&lt;/p&gt;
&lt;p&gt;I often find it difficult to explain to my younger colleagues why you would use Emacs or Vim (another vintage editor) over modern tools such as &lt;a href="https://code.visualstudio.com/"&gt;VS Code&lt;/a&gt; or &lt;a href="https://www.jetbrains.com/idea/"&gt;Intellij&lt;/a&gt;. The old editors are harder to learn. You can&amp;rsquo;t just sit down at one and be productive in minutes. It takes some effort, and usually at least a several weeks of it, to get the hang of them.&lt;/p&gt;
&lt;p&gt;But Emacs and Vim have been shaped, balanced, sharpened and smoothed over by decades of usage by hundreds of thousands of programmers, each trying to get through their day as efficiently and fuss-free as possible. In the right hands, they move lines, shift paragraphs and fling code better and faster than anything out there.&lt;/p&gt;
&lt;p&gt;They&amp;rsquo;ve evolved so much because it is trivial to extend them, to change even the most fundamental features. You just do it as a matter of course, to save a few keystrokes here and there. And many of these incremental modifications have been shared, one programmer to another, almost as long as there have been programmers.&lt;/p&gt;
&lt;p&gt;Today&amp;rsquo;s bit of magic took a lot less time to write than this blog post, but it turns a half-dozen steps into one. It opens this site&amp;rsquo;s Github project, creates a new branch (using a plugin called &lt;a href="https://magit.vc/"&gt;Magit&lt;/a&gt;), creates a new file and inserts some boilerplate configuration. With one keystroke, my cursor is blinking, in the right file, in a new branch, configured and ready for me to start typing the post.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-elisp" data-lang="elisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;eds/insert-skeleton-blog-post&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Insert a basic skeleton for a blog post.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;goto-char&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;point-min&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;{:title \&amp;#34;new post\&amp;#34;\n :layout :post\n :tags []}\n\n&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;init-git&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;;; for magit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;eds/start-blog-post&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Create a new post with today&amp;#39;s date and a new branch&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;interactive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let*&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;branch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;format-time-string&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;%Y-%m-%d&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;filename&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;~/git/eamonnsullivan.co.uk/content/md/posts/&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;branch&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;-new-post.md&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;find-file&lt;/span&gt; &lt;span class="nv"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;eds/insert-skeleton-blog-post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;save-buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;magit-branch-create&lt;/span&gt; &lt;span class="nv"&gt;branch&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;master&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;magit-checkout&lt;/span&gt; &lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s not great code. There&amp;rsquo;s no error handling (it&amp;rsquo;ll just blow up if there&amp;rsquo;s already a branch with the same name) and the file names, title and configuration are hard-coded, but it&amp;rsquo;s good enough to start with. It&amp;rsquo;ll continue to evolved, as my needs change. I might prompt for the title or tags, for example.&lt;/p&gt;
&lt;p&gt;Of course, you can extend Intellij and VS Code, too (VS Code has a &lt;a href="https://code.visualstudio.com/api"&gt;good API&lt;/a&gt;, for example). Users don&amp;rsquo;t focus on tinkering as much, mostly because these tools probably already have the features they want. I tried Intellij for a long time when I first started at my current job a few years ago, because I was new to the Java Virtual Machine world (this was for Scala) and absolutely everyone else used it. It has just about every feature you would want, and it mostly just works.&lt;/p&gt;
&lt;p&gt;What drew me back and motivated me to put effort into adding Scala features to Emacs is that I didn&amp;rsquo;t like all the black-box magic in Intellij. By adding the features I needed to Emacs, I was learning how the language and the build tools work, from the ground up. By shaping Emacs to fit my workflow, I believe I was becoming a better Scala programmer.&lt;/p&gt;
&lt;p&gt;My Emacs &lt;a href="https://github.com/eamonnsullivan/emacs.d"&gt;configuration&lt;/a&gt; is nearing its third decade. I started using it to write HTML and, once I got the hang of it, started using it for my day job as a journalist. I found myself enjoying its sparse, no-nonsense, no-distractions interface. And piece by piece, one tiny snippet at a time, it has become almost an extension of my brain. I wouldn&amp;rsquo;t trade it for anything.&lt;/p&gt;</description></item><item><title>I love Zoom, but...</title><link>https://eamonnsullivan.co.uk/posts-output/zoom-woes/2020-03-31-zoom-woes/</link><pubDate>Tue, 31 Mar 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/zoom-woes/2020-03-31-zoom-woes/</guid><description>&lt;p&gt;I&amp;rsquo;m trying to get one of the charities I work with on &lt;a href="https://zoom.us/"&gt;Zoom&lt;/a&gt; before our next scheduled meeting on Monday. This is at least the third time now that I&amp;rsquo;ve gone through the pain of getting a new group of people on the app, and it&amp;rsquo;s a very painful process. Every. Single. Time.&lt;/p&gt;
&lt;p&gt;What kind of problems, you ask? Let me count the ways.&lt;/p&gt;
&lt;h3 id="i-cant-see-or-hear-you"&gt;I can&amp;rsquo;t see (or hear) you..&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/zoom-woes/controls.png" alt="Zoom&amp;rsquo;s video and audio controls are hidden by default"&gt;&lt;/p&gt;
&lt;p&gt;One of the first problems new users run into is how to turn on the camera and microphone. For many good reasons, an application must be given permission to access your camera and microphone &amp;ndash; you don&amp;rsquo;t want apps to be able to surreptitiously film you or eavesdrop. But is it really necessary to &lt;strong&gt;hide&lt;/strong&gt; these controls the very first time a user runs your application?&lt;/p&gt;
&lt;p&gt;First of all, we have to ask if the user connected via a laptop or a phone, and because they are muted and have their camera off, they can&amp;rsquo;t tell you. If it&amp;rsquo;s the former, they have to move their mouse to the bottom of the screen to reveal these controls. If the latter, they have to touch the screen. Neither action is obvious.&lt;/p&gt;
&lt;p&gt;And the first questions to the new user are very confusing:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/zoom-woes/join_audio.png" alt="Guess which one?"&gt;&lt;/p&gt;
&lt;p&gt;What does &amp;ldquo;Call using Internet Audio&amp;rdquo; mean, exactly? I dunno either, and I make a living at this kind of thing. (&lt;em&gt;This is the one you want to pick, by the way&amp;hellip;&lt;/em&gt;). Even people who consider themselves technically savvy will be tempted by the other options (such as to &amp;ldquo;Dial in&amp;rdquo;), but those are almost always wrong.&lt;/p&gt;
&lt;h3 id="where-are-you"&gt;Where are you?&lt;/h3&gt;
&lt;p&gt;In most apps made in the last several years, if you want to share something that you&amp;rsquo;re looking at (like a Web address or photo), you click on a button usually labelled &amp;ldquo;share&amp;rdquo;. Often it has an arrow pointing up. In fact, just like this one:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/zoom-woes/share_button.png" alt="Don&amp;rsquo;t click this"&gt;&lt;/p&gt;
&lt;p&gt;You would expect this button to provide some way for you to share this meeting with someone else, so that they can join. But you would be wrong. This button is only used to share your desktop with the people who have already found your meeting.&lt;/p&gt;
&lt;p&gt;How you actually share the address of this meeting varies (as always) between the desktop and the phone apps. On my desktop, I have to find this tiny little &amp;lsquo;i&amp;rsquo; in a circle on the top left of my screen, like this one:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/zoom-woes/info.png" alt="Does the &amp;lsquo;i&amp;rsquo; mean info?"&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s really, really tiny and took me forever to find it. But when I did and decided to try clicking on it (not immediately obvious, in the least), I finally found what I was looking for:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/zoom-woes/shareurl.png" alt="Finding the address of your call is challenging"&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s just the beginning of the issues. Most people will also have to wrestle with their device settings (&lt;em&gt;completely&lt;/em&gt; different on the Mac and Windows), for example.&lt;/p&gt;
&lt;h3 id="practice-makes-perfect"&gt;Practice makes perfect&lt;/h3&gt;
&lt;p&gt;It&amp;rsquo;s so painful getting started on Zoom, it&amp;rsquo;s a wonder anyone uses it. We persevere because it is, by far, the best solution out there right now, with the best video quality and with the least restrictions on the free version.&lt;/p&gt;
&lt;p&gt;The only solution I&amp;rsquo;ve found for Zoom&amp;rsquo;s innumerable UX issues is practice, and lots of it. Our charity has several days until our first remote-first meeting, so I&amp;rsquo;m holding a surgery each night. I&amp;rsquo;m coaxing our members, one-by-one, around all of the obstacles and over all of the hurdles.&lt;/p&gt;
&lt;p&gt;By the time Monday comes around, we&amp;rsquo;ll still probably spend the first 20-30 minutes getting everyone connected. Once we connect, however, we&amp;rsquo;ll be able to carry on our work, which is needed more than ever now. And that&amp;rsquo;s what matters.&lt;/p&gt;</description></item><item><title>What will change?</title><link>https://eamonnsullivan.co.uk/posts-output/2020-03-29-changes/</link><pubDate>Sun, 29 Mar 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2020-03-29-changes/</guid><description>&lt;p&gt;Here&amp;rsquo;s a game to while away some of those long evenings in lockdown: Predict how the pandemic will change the world. What do you hope, or fear, will be different about our everyday life in a year or two?&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s likely that very little will change. We&amp;rsquo;re pretty good at getting used to anything and carrying on as before. Eventually, a vaccine, wide-spread immunity or an effective treatment will relegate COVID-19 to just another hazard out there that might kill us, but probably won&amp;rsquo;t. The roads will fill with traffic again, commuters will pack the Tube and we&amp;rsquo;ll all just go back to business as usual. It&amp;rsquo;s hard to knock us off track, especially well-grooved ones.&lt;/p&gt;
&lt;p&gt;But this pandemic is big enough that there will be changes in the way we see the world, some of them long lasting.&lt;/p&gt;
&lt;h3 id="cottage-industry"&gt;Cottage Industry&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s take an easy one to start: Many businesses will be discovering that they can operate from their employees&amp;rsquo; kitchen tables or spare bedrooms. It isn&amp;rsquo;t easy at first, but as the weeks drag on, we will start getting the hang of this. Stuff will get done.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;ll be hard to put that genie back in the bottle. The staff will grow to like spending time with their families instead of commuting. The companies will find it hard to justify vast floor space at prestigious addresses.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Putting-out_system#Cottage_industry"&gt;Cottage industry&lt;/a&gt; has some downsides, of course. The biggest danger with working from home is over-work (not slacking off as most managers imagine), so employees will need to learn to keep a strict separation between home and work life, without the benefit of time and distance.&lt;/p&gt;
&lt;p&gt;Also, ergonomic chairs that you can sit in for five or six hours at a time without killing your back cost a freaking fortune.&lt;/p&gt;
&lt;p&gt;A slightly less obvious side effect of this will, I hope, be a resurgence of local high streets. All these people working at home will eventually be allowed outside again. They will be looking for things to do with that not-commuting time. They&amp;rsquo;ll want to work and hang out in coffee shops, for a change of pace from the months of solitary confinement. They&amp;rsquo;ll want to eat or buy lunch, and take yoga classes. They&amp;rsquo;ll want a drink with friends who they haven&amp;rsquo;t seen in person since the before-time. And they&amp;rsquo;ll want to walk to these places, like they used to do at the office. Our local high streets, which have been dying for years, will get a new boost from a more dispersed workforce &amp;ndash; knowledge workers on break.&lt;/p&gt;
&lt;h3 id="priorities"&gt;Priorities&lt;/h3&gt;
&lt;p&gt;Finally, how about a long overdue big shake-up of what patriotism means? To be patriotic for the last century or two has often been tied up with a near worship of the armed forces &amp;ndash; all those marches, drumming and wreath laying. But if this pandemic has shown us anything it&amp;rsquo;s that our health services are as important to our security &amp;ndash; and, really, much more so &amp;ndash; than even the military. We&amp;rsquo;re seeing men and women putting their lives on the line in an entirely different way. Risking death to hold someone&amp;rsquo;s hand so that they won&amp;rsquo;t die alone. They are the new heroes.&lt;/p&gt;
&lt;p&gt;I don&amp;rsquo;t necessarily think we need a medical &lt;a href="https://en.wikipedia.org/wiki/The_Cenotaph"&gt;Cenotaph&lt;/a&gt; (although maybe we should), just recognition and financial support. How many ICU beds can we build and staff if we build one less aircraft carrier? How many medical school placements equal one Trident nuclear submarine?&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s my go. &lt;a href="https://twitter.com/EamonnSullivan2/status/1244203737708736512"&gt;Your turn&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>A useful tweak</title><link>https://eamonnsullivan.co.uk/posts-output/tweaking-configuration/2020-03-23-tweaking-configuration/</link><pubDate>Mon, 23 Mar 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/tweaking-configuration/2020-03-23-tweaking-configuration/</guid><description>&lt;p&gt;On this site, I want URLs like &lt;code&gt;https://my-site.com/my-page/&lt;/code&gt; instead of &lt;code&gt;https://my-site.com/my-page.html&lt;/code&gt;. It&amp;rsquo;s a subtle difference, but the addresses just look a bit neater, a bit more professional, less late-1990s without a filename at the end.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://cryogenweb.org/"&gt;Cryogen&lt;/a&gt;, which I use to generate this site, can do this and does this by default. I had been overriding this behaviour because it wasn&amp;rsquo;t working with Amazon&amp;rsquo;s S3 service, but apparently I was just &lt;a href="https://stackoverflow.com/questions/49082709/redirect-to-index-html-for-s3-subfolder"&gt;doing it wrong&lt;/a&gt; and Amazon&amp;rsquo;s S3 should be able to handle URLs like this.&lt;/p&gt;
&lt;p&gt;My issue now was that I had already deployed the site and had to fix it on production. Here&amp;rsquo;s how I did it, albeit with a few minutes of downtime:&lt;/p&gt;
&lt;h2 id="get-the-correct-endpoint"&gt;Get the correct endpoint&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/tweaking-configuration/endpoint.png" alt="Grab the endpoint from the S3 bucket"&gt;&lt;/p&gt;
&lt;p&gt;I copied the entire endpoint (including the &amp;ldquo;http://&amp;rdquo;) from the properties of the bucket in S3 where my content is stored.&lt;/p&gt;
&lt;h2 id="edit-the-origin"&gt;Edit the origin&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/tweaking-configuration/origin2.png" alt="Edit the origin in CloudFront"&gt;&lt;/p&gt;
&lt;p&gt;In CloudFront, find the distribution for the static website, select it and click &amp;ldquo;Distribution Settings&amp;rdquo;, then &amp;ldquo;Origins and Origin Groups&amp;rdquo; and paste in the URL you copied from the endpoint into &amp;ldquo;Origin Domain Name&amp;rdquo;, then save it.&lt;/p&gt;
&lt;h2 id="change-the-sites-configuration-and-publishing-script"&gt;Change the site&amp;rsquo;s configuration and publishing script&lt;/h2&gt;
&lt;p&gt;The next couple of steps are a bit tricky, because we&amp;rsquo;re going to change the way the site is generated pretty radically, push those changes and then invalidate the CloudFront distribution (removing all of the files and directories from the global cache). This will mean that the site is broken for a few minutes (between the push and invalidate steps). But, in my case, that is fine. I think I&amp;rsquo;m the only reader of this site! There probably is a better way.&lt;/p&gt;
&lt;p&gt;The configuration change in Cryogen was to change the &lt;code&gt;:clean-urls&lt;/code&gt; setting to &lt;code&gt;:trailing-slash&lt;/code&gt; from &lt;code&gt;:dirty&lt;/code&gt;, which was the default that I overrode at the beginning.&lt;/p&gt;
&lt;p&gt;Also, in my &lt;code&gt;.github/workflows/publish.yml&lt;/code&gt; file, I needed to add an argument to the &lt;a href="https://github.com/jakejarvis/s3-sync-action"&gt;S3 sync job&lt;/a&gt;: &lt;code&gt;--acl public-read&lt;/code&gt;. This is actually a recommended setting, but I didn&amp;rsquo;t need it previously. With this new configuration, where it creates a sub-directory for each post and page, it became necessary.&lt;/p&gt;
&lt;p&gt;When I commit these two changes, and push to Github, the site is generated automatically and synced to S3.&lt;/p&gt;
&lt;p&gt;And, at this point, the site was badly broken to anyone looking at it. The front page was OK, but none of the links worked. The cache didn&amp;rsquo;t match up at all with reality.&lt;/p&gt;
&lt;h2 id="invalidate-the-cloudfront-distribution"&gt;Invalidate the CloudFront distribution&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://eamonnsullivan.co.uk/posts/tweaking-configuration/invalidate.png" alt="Invalidate CloudFront distribution"&gt;&lt;/p&gt;
&lt;p&gt;To fix that, I needed to clear the cache in CloudFront.&lt;/p&gt;
&lt;p&gt;That was it. After a few minutes (this is still a small site), the distribution was cleared and the site began to work with better, cleaner URLs.&lt;/p&gt;</description></item><item><title>Reasons to be cheerful</title><link>https://eamonnsullivan.co.uk/posts-output/2020-03-22-reasons-to-be-cheerful/</link><pubDate>Sun, 22 Mar 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2020-03-22-reasons-to-be-cheerful/</guid><description>&lt;p&gt;&lt;strong&gt;Reason #1&lt;/strong&gt;: My immediate family (my kids, my sisters and my parents) have been using videoconferencing to keep in contact regularly. I think I&amp;rsquo;ve seen some of my kids &lt;em&gt;more&lt;/em&gt; often because of this. A new tool we&amp;rsquo;re using is &lt;a href="https://zoom.us/"&gt;Zoom&lt;/a&gt;. It allows several of us to have a virtual get-together. I use Zoom at work, which is where I first heard of it, but hadn&amp;rsquo;t realised how useful the free version is. The free version limits meetings to 40 minutes, after which you just need to create another meeting. But if you know someone (i.e., me) with a paid account, you can have them host the meetings and go over that limit. It&amp;rsquo;s easy enough to set up that I might even introduce my parents to the idea, gently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reason #2&lt;/strong&gt;: The food bank I belong to has had to curtail a lot of its activities, such as serving tea and coffee, but we&amp;rsquo;re still open. We were quite worried about yesterday&amp;rsquo;s session &amp;ndash; the first we&amp;rsquo;ve held since the near lock-down conditions have been imposed on London. We expected large, possibly aggressive crowds because the local supermarket shelves are empty. But that didn&amp;rsquo;t happen at all. We were busy, unsurprisingly, but everyone was lovely and helpful. I&amp;rsquo;m going to miss sitting down and having good conversations with many of our visitors, but we can still exchange a few kind words and we&amp;rsquo;re staying open.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reason #3&lt;/strong&gt;: I thought &lt;a href="https://twitter.com/Greg_Callus/status/1241507154030473216?s=20"&gt;this tweet thread&lt;/a&gt; was uplifting. It turns out business at supermarkets are only up about 10% or so over the normal. The businesses are so efficient in just-in-time delivery and stock, that this is causing quite a bit of mayhem. People really aren&amp;rsquo;t hoarding. They&amp;rsquo;re just working from home and going out less, buying a bit more each week. It isn&amp;rsquo;t just mobs of idiots. The supermarkets will adjust. It &lt;em&gt;will&lt;/em&gt; be OK.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reason #4&lt;/strong&gt;: I&amp;rsquo;m getting stuff done. Last week was busy at work, but we managed to shift a lot of code. The long evenings at home is also helping finally dig into things I&amp;rsquo;ve been meaning to do, like creating this web site, and the &lt;a href="https://www.svpsouthruislip.org.uk/"&gt;charity one&lt;/a&gt; I&amp;rsquo;ve promised for years. Yeah, it&amp;rsquo;s a bit harder doing everything over videoconferencing, but it&amp;rsquo;s not &lt;em&gt;that&lt;/em&gt; much harder. We&amp;rsquo;re getting good at this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reason #5&lt;/strong&gt;: The weather is improving. It&amp;rsquo;s beautiful out today and it&amp;rsquo;s forecast to get even better next week. I even mowed the lawn today, for the first time this year. The apocalypse will be a lot more enjoyable if the sun is shining.&lt;/p&gt;</description></item><item><title>It's getting real</title><link>https://eamonnsullivan.co.uk/posts-output/2020-03-19-getting-real/</link><pubDate>Thu, 19 Mar 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2020-03-19-getting-real/</guid><description>&lt;p&gt;If I had to put a word on what I&amp;rsquo;m feeling about all of this, it would be &amp;ldquo;grief.&amp;rdquo; In a matter of a few short weeks, just about everything we were looking forward to is gone. Holidays, family get-togethers, celebrations &amp;ndash; all put on hold, as everything we once thought stable becomes uncertain. So many people will lose their jobs, their businesses, and even their lives.&lt;/p&gt;
&lt;p&gt;I see people going through the &lt;a href="https://grief.com/the-five-stages-of-grief/"&gt;stages&lt;/a&gt;: The roads in my neighbourhood have been full of the hard-core deniers, off to work as normal. When the schools close next week, even those last hold-outs won&amp;rsquo;t be able to ignore the pandemic any longer. The next stage is anger. We&amp;rsquo;re beginning to see that already as people queue for hours for a bag of dried pasta. Bargaining? What is meant by &amp;ldquo;isolation&amp;rdquo;, exactly? How far can I push that?&lt;/p&gt;
&lt;p&gt;Depression isn&amp;rsquo;t far behind. The enthusiasm among my colleagues is waning. Day after day of constant video conferences is getting old. Even introverted software developers realise they can&amp;rsquo;t live on Internet alone.&lt;/p&gt;
&lt;p&gt;What will acceptance look like? I don&amp;rsquo;t doubt that we will get to that last stage. We will learn to cope, but how? What will we learn during this period of forced separation? How will this change our societies, economies or neighbourhoods? We will soon see.&lt;/p&gt;</description></item><item><title>Home Sweet Work</title><link>https://eamonnsullivan.co.uk/posts-output/2020-03-16-home-sweet-work/</link><pubDate>Mon, 16 Mar 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2020-03-16-home-sweet-work/</guid><description>&lt;p&gt;As of today, I&amp;rsquo;ve joined the hordes told to work from home, for an indefinite and probably lengthy period. It felt weird walking out of the office this evening. I may not see my colleagues in person again for weeks or months, people I&amp;rsquo;ve spent the bulk of my waking hours with for the last four years.&lt;/p&gt;
&lt;p&gt;We are living through an extraordinary, historic time. This compares with 9/11 in momentousness, but is unfolding in slow motion, as &lt;a href="https://www.brainyquote.com/quotes/karl_marx_382655"&gt;farce&lt;/a&gt;. Imagine if Coronavirus was as deadly as Ebola? We&amp;rsquo;ve proven ourselves unready. I hope we learn something.&lt;/p&gt;
&lt;p&gt;So, a few resolutions to keep me sane over the next several weeks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;My home office is &amp;ldquo;work&amp;rdquo;. Outside of my home office is &amp;ldquo;home&amp;rdquo;. The two shall be kept separate.&lt;/li&gt;
&lt;li&gt;I&amp;rsquo;ll get up, shower, run and otherwise prepare for work like I&amp;rsquo;m going to the office. No working in my PJs.&lt;/li&gt;
&lt;li&gt;Get as much social time as feasible, in whatever way possible. We&amp;rsquo;re going to make liberal use of Facetime, WhatsApp, Slack and Zoom. This is what social media is really made for, not just something to stare mindlessly at while standing in queues.&lt;/li&gt;
&lt;li&gt;I&amp;rsquo;m not going to pull up the drawbridge. I&amp;rsquo;ll be at the (already very busy) food bank each Saturday, and checking in on the neighbours, as long as I&amp;rsquo;m healthy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keep calm. Carry on.&lt;/p&gt;</description></item><item><title>Well, that was almost too easy.</title><link>https://eamonnsullivan.co.uk/posts-output/2020-03-12-well-that-was-easy/</link><pubDate>Thu, 12 Mar 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2020-03-12-well-that-was-easy/</guid><description>&lt;img src="https://eamonnsullivan.co.uk/img/github_actions.png" hspace="20" vspace="10" align="right"/&gt;
&lt;p&gt;As described in my &lt;a href="https://eamonnsullivan.co.uk/posts-output/2020-03-10-creating-this-site/"&gt;previous post&lt;/a&gt;, I wanted to automate publishing to this site via &lt;a href="https://github.com/features/actions"&gt;Github Actions&lt;/a&gt;. As a Jenkins user at work, I expected this to take some time, requiring reading reams of documentation and writing fiddly scripts that don&amp;rsquo;t work the first 56 times you try them.&lt;/p&gt;
&lt;p&gt;In the end it took me about 30 minutes, and hardly any RTFMing.&lt;/p&gt;
&lt;h2 id="publishing-workflow"&gt;Publishing workflow&lt;/h2&gt;
&lt;p&gt;The process I wanted to automate is basically two steps, but it requires a bit of setup. I have to have a Java environment and a Clojure build tool called &lt;a href="https://leiningen.org/"&gt;Leiningen&lt;/a&gt;. I also need the Amazon Web Service command-line interface and some environmental variables (the S3 bucket to use, the AWS access key and secret, etc.).&lt;/p&gt;
&lt;h3 id="setup-secrets"&gt;Setup secrets&lt;/h3&gt;
&lt;p&gt;The first step I did was create a new AWS user, with limited access, to do the uploading. I create a user that is for script-use only (it can&amp;rsquo;t log into the console). It also needed access to my S3 bucket, but nothing else.&lt;/p&gt;
&lt;p&gt;I then generated a key and secret for this user, and stored that on my repository using the Secrets part of the repository&amp;rsquo;s settings.&lt;/p&gt;
&lt;h3 id="investigate-existing-workflows"&gt;Investigate existing workflows&lt;/h3&gt;
&lt;p&gt;Github has a &lt;a href="https://github.com/marketplace?type=actions"&gt;marketplace&lt;/a&gt; of Actions. These are actions that people have put together and shared publicly. In addition to the standard Github ones (for checking out master and setting up Java) needed one for installing and configuring Leiningen and another for syncing my generated pages with the S3 bucket.&lt;/p&gt;
&lt;p&gt;For syncing with S3, &lt;a href="https://github.com/marketplace/actions/s3-sync"&gt;S3 Sync&lt;/a&gt; comes highly rated. For setting up my Clojure tools, I used &lt;a href="https://github.com/marketplace/actions/setup-clojure"&gt;Setup Clojure&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="combine-everything-in-a-workflow"&gt;Combine everything in a workflow&lt;/h3&gt;
&lt;p&gt;The final step is to create a new directory and file in my repository: &lt;code&gt;.github/workflows/publish.yml&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The first couple of attempts had mistakes in it, but fortunately, I did the work on a branch and then created a pull request. Github will automatically syntax-check any workflows in the pull request. The first time, I mixed the &lt;code&gt;uses&lt;/code&gt; and &lt;code&gt;runs&lt;/code&gt; keys in the same steps, which isn&amp;rsquo;t allowed. The second time I tried it, it caught a simple syntax error (a missing full colon).&lt;/p&gt;
&lt;p&gt;Once the check passed, I merged and then fixed a typo to check if it worked. And it did. The first time. I&amp;rsquo;m still catching my breath!&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the final script:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Upload Website&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;master&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Checkout Master&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@master&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Prepare Java&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/setup-java@v1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;java-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1.8&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Prepare Clojure&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;DeLaGuardo/setup-clojure@master&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;lein&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2.9.2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Build&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;lein run&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;publish&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;jakejarvis/s3-sync-action@master&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--&lt;span class="l"&gt;follow-symlinks --delete&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;AWS_S3_BUCKET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.AWS_S3_BUCKET }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;AWS_REGION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;eu-west-1&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;SOURCE_DIR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;public&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice that the only part that needed to be explicitly &lt;code&gt;run&lt;/code&gt; was the lein build command (&lt;code&gt;lein run&lt;/code&gt;), everything else just happens. And the steps are nice and clear. The labels I gave to the steps (&lt;code&gt;name:...&lt;/code&gt;) show up in the actions, and I can drill down into each step and see the logs.&lt;/p&gt;
&lt;p&gt;Overall, this was very easy and impressive. The site-generation and the continuous integration pipeline is beginning to look like a usable MVP (minimum viable product) for the charity site.&lt;/p&gt;</description></item><item><title>Creating this site</title><link>https://eamonnsullivan.co.uk/posts-output/2020-03-10-creating-this-site/</link><pubDate>Tue, 10 Mar 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2020-03-10-creating-this-site/</guid><description>&lt;p&gt;I&amp;rsquo;ve had this domain for a few years, letting Amazon&amp;rsquo;s Route 53 auto-renew it each year without actually doing anything with it. I&amp;rsquo;ve made a few starts, but hubris would always get the better of me. I&amp;rsquo;d start writing something overly complicated like, say, a single-page app in React (or whatever the framework &lt;em&gt;du jour&lt;/em&gt; was at the time). I kept rediscovering the obvious: I&amp;rsquo;m too busy, and I&amp;rsquo;m rubbish at estimating what I can accomplish in my free time. I&amp;rsquo;d get 80 percent there, life would crowd in, the work would languish until some other shiny new technology caught my eye and I&amp;rsquo;d start again. A cycle of failure.&lt;/p&gt;
&lt;p&gt;When a mostly free Sunday afternoon presented itself last week, I remembered with some chagrin that I had promised one of the charities I work with a web site. I promised this back in 2017. You already know what happened with that (see &amp;ldquo;cycle of failure&amp;rdquo;) and no web site has materialised. So, I really do need to get going on this, and I figured the best way to get started on the charity web site is to practice on one of my own.&lt;/p&gt;
&lt;h2 id="requirements"&gt;Requirements&lt;/h2&gt;
&lt;p&gt;That leads me to my first requirement: It has to be simple. I just don&amp;rsquo;t have the time for complicated. Yes, static web sites are very late 1990s/early 2000s, but I don&amp;rsquo;t really have the need yet for anything more fancy.&lt;/p&gt;
&lt;p&gt;Other requirements: It has to be cheap. I don&amp;rsquo;t want to be spending even tens of pounds a month for something I&amp;rsquo;m likely to stop updating after a while. (Maybe not, we&amp;rsquo;ll see.) I also want the site served from HTTPS with a globally valid SSL certificate because, well, it&amp;rsquo;s the 21st century and browsers are starting to get bitchy about &amp;ldquo;not secure&amp;rdquo; web sites. Finally, I&amp;rsquo;d like the site to be responsive and be usable from a small iPhone screen &amp;ndash; because, again, 21st century and mobile-first is becoming mobile-only.&lt;/p&gt;
&lt;p&gt;Note a missing requirement: It doesn&amp;rsquo;t have to look super spiffy. If I get the urge, I might tinker with the CSS, but if the pages are legible, and the design doesn&amp;rsquo;t make my eyes bleed, then that&amp;rsquo;s good enough for now.&lt;/p&gt;
&lt;p&gt;All of these requirements apply equally well to the charity site. If this works, I&amp;rsquo;ll have a pattern to follow.&lt;/p&gt;
&lt;h2 id="cryogen"&gt;Cryogen&lt;/h2&gt;
&lt;p&gt;I had recently bookmarked &lt;a href="https://cryogenweb.org/"&gt;Cryogen&lt;/a&gt;, a static website generator, as something to investigate. It creates a very simple blog site with posts (like this one) and pages (like &lt;a href="https://eamonnsullivan.co.uk/pages-output/about/"&gt;About Me&lt;/a&gt;). By default, pages and posts are written in markdown and then compiled into HTML that can just be uploaded directly. It&amp;rsquo;s also written in Clojure, a language that I&amp;rsquo;ve been focusing on lately, so that was another plus.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/cryogen-project/cryogen/blob/master/README.md"&gt;Readme&lt;/a&gt; explains how to get up and running. Basically, all I did was delete the sample pages and posts and write a couple of my own.&lt;/p&gt;
&lt;p&gt;However, as I discovered a bit later, I also needed to make some &lt;a href="https://github.com/cryogen-project/cryogen/issues/179"&gt;configuration adjustments&lt;/a&gt; due to my chosen deployment platform: Amazon Web Services. The configuration you need still isn&amp;rsquo;t documented, so I really should put in a pull request. I&amp;rsquo;ll add that to the to-do pile.&lt;/p&gt;
&lt;h3 id="steps"&gt;Steps&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Create a repository on Github for the site.&lt;/li&gt;
&lt;li&gt;Use the leiningen Cryogen template to generate the site skeleton (&lt;code&gt;lein new cryogen eamonnsullivan.co.uk&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Clear out the sample pages and write an &amp;ldquo;about&amp;rdquo; page, an &lt;a href="https://eamonnsullivan.co.uk/posts-output/2020-03-08-intro/"&gt;initial post&lt;/a&gt; and edit the configuration.&lt;/li&gt;
&lt;li&gt;Run the site locally (&lt;code&gt;lein ring server&lt;/code&gt;) and verify that it all works.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="deployment"&gt;Deployment&lt;/h2&gt;
&lt;p&gt;Amazon Web Services has a &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html"&gt;good guide&lt;/a&gt; for getting a static web site installed in S3. I followed that and ended up (once I had configured Cryogen to generate all of its files at the top level) with a public bucket on S3 that&amp;rsquo;s accessible via HTTP.&lt;/p&gt;
&lt;p&gt;I then followed &lt;a href="https://medium.com/@channaly/how-to-host-static-website-with-https-using-amazon-s3-251434490c59"&gt;this article on Medium&lt;/a&gt; to configure Cloudfront (a global caching layer) and an SSL certificate. The final bit of this document describes how to configure the bucket in S3 to only allow access via Cloudfront.&lt;/p&gt;
&lt;p&gt;Once that was all configured, I can publish changes to the web site using the AWS command-line interface, like so (assuming I&amp;rsquo;ve nicknamed my account as &amp;ldquo;home&amp;rdquo; in AWS&amp;rsquo;s configuration):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;lein run
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;aws s3 cp ./public/ s3://eamonnsullivan.co.uk/ --recursive --profile home
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="next-steps"&gt;Next steps&lt;/h2&gt;
&lt;p&gt;My next challenge is to automate updates as much as possible. I&amp;rsquo;ve been meaning to learn &lt;a href="https://github.com/features/actions"&gt;Github Actions&lt;/a&gt; and this is a good opportunity. Ultimately, I&amp;rsquo;d like for the web site to automatically publish each time I merge to master.&lt;/p&gt;
&lt;p&gt;Wish me luck!&lt;/p&gt;</description></item><item><title>Introduction</title><link>https://eamonnsullivan.co.uk/posts-output/2020-03-08-intro/</link><pubDate>Sun, 08 Mar 2020 00:00:00 +0000</pubDate><guid>https://eamonnsullivan.co.uk/posts-output/2020-03-08-intro/</guid><description>&lt;p&gt;This is my own web site. There&amp;rsquo;s not much here yet. I&amp;rsquo;m hoping to document how I get this online.&lt;/p&gt;
&lt;p&gt;The title comes from a prayer I read once. In any given moment, there is one thing necessary for us to do that will brighten the world. We don&amp;rsquo;t need to solve the bigger picture, we just need leave the world and the people around us a little better off, more compassionate, kinder.&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s that one necessary thing right now?&lt;/p&gt;</description></item></channel></rss>