<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
  <atom:link href="https://pdx.su/feed.xml" rel="self" type="application/rss+xml" />
  <title>Jeff Sandberg&#39;s Blog</title>
  <link>https://pdx.su</link>
  <description>The personal blog of Jeff Sandberg, software engineer</description>
  <language>en-us</language>
  <generator>Tableau v0.25.0</generator>
    <item>
       <title>The quiet software tooling renaissance</title>
       <link>https://pdx.su/blog/2025-08-13-the-quiet-software-tooling-renaissance</link>
       <pubDate>Wed, 13 Aug 2025 19:06:34 -06</pubDate>
       <guid>https://pdx.su/blog/2025-08-13-the-quiet-software-tooling-renaissance</guid>
       <description><![CDATA[ <html><head></head><body><section id="The-quiet-software-tooling-renaissance">
<h1>The quiet software tooling renaissance</h1>
<p>A quiet renaissance has been happening in software tooling. New projects are cropping up that do things <em>far</em> better than the tools they replace, and in many ways, they carry the ideals of older unix tools into the 21st century. Newer tools typically discard decades of baggage, embrace modern standards, and ultimately <em>just work better</em>. And the best part about most of them is that you can start using them <em>now</em>, without having to get your team or coworkers or anyone else on-board, as they interoperate extremely well with older tools.</p>
<p>Recently, I took a week off for a staycation. During that week, I spent some time looking at updating the ways I did some things. Some personal projects got updates yes, but most of that was just a vehicle to do something new with new tools. Some of those tools are tools I’ve been using for a while now, but not to their fullest potential. Other tools are things I tried in the past, saw the potential, but ultimately had to stop using due to their immaturity at the time of testing. I think that, from time to time, everyone needs a week to themselves to hone their skills in something that they might not have the time to use during their normal work.</p>
<section id="Mise">
<h2>Mise</h2>
<p>I’ve been using <a href="https://mise.jdx.dev">Mise</a> for a while now. I was actually using it when it was called rtx. On the “tin,” mise is <em>just</em> another tool version manager, in the vein of generic ones like <a href="https://asdf-vm.com">asdf</a> and language-specific ones like <a href="https://github.com/rbenv/rbenv">rbenv</a>. And you can use mise as just a tool version manager. Its very good at that. But its got way more features than just version management.</p>
<p>First, Mise lets you install <em>far more</em> than the usual language runtimes that are what you’d typically think of. I’m using it to manage a few different binaries that come from rust packages, as cargo binaries. Sure, I could use cargo-binstall (which is what mise wraps) to do most of that, but keeping them in my user level mise configuration means that, regardless of rust version, I’ve got those tools installed, and managed in a centralized place.</p>
<section id="Tasks">
<h3>Tasks</h3>
<p>Second, and this is where the power really starts to shine through with mise, is its tasks system. <a href="https://mise.jdx.dev/tasks/">The documentation on tasks is extensive</a>, but the tl;dr is that they are a convenient way to provide common scripts and other, well, tasks, that you’d need in software development. Sure, other tools have been around for ages that cover this as well. There’s the venerable makefile, rake, node and elixir have supported tasks for their entire existence, and then there are modern dedicated task tools like just, and if all that fails, just writing a quick shell script can usually get the job done. But mise steps things up a bit in two very important ways. First, tasks have metadata associated with them, so you can compose them together. Say you have a build task, and want an install task. You can make the install task <em>require</em>the build task to run before it runs, so it will install new versions of a binary. You can also tag the files, both input and output, that a mise task will interact with, so you can do things like have tasks noop if any of their source files haven’t changed. And tasks can either be encoded as short little scripts in your project or global mise configuration, <em>or</em> you can just take the various shell scripts some projects have lying about a <code>bin/</code> directory, and turn them into mise tasks. And when you make a shell script a mise task, you get excellent arg parsing for free, via the <code>usage</code> tool.</p>
</section>
<section id="Usage">
<h3>Usage</h3>
<p>Its not really fair to call <a href="https://usage.jdx.dev">usage</a> a subset of mise, as its got plenty of utility on its own as a standard way of encoding CLI options, and as a parsing library for said options. I’m shoving it as a sub-section of mise because you can still use usage based arguments in mise tasks without having usage installed.</p>
<p>The <a href="https://mise.jdx.dev/tasks/file-tasks.html#arguments">mise file task documentation</a> has a rather good example of how a <code>usage</code> configuration looks in a task, but suffice to say it sure beats having to muck about with optparse, or even “better” parsers like fish shell’s <code>argparse</code>. You can get flags, enum options, arguments, and even subcommands, without having to go too far out of your way. Since a lot of portable scripts for projects like this just target <code>sh</code>, which isn’t terribly pleasant to write, having <code>usage</code> handle a lot of the heavy lifting can dramatically simplify things.</p>
</section>
<section id="Envars">
<h3>Envars</h3>
<p>Finally, mise has an envar management system, that works extremely similarly to how tools like <a href="https://direnv.net">direnv</a> work. You define your environment variables, and when you enter the part of the directory tree where those are configured, those are set, and when you exit, they’re reverted.</p>
<p>But mise has a few more tricks going for it. First, you don’t have to ship a <code>.envrc</code> sidecar. You can configure your envars in your mise config file, which reduces the number of configuration files you have floating around. If you already have an envrc, and don’t want to port it over to mise, you can actually include envrc style files, easily, into your mise config. Some of the more advanced direnv directives don’t work with mise, but I haven’t seen much use of them.</p>
<p>Since mise env configurations live in your mise configs, you get all the benefits mise configs give you, like easy nesting, environments, local-only configs, etc, as well as some that are <em>just</em> for envars, like secrets.</p>
</section>
</section>
<section id="Jujutsu">
<h2>Jujutsu</h2>
<p>I like git. I like to think I’m rather good at git as well. It’s been a long time since I got myself into a git hole that I couldn’t get myself out of. I’m often the guy friends and coworkers will ask to un-break their git repos or config. I do things like set up automatic git trailers for my commits, based on the branch they’re added to. I have custom scripts built atop git that do repetitive things.</p>
<p><a href="https://jj-vcs.github.io/jj/latest/">Jujutsu</a> aims is to be a modern VCS, solving some of the issues that previous VCSes grappled with, while building atop the good (and hopefully leaving behind the bad) other VCSes have brought. It adopts a committing and branching model more akin to what you’d find in Mercurial than git, which takes a bit of getting used to, but ultimately proves to be a more powerful approach. At the same time, it builds atop git’s speed, and the relative ease of collaboration with a tool like git.</p>
<p>In jujutsu, unlike git, there <em>is no index and staging area</em>. Every change you make to any file that can be tracked, <em>is tracked</em>. This is probably the biggest thing to get used to, coming from git, but in terms of changes to development workflow, its actually an easy change to make. A pattern I’ve long followed was to work on a <em>bunch</em> of changes in git, making commits whenever I felt like it, then when whatever I was working on was in a state that needed to be made more widely available (pull request, pushed up to remote, etc), I’d rebase the whole set of changes down, and split it up into <em>logical</em> chunks, based either on what files were changed, or what functional change I was making. You can copy this workflow <em>very easily</em> in jujutsu. I’m going to use git terms here, but remember, <em>jujutsu has no index, no stash, none of those crutches git relies on</em>. Working copy, in this example, is <em>just</em> the head commit.</p>
<p>To take changes out of the current index and put them into commits, you can use the <code>jj split</code> command. If you just want to move a whole file into a new commit, you just run <code>jj split file.txt</code>, and a commit edit dialog will pop up, allowing you to describe the commit for that file. But if you want to do patch-level changes, for splitting up a change into logical commits, rather than file-based commits, jujutsu has you covered. Running <code>jj split -i</code> gives you an <em>interactive</em> split tool, that lets you select which chunks, lines, or files to cleave out into the split, and which ones to leave on the head commit. Note that after every split, the files chosen to be split are placed in a commit <em>before</em> the head commit (index), and the head commit now contains everything else. You can change where the split commit is placed, but thats beyond the scope of this example. Since I typically work on a large feature, and then want to recursively split it up into smaller chunks, I wrote a <a href="https://github.com/paradox460/.dotfiles/blob/master/bin/jjx">small fish shell script</a> that recursively invokes <code>jj split -i</code> until there is <em>nothing</em> left in the head commit, or the split command is exited with a non-zero exit status.</p>
<p>Moving the other way is just as easy. If you have a <em>bunch</em> of commits, and you want to combine them into each other, or take files from one commit and move them to another, jujutsu has you covered via the <code>jj squash</code> command. Unlike split, it doesn’t create new commits, but only works with existing ones.</p>
<p>You might think you want to use the squash command fairly often, particularly if you’re a heavy user of fixup commits and interactive rebase in git. But in jujutsu, you dont have to. You can take <em>any</em> commit<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a> and edit it, right there. All the downstream changes from your changes will be automatically rebased. And thats kind of the secret sauce of jujutsu. Where git made <em>branches</em> cheap compared to subversion, jujutsu makes <em>rebases</em> cheap, while keeping the cheap branches. In fact, jujutsu doesn’t really use the git branch paradigm at all. Instead, they are called <code>bookmarks</code>, and work more like mercurial bookmarks. Any single change can have many bookmarks, and change histories can weave in and out, with changes that exist as the head of <em>multiple</em> branches at once.</p>
<p>The best part about jujutsu is that you can use it seamlessly with git. Currently the most common way to use jujutsu is with git providing a backend, although there is theoretical support for other backends. jujutsu repos can be initialized on top of existing git repos, and all your jujutsu changes will map nearly perfectly down to plain old git commits, branches, and so forth. Other users of the git repo won’t be able to tell you’re using jujutsu at all, unless you do things like single changes up as their own branch, or suddenly get a lot better at rebasing.<a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a> Support for some parts of git is limited, such as minimal support for subrepos, tags, and virtually no support for extensions like git-lfs, but there is work being done on these. And if you run into any corners where you can’t do something in jujutsu but <em>can</em> do it in git, you can always just <em>do it in git</em>, and jujutsu will pick up the changes and show them to you. Some people see this and assume jujutsu is little more than a git interface, like tower or lazygit. But I’d say that’s an unfair comparison, given jujutsu fundamentally changes how you think about things like changes, branches, and rebases.</p>
<p>And if you ever make a mistake in jujutsu, you have <code>jj undo</code> right there, which immediately reverts your last change, whatever it was, just like in every other modern program.</p>
<p>Learning jujutsu is fairly easy, and it has a decent ecosystem of articles and tooling around it. Here’s some of my favorites:</p>
<ul>
<li>
<a href="https://jj-vcs.github.io/jj/latest/">Jujutsu’s official documentation</a>
</li>
<li>
<a href="https://steveklabnik.github.io/jujutsu-tutorial/">Steve Klabnik’s big tutorial on JJ</a>
</li>
<li>
<a href="https://maddie.wtf/posts/2025-07-21-jujutsu-for-busy-devs">Madeleine Mortensen’s excellent JJ article</a>, which inspired me to try JJ again
</li>
<li>
<a href="https://github.com/idursun/jjui">jjui</a>, an extremely good jujutsu TUI, which is honestly how I mostly interact with it
</li>
</ul>
<p>I actually tried jj a few years back, when it was much newer. I liked it then too, but it’s git interop was still rather shabby, and I quickly got into a few places that I couldn’t easily get out of, and had to fall back down to git. This time around, I’ve only had to pop out to plain git once, and that was to manage some submodules.</p>
</section>
<section id="Pkl">
<h2>Pkl</h2>
<p>The third thing I “learned” during my week off was Apple’s <a href="https://pkl-lang.org/index.html">pkl</a> language. Pkl is a new language for writing configuration files from Apple. It’s got a clean syntax, and the typing system in place makes it possible to easily compose complex configs without getting lost. It’s designed to easily compile down to json, yaml, toml, xml, property lists, or really any other configuration language you care to use.</p>
<p>Like with the other tools mentioned in this article, I wont go over <em>every</em> facet of it; the docs are very good and I suggest reading them. But I will highlight a few of my favorite features.</p>
<p>Pkl, by default, is rather forgiving when it comes to types. If you don’t specify a type of something in it, its more or less dynamically typed, letting maps and objects contain arbitrary key-value pairs. But you can very easily start typing things, creating classes and so forth, which then tighten up your configuration tremendously, and catch dumb fat-finger errors.</p>
<p>Pkl supports late binding, which means you can make any field reference <em>any other field</em> in your document for its value. This significantly reduces the amount of typing you have to engage in, and, again, prevents mistakes. You can also run various built-in functions, or define your own, that accept values. A common one I’ve used in some of my things is the <code>sha1</code> function to generate unique IDs for HomeAssistant entities based off their names.</p>
<p>Finally, Pkl supports arbitrary output drivers. It ships with some useful built-in ones, like a yaml formatter, a json formatter, xml, plist, and more. But you can create your own quite easily. The toml output formatter, for example, isn’t actually a built in, but rather an external package that just ships as a part of the Pkl Pantry, which is a registry of useful and common Pkl packages. Since outputs can be configured as <em>part of the document</em>, or <em>part of a document you’re amending</em>, you don’t have to have complex compilation strings. For a lot of things, <code>pkl eval file.pkl</code> will do the trick.</p>
<p>Pkl has a robust package system. Pkl can load other pkl files locally, or source them from anywhere on the internet. You can easily use other pkl files that set up primitives for things like <a href="https://github.com/StefMa/pkl-gha/">Github Actions</a>, <a href="https://github.com/paradox460/homeassistant-pkl">HomeAssistant configs</a>, <a href="https://github.com/declix/pkl-systemd">SystemD configs</a>, and more.</p>
<p>I’ve actually <a href="https://github.com/paradox460/pdx.su/blob/main/.github/pkl-workflows/ssg.pkl">used pkl to generate the Github Actions that deploy this blog</a>. They may look a bit more verbose than the corresponding output Yaml file, but they’re much easier to reason about, their code is deterministic, and the yaml output will <em>always</em> be valid Yaml. No more failed to parse due to a misplaced tab.</p>
</section>
<section id="Closing">
<h2>Closing</h2>
<p>One interesting thing to note, that someone else brought up in a hackernews comment on Maddie’s JJ article, was that it seems like a lot of the new tools are being written in Rust. Mise, Usage, and JJ are all rust projects, as are a bunch of other tools I use (sd, bat, fd, rg, czkawka, deno, cargo-generate, and more). By now, most people have encountered the “rewrite it in rust” phenomenon, which is where older unix utilities are rewritten in rust, or similar enough utilities to cover the same task are written in rust. Sometimes these tools are <em>very good</em>, significant improvements (in at least ergonomics, if not functionality) over their older counterparts. Other times they’re basically just “old tool but with colorful output”. Is rust the reason we’re seeing so many new, useful tools? Maybe. Its certainly enjoyable to write, although I still prefer Elixir. Or is this more of a cyclic pattern, with the language almost being irrelevant?</p>
<p>20 years ago, Ruby was “taking over.” Many new tools were cropping up, all of them written in ruby and distributed as rubygems. The number of tools that required you to install some gem was surprisingly high. 20 years before that, Perl caused a similar renaissance in tools. A lot of the “ruby renaissance” tools were just rewrites of older Perl tools into ruby. Are rust based tools just the continuation of this pattern? Possibly, but they do have some distinct advantages over their predecessors. Rust tools can be compiled, they don’t require you to install some runtime to use them, and then there’s the whole safety ballyhoo with rust, which doesn’t really matter day-to-day when you’re using one of these tools, but does matter when it can’t crash your system.</p>
<p>Ultimately, there’s a lot of inertia thats driving newer tools, with better ergonomics around <em>nearly everything</em>. Mise configs are a breath of fresh air compared to the somewhat awkward configurations we’d see with tools like Asdf or its language-specific predecessors. JJ commands are clear and to the point, you don’t have to open up a glossary to determine what <code>jj abandon</code> means, nor do you have to ask StackOverflow or some stackoverflow-regurgitating AI on how to use it. sd, fd, and rg all do similar things to their older unix counterparts, but with saner defaults (like starting with regexp, not requiring extra flags to use it) and simpler invocations. Want to find all the jpgs in a directory tree? You can write a somewhat simple find command:</p>
<pre class="athl"><code class="language-bash" translate="no" tabindex="0"><span class="line" data-line="1"><span class="function-call">find</span> <span class="variable-parameter">.</span> <span class="variable-parameter">-type</span> <span class="variable-parameter">f</span> <span class="variable-parameter">-iname</span> <span class="string">&quot;*.jpg&quot;</span> <span class="variable-parameter">-o</span> <span class="variable-parameter">-iname</span> <span class="string">&quot;*.jpeg&quot;</span>
</span></code></pre>
<p>or just</p>
<pre class="athl"><code class="language-bash" translate="no" tabindex="0"><span class="line" data-line="1"><span class="function-call">fd</span> <span class="variable-parameter">jpe?g</span>
</span></code></pre>
<p>Sure, you can make GNU find use regexp patterns, but the point is that with modern tools like <code>fd</code>, <em>you don’t have to</em>.</p>
<p>And thats ultimately the point of this whole “renaissance.” Tools are getting better, either directly or via replacements, and there’s been relatively little fanfare about it.</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>You can really edit any commit, but if you’re working with other people, jj has a sanity check to make sure you don’t edit a commit that you’ve already pushed up accidentally. If a commit is marked “immutable”, which is to say exists on a remote, you have to pass an extra param to any change that will modify it, be it an edit, squash, or rebase.<a href="#fnref1" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn2">
<p>I’ve actually been using jujutsu to manage this blog’s source code, which is hosted on github. <a href="https://github.com/paradox460/pdx.su">See if you can tell by reading the source</a><a href="#fnref2" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</section>
</body></html> ]]></description>
    </item>
    <item>
       <title>Make your repo ergonomic</title>
       <link>https://pdx.su/blog/2025-07-10-make-your-repo-ergonomic</link>
       <pubDate>Thu, 10 Jul 2025 20:11:57 -06</pubDate>
       <guid>https://pdx.su/blog/2025-07-10-make-your-repo-ergonomic</guid>
       <description><![CDATA[ <html><head></head><body><section id="Make-your-repo-ergonomic">
<h1>Make your repo ergonomic</h1>
<p>Throughout my entire career as a software engineer, I’ve had to clone and set up many projects. They have each done different things, and have been in a variety of different languages, built around a variety of different systems. Some are open source and public, others are proprietary and available only through the terms of my employ. One commonality nearly all of them share is that they <em>suck</em> to get set up on a new machine.</p>
<p>Proprietary codebases are typically slightly worse than open source ones, for the simple reason that open source ones treat their ease of contributions as a marketing device, while proprietary codebases, with privileged and limited access, typically gated behind an employment contract of some kind, don’t have to care about that. While its nice that open source projects respect the time of potential contributors more, it seems foolish that private repositories don’t seem to care as much, given they’re typically <em>paying</em> for the “privilege” of having a developer work on it.</p>
<p>I’m going to write most of this article from the perspective of someone using Github, as thats generally the most prevalent, but the ideas I discuss are portable across whatever you use for your source control, be it patches emailed around or some proprietary behemoth.</p>
<section id="How-it-is">
<h2>How it is</h2>
<section id="Your-readme-sucks">
<h3>Your readme sucks</h3>
<p>The first thing most of us will encounter when starting with a repository is the README. Github displays this prominently, and most developers seem to have a feel that the README should be of some minor utility to future developers. Typically you’ll find a section on some dependencies needed for the project, how to get things running, some discussion of architecture, and, if you’re really lucky, some documentation about particular warts of the project, or at least links to such.</p>
<p>But the README is almost always full of false promises. You’ll see things like “Getting started is easy, just clone the repo and run these 5 commands”. But when you do, you’ll find that Homebrew or whatever no longer packages up one particular dependency, or that it doesn’t run on M-series Macs, or some other crap. If you’re at work, you’ll probably go ask in Slack, maybe ask a coworker assigned to help onboard you, and get a snippet that tells you how to get around it. You’ll run the snippet, it will work, and you’ll move on to the next undocumented problem.</p>
<p>Maybe one of your dependencies is getting a copy of the database to run locally. So you’ll have to go find that through some proprietary internal service, download a massive (hopefully anonymized) dump, try to import it, find that your version of PostgreSQL and the one that the company is running are incompatible, try and get the right one installed, you get the picture.</p>
<p>Once you finally get everything working, you’ll be so exhausted from the gauntlet that you won’t have the mental capacity to do any more work. But luckily, your first day is probably finished, so time to go home and forget all the pain. Tomorrow you’ll come in fresh, and actually start working on tickets. But what about the next guy? They’re going to go through the same crap, deal with the same random bullshit, and have to dig out the same answers you did.</p>
<p>All this stems from the fact that the README is text. It’s not code. It’s typically written early in a projects lifetime, and then more or less left alone. Maybe someone will come and update it from time to time, but there’s no guarantee that it will be kept up to date. Developers used to the project won’t reference the README, they’ll either use their shell history or snippets passed around in Slack. The README becomes a snapshot of the project at a particular time in the past.</p>
</section>
<section id="Your-dependency-management-sucks">
<h3>Your dependency management sucks</h3>
<p>Most repos will have dependency management. Typically its going to be confined to the language(s) the repo uses, and managed with a tool specific to the language(s). Thats fine and all, and usually works pretty well. But that only handles libraries and such <em>for that language</em>. What about all the auxiliary crap you need to run your project? Some tools, like Docker, can be assumed to be installed, and generally are pretty stable across versions, and have carve-outs for each developer to choose how to run it (Colima, Orbstack, etc). Other tools are incredibly repo specific, and most of them are left up to the developer to manage their install of. And then you get issues that go something like this:</p>
<blockquote>
<p>My froblar isn’t working anymore</p>
<blockquote>
<p>We upgraded froblar versions last week</p>
</blockquote>
<p>Oh ok I’ll run <code>brew update</code> and get the newer version</p>
</blockquote>
<p>Yeah, its not a big deal to get around, but every time you have to do it, its a waste of time. And it adds up.</p>
<p>I’ve seen some devs try to get around this by using tools <em>in</em> the language. This is likely the cause of a proliferation of things like <code>rake</code>, <code>just</code>, and other build tools. You <em>have</em> to have the language the repo uses installed, so getting tools in it seems like a simple way to manage the dependencies of these tools. It works, but always has felt like a kludge to me. Why am I managing the version of my tools in the exact same place I’m managing the versions of my application dependencies?</p>
<p>There are better ways to manage dependencies.</p>
</section>
<section id="Starting-your-app-and-its-dependencies-shouldnt-require-memorization">
<h3>Starting your app and its dependencies shouldn’t require memorization</h3>
<p>Finally, for some modern systems, particularly those heavy with microservice architecture gobbledygook, you’ll have to have 5 or 6 things running at once, just to develop your app. The ideal solution to this is just have most everything running in Docker or similar containers, so you can shove the apps you don’t care about into the background, and work on the one you do care about presently.</p>
<p>But thats rarely the case. More often than not, you’ll be working on your local codebase, and tying it into some remote system. If your IT department is kind, you’ll have something like Tailscale managing all the VPN stuff for you, so you can just connect to remote systems as if they were just another URL. But if they’re more of the average IT department, you’ll run into the classic problem of having to set up half a dozen port forwards just to talk to the various remote services you don’t want to spend weeks getting running locally.</p>
<p>You’ll find the readme saying things like:</p>
<blockquote>
<ul>
<li>
Ensure Docker with redis is running
</li>
<li>
In one terminal, run <code>kubectl --context &quot;honk-staging&quot; --namespace &quot;honk-core&quot; port-forward honk-instance-0 50001:50000</code>
</li>
<li>
In another terminal, run <code>kubectl --context &quot;blarg-staging&quot; --namespace &quot;blarg-core&quot; port-forward blarg-instance-0 50002:50000</code>
</li>
<li>
In a third, run <code>bin/graphql-router</code>
</li>
<li>
In a fourth, run the application <code>iex -S mix phx.server</code>
</li>
</ul>
</blockquote>
<p>Every time you want to get enough of the application running locally to develop your code, you’ll have to open at least 4 terminals, run 4 separate commands, and hope nothing has broken since the last time you, or the person who wrote them into the readme, ran them.</p>
<p>Maybe you use a tool like <a href="https://atuin.sh">Atuin</a> to manage your shell history, so you have some portability across machines, and getting a new one doesn’t mean losing years worth of “snippets.” But that does no good for new engineers.</p>
</section>
</section>
<section id="How-to-fix-it">
<h2>How to fix it</h2>
<section id="Fixing-your-README">
<h3>Fixing your README</h3>
<p>Fixing the README requires a bit of a cultural change. The README should be treated as a living document. As changes to the application happen, there should be a conscious effort to make sure the readme reflects the current state of things. If a PR changes how something starts, in a way that requires developers to do something different, it should update the README to reflect that. If it doesn’t, people on your team (maybe even you!) should request changes, to have an updated README.</p>
<p>But we can make sure the README doesn’t have to be updated all the time, by taking common processes, like all the code snippets on how to set things up and how to start the app, and pulling them out into scripts or tasks, that can be run via a single command. Then the readme doesn’t have to be updated every time the invocation of the script changes, just the script needs to be updated. If the script is something all your developers use every time they start the app, it will stay updated. More on how to do these scripts in a moment.</p>
<p>For your “set up this app” step, you should first strive to minimize all that the developer has to do. Move slow, unchanging things into scripts or Docker containers. Write a docker compose file, that can be started with just a single <code>docker compose up</code>. You can dramatically improve your dependency management, which will alleviate a massive category of setup pains.</p>
</section>
<section id="Dependency-management">
<h3>Dependency management</h3>
<p>Most teams already use a tool to manage the <em>version</em> of their application’s language. Node, Ruby, and Python all have a bunch of dedicated tools for this, and there are general purpose tools like <a href="https://asdf-vm.com">asdf</a> and <a href="https://mise.jdx.dev">mise</a> that aim to manage many different languages at once. Other teams may use a tool like <code>Nix</code>, which can do everything else I’m going to elaborate on in this section, and more.</p>
<p>If you’re already using a tool like <code>asdf</code>, you’re in a decent enough place. If you’re already using <code>mise</code>, you’re already halfway through the suggestions I’m gonna make. If you’re on <code>asdf</code>, you should move to <code>mise</code>. It’s more or less a drop-in replacement for <code>asdf</code>, and does oh so much more. The rest of this section will be about some <code>mise</code> tricks that I’ve found useful.</p>
<p>Once you’ve got mise running (and I’d add a link to the mise installer to your README), I’d recommend setting the following settings</p>
<pre class="athl"><code class="language-bash" translate="no" tabindex="0"><span class="line" data-line="1"><span class="function-call">mise</span> <span class="variable-parameter">settings</span> <span class="variable-parameter">set</span> <span class="variable-parameter">experimental</span> <span class="variable-parameter">true</span>
</span><span class="line" data-line="2"><span class="function-call">mise</span> <span class="variable-parameter">settings</span> <span class="variable-parameter">set</span> <span class="variable-parameter">lockfile</span> <span class="variable-parameter">true</span>
</span><span class="line" data-line="3"><span class="function-call">mise</span> <span class="variable-parameter">touch</span> <span class="variable-parameter">mise.lock</span>
</span></code></pre>
<p>In short, these settings enable experimental features, which lets you install more tools than the base does, and sets up lockfiles, so you can use looser version specifiers for languages and tools, while still maintaining the same “version” across machines.</p>
<p>Now, there are probably a few tools that you’ve just told devs to install. Tools like <code>rover</code> or <code>docker</code>. You can probably get away with leaving these as system tools, particularly if they’re stable, like Docker, but for more ephemeral tools, or tools that don’t really fit your application architecture but are useful (think prettier for your CSS, markdownlint, etc), you can add them to mise, and they’ll be versioned.</p>
<p>Mise has a ton of support for various tools, so check with <code>mise search &lt;toolname&gt;</code> first. If there isn’t a tool listed there, you can install nearly anything that can be grabbed off the internet in an architecture agnostic way via mise’s support for the <a href="https://github.com/houseabsolute/ubi">universal binary installer</a>. You just add these to your mise config via <code>mise use ubi:user/repo@version</code>, and you’re off to the races. If that doesn’t work for you, the <a href="https://mise.jdx.dev/dev-tools/backends/ubi.html">mise documentation on this feature</a>, which might solve your use case.</p>
<p>For the tools that are more general purpose tools, like docker, I’d still recommend adding a <code>brewfile</code> to your repo. This lets new devs install global dependencies they may not have by simply running <code>brew bundle</code>. This has some overlap with mise, particularly for tool management, but Homebrew in general doesn’t keep around older versions of tools (barring things like <code>postgres@14</code>), so its only good if the version of each tools is largely irrelevant. Use your best judgement</p>
</section>
<section id="Make-your-scripts-into-tasks">
<h3>Make your scripts into tasks</h3>
<p>Mise also has a very robust <a href="https://mise.jdx.dev/tasks/">tasks system</a>. You can define tasks either in your <code>mise.toml</code>, or as scripts in <code>.mise/tasks</code>. I generally prefer the latter, as they’re a bit easier to write, port older scripts into, and if you’re in a circumstance where you don’t have mise, depending on how you write your tasks, you can still run them same as any other shell script.</p>
<p>These tasks are very useful for doing things like setting up your application, linting, compiling, releasing, etc. Look at commands you have to run all the time, and try and port them into a task. If you need options parsing, <a href="https://mise.jdx.dev/tasks/file-tasks.html#arguments">mise has you covered</a>, no need to use <code>getopts</code>.</p>
<p>You can even specify task dependencies, so you can make a release task depend on a build task.</p>
<p>For our “start these 4 programs across multiple shells” step, I’d recommend using a task, coupled with <code>tmux</code>, to start up all the programs you need, in a single command. Here’s a simple example of this:</p>
<pre class="athl"><code class="language-bash" translate="no" tabindex="0"><span class="line" data-line="1"><span class="keyword-directive">#!/usr/bin/env bash</span>
</span><span class="line" data-line="2"><span class="comment">#MISE description=&quot;Runs everything locally, in a Tmux session named MyApp. To kill a running session, run this command again&quot;</span>
</span><span class="line" data-line="3"><span class="comment">#USAGE flag &quot;-w --window&quot; help=&quot;Attempts to use iTerm2&#39;s tmux integration, opening in real windows instead just a tmux pane&quot; default=&quot;false&quot;</span>
</span><span class="line" data-line="4">
</span><span class="line" data-line="5"><span class="function-builtin">set</span> <span class="variable-parameter">-Eeuo</span> <span class="variable-parameter">pipefail</span>
</span><span class="line" data-line="6">
</span><span class="line" data-line="7"><span class="comment"># Check if we&#39;re already running the tmux session, if so, kill it</span>
</span><span class="line" data-line="8"><span class="function-call">tmux</span> <span class="variable-parameter">has-session</span> <span class="variable-parameter">-t</span> <span class="variable-parameter">MyApp</span> <span class="operator">&amp;&gt;</span><span class="string-special-path">/dev/null</span> <span class="operator">&amp;&amp;</span> <span class="function-call">tmux</span> <span class="variable-parameter">kill-session</span> <span class="variable-parameter">-t</span> <span class="variable-parameter">MyApp</span> <span class="operator">&amp;&amp;</span> <span class="function-builtin">exit</span> <span class="number">0</span>
</span><span class="line" data-line="9">
</span><span class="line" data-line="10"><span class="variable">tmux_config</span><span class="operator">=</span><span class="punctuation-special">$(</span>
</span><span class="line" data-line="11">  <span class="function-call">cat</span> <span class="operator">&lt;&lt;</span><span class="label">HERE</span>
</span><span class="line" data-line="12"><span class="string">new -s MyApp -n &quot;DB Port Forward&quot; mise run port_forward_db</span>
</span><span class="line" data-line="13"><span class="string">neww -n &quot;Search Port Foward&quot; mise run port_forward_search</span>
</span><span class="line" data-line="14"><span class="string">neww -n Apollo mise run apollo</span>
</span><span class="line" data-line="15"><span class="string">neww -n Phoenix</span>
</span><span class="line" data-line="16"><span class="string">send-keys -t Phoenix &quot;iex -S mix phx.server&quot; Enter</span>
</span><span class="line" data-line="17"><span class="string"></span><span class="label">HERE</span>
</span><span class="line" data-line="18"><span class="punctuation-special">)</span>
</span><span class="line" data-line="19">
</span><span class="line" data-line="20"><span class="keyword-conditional">if</span> <span class="punctuation-bracket">[[</span> <span class="string">&quot;<span class="punctuation-special">$</span><span class="variable">usage_window</span>&quot;</span> <span class="operator">==</span> <span class="string">&quot;true&quot;</span> <span class="punctuation-bracket">]]</span> <span class="operator">&amp;&amp;</span> <span class="punctuation-bracket">[[</span> <span class="punctuation-special">$</span><span class="constant">TERM_PROGRAM</span> <span class="operator">==</span> <span class="string">&quot;iTerm.app&quot;</span> <span class="punctuation-bracket">]]</span><span class="punctuation-delimiter">;</span> <span class="keyword-conditional">then</span>
</span><span class="line" data-line="21">  <span class="variable">tmux_options</span><span class="operator">=</span><span class="string">&quot;-CC&quot;</span>
</span><span class="line" data-line="22"><span class="keyword-conditional">else</span>
</span><span class="line" data-line="23">  <span class="variable">tmux_options</span><span class="operator">=</span><span class="string">&quot;&quot;</span>
</span><span class="line" data-line="24"><span class="keyword-conditional">fi</span>
</span><span class="line" data-line="25">
</span><span class="line" data-line="26"><span class="function-call">tmux</span> <span class="punctuation-special">$</span><span class="variable">tmux_options</span> <span class="variable-parameter">-f</span> <span class="punctuation-special">&lt;(</span><span class="function-builtin">echo</span> <span class="string">&quot;<span class="punctuation-special">${</span><span class="variable">tmux_config</span><span class="punctuation-special">}</span>&quot;</span><span class="punctuation-special">)</span> <span class="variable-parameter">attach</span>
</span><span class="line" data-line="27">
</span></code></pre>
<p>This script will start a new tmux session, with 4 windows, each named after their respective service. Each service’s invocation is a separate mise task, to keep the code simple, and allows for you to start each one independently, as you may need to. The last window we open runs our application server, and we invoke it using <code>send-keys</code>, instead of just running it, so we may kill and restart the application server without losing our window. The last convenience item is that, if tmux is already running a session of this, we kill it instead of starting a new one. This acts as a soft mutex, preventing duplicate sessions from running, and lets you “exit” the session from within by running <code>mise run start</code> again, which should be fairly close in the shell history of the application server window.</p>
</section>
</section>
<section id="Conclusion-and-Alternatives">
<h2>Conclusion and Alternatives</h2>
<p>While none of these are an immediate panacea, they have improved ergonomics on many of the projects I work on. Bundling your random scripts up into tasks, bringing some of your dependency management closer to your application code, and ensuring that the README is a more resilient document should eliminate a lot of pain points.</p>
<p>If you’re not interested in using mise, another solution is to use Devcontainers. These are essentially docker containers with some conventions around them. Editors like VSCode can connect <em>directly</em> to a devcontainer, allowing you to edit your code within the container, without too much fussing with volumes and mounts. They manage to solve some of the pain points with docker, but I have less experience with them, so I can do little more than point out their existence.</p>
</section>
</section>
</body></html> ]]></description>
    </item>
    <item>
       <title>Writing in Djot</title>
       <link>https://pdx.su/blog/2025-06-28-writing-in-djot</link>
       <pubDate>Sat, 28 Jun 2025 17:33:54 -06</pubDate>
       <guid>https://pdx.su/blog/2025-06-28-writing-in-djot</guid>
       <description><![CDATA[ <html><head></head><body><section id="Writing-in-Djot">
<h1>Writing in Djot</h1>
<p>I’ve completed an effort to get Djot support implemented into <a href="https://github.com/elixir-tools/tableau">Tableau</a>, the Elixir powered static site generator I use to run this blog, and have started writing posts in it<a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a>. So far I’m pretty happy with it, although there are a few oddities that I’ve had to work around, and some changes that will take getting used to. Still, if you’re looking for something a bit more stringent than Markdown, Djot might be a good candidate for you.</p>
<section id="History">
<h2>History</h2>
<p>This is more of the history as it pertains to this blog. Djot itself has its own interesting history, starting with John MacFarlane’s <a href="https://johnmacfarlane.net/beyond-markdown.html">beyond markdown</a> blogpost, and moving into a proper <a href="https://github.com/jgm/djot#rationale">set of rationales</a> and then a <a href="https://djot.net">full specification</a>.</p>
<p>A few years ago, I <a href="/blog/2023-02-05-asciidoc-and-markdown/#djot">wrote a piece on AsciiDoc and Djot</a>, as alternatives to markdown. I mentioned at the time that I’d looked into using AsciiDoc to write <em>this</em> blog, but deemed that it would be untenable, due to the dearth of converters and simple tools at the time. With regards to AsciiDoc, not much has changed in 2.5 years, so its more or less still a curiosity more than a useful tool.</p>
<p>Djot, on the other hand, has seen a bit more success in adoption. While nowhere near the success of markdown, Djot has parsers available in a number of languages, some blessed officially, others just working nicely and quietly. It reminds me of the state of Markdown in the early 2010s, where people were ready to move beyond markdown.pl, but the field wasn’t saturated yet. Unlike that era however, Djot started off with rigorous standards, and so implementing support has a lot fewer unknowns.</p>
</section>
<section id="Djot-in-Elixir">
<h2>Djot in Elixir</h2>
<p>When I first started thinking about writing posts in Djot, I explored the field, to judge feasibility. At the time, there were no native Elixir, Erlang, or any other BEAM language based Djot parsers. There was, of course, the official JavaScript/TypeScript implementation, a few implementations in other languages, and <a href="https://pandoc.org/">Pandoc</a> support, so there were options.</p>
<p>At first, I explored writing a parser. I looked at using Packrat grammars, which are used to write the <a href="https://github.com/slime-lang/slime">slime</a> elixir parser, but ran into some issues with how Djot would be parsed. I’m sure if I’d kept at this path, a proper grammar could have been written, quite easily. But this was still at the toy project stage, and I didn’t want to spend hours thinking about grammars. This also knocks writing a parser using tools like NimbleParsec out of the running, as thats just trading one parsing technique for another.</p>
<p>I toyed with the idea of writing a simple Port around <a href="https://pandoc.org/">Pandoc</a>, since it has very good support. But ports are always tricky beasts. You have to figure out how to manage their version with the systems you’d install them on, in this case getting it running neatly in a Github Actions workflow, as thats what I use to build this site. Finally, I’d have to choose an output format from Pandoc that <a href="https://github.com/elixir-tools/tableau">Tableau</a> could use. Would I output markdown and have Tableau then parse+render it? Would I directly output HTML and transform it? Or should I use some intermediary format, an AST of some kind.</p>
<p>Using a port ultimately seemed like more complexity than it was worth, and this also disqualified a number of other options, like a shim to run the JavaScript based implementation. So all that really left was either writing it entirely in Elixir, which I discussed above, or using an existing implementation in another language that could be brought into elixir via a NIF.</p>
<p>I’ve done a bit of work with NIFs before, both in closed source codebases as well as minor contributions to tools like <a href="https://github.com/leandrocp/mdex">MDEx</a>, the markdown engine used by this blog, so I’m a little comfortable with them. I still have a lot I don’t know about them, but I knew enough to get started.</p>
<section id="Djot-in-Rust-gives-us-Djot-in-Elixir">
<h3>Djot in Rust gives us Djot in Elixir</h3>
<p>There is a very good, and stable, Djot implementation in rust, called <a>jotdown</a>. Getting it to output high quality HTML is very easy, and should you want to do more, its got a nice event-based API for handling parsing, letting you write renderers in any output format you choose.</p>
<p>Since it already has a good HTML renderer built in, I felt like the hard part was mostly done. I started reading up on <a href="https://docs.rs/rustler/latest/rustler/">rustler</a>, a nice library for getting Rust NIFs working in Elixir. More or less stepping through the readme, I got version v0.1.0 of my <a href="https://github.com/paradox460/djot">Djot NIF</a> working. Shortly thereafter, I added a sigil for rendering Djot from markdown directly. And then it just sat there for a year, thereabouts.</p>
</section>
</section>
<section id="Djot-in-Tableau">
<h2>Djot in Tableau</h2>
<p>Around October 2024, I had succesfully moved my site from NuxtJS to Tableau, and was writing in Markdown. I messaged the author of Tableau, the excellent <a href="https://www.mitchellhanberg.com">mhanberg</a>, asking about feasibility of getting other markup parsing formats working in Tableau. He was open to the idea and started implementing it, but there were a few minor issues with my elixir djot package at the time. I’d specified it with a rather strict version for Rustler, which prevented it from compiling neatly with Tableau, which uses a Rustler based package for markdown parsing (<a>mdex</a>).</p>
<p>The easiest way to get them both working well together was just to relax the version number, which is what was done for Djot v0.1.2. But that still means that, to compile the application, you have to have a <em>full</em> rust install alongside your elixir install.</p>
<section id="Rustler-Precompiled">
<h3>Rustler Precompiled</h3>
<p><a href="https://github.com/leandrocp/mdex">MDEx</a>, and a number of other Rustler based packages for Elixir, use a trick to install precompiled rust binaries for the their NIFs to use. The advantage of this is that wherever you’re actually running the Elixir code <em>only has to be elixir</em>.</p>
<p>Setting up <a href="https://github.com/philss/rustler_precompiled">Rustler Precompiled</a> is a bit complex, mostly because you have to figure out how to make your CI of choice build the version matrices for as many systems as you want to support. Github Actions aren’t that difficult to make work across different arch+os combos, but its still tedium that has to be done. Once I’d got it working, I was of the opinion that I was ready to get things working in Tableau.</p>
</section>
<section id="More-Tableau-Changes">
<h3>More Tableau Changes</h3>
<p>My initial attempt to get everything working required me to update Tableau. Updates are always tricky, and the Tableau API had changed since I moved this site over to it. A particular sticking point was that extensions couldn’t easily access the rendered page output anymore. I use this feature to add the Table of Contents you see (on desktop) on the left side of the page, and to handle things like metadata for social sharing. Mitch had some life get in the way, so things stagnated for a while, but recently he made some changes to the API that have enabled me to not only upgrade this site, but make some important improvements to how I was handling things like the ToC.</p>
<p>For generating the ToC before these changes, I parsed the resulting HTML from the markdown document, encoded it, and stored that in a separate map on the internal state of the Tableau generation. Then at render time I fetched the ToC from that map, keyed off of the post’s filename, and used it to render the ToC.</p>
<p>With the updates to both Tableau and MDEx, I didn’t have to parse HTML anymore. MDEx has an <em>excellent</em> API for the AST it can generate from markdown, which enables all sorts of useful things, including <a href="https://github.com/paradox460/pdx.su/blob/f92fe5d53fa2722c9331634402fa10e1a16cbb25/lib/converters/mdex_converter.ex#L35-L44">netlify URL rewriting</a>. This API is <a href="https://hexdocs.pm/elixir/Access.html">Access</a> based, which means you can use some fairly clean tricks to traverse the AST and extract nodes.</p>
</section>
<section id="Adding-Djot-to-the-mix">
<h3>Adding Djot to the mix</h3>
<p>Getting the initial parsing of Djot and output of HTML working in Tableau was probably the easiest part. I just updated the configs, added a new converter, and I was off. However, <em>all</em> my extensions stopped working because they were built around the MDEx APIs, and my Djot library doesn’t have anything quite that powerful.</p>
<p>Since I was working with HTML output directly, and Djot’s HTML output differs from MDEx’s HTML output in some significant ways, I basically had to write separate pathways for each document to be parsed by, for each extension. For Djot documents I went back to HTML parsing. Not great, but since its a static site it only has to do that once for each update, not every request.</p>
<p>The final part of backend work was getting syntax highlighting working. MDEx has a syntax highlighter built in, but its also available as a <a href="https://github.com/leandrocp/autumnus">separate library</a>. Since I’ve already got a css-based theme working with this, it was the obvious choice. Plumbing it into the Djot converter wasn’t too hard, just a bit of HTML traverse and update logic, and now code blocks work just as well in Djot as they do in Markdown</p>
<p>Frontend wise, most of my JS and CSS worked nearly perfectly. I had to <a href="https://github.com/paradox460/pdx.su/commit/286318b72fedd1076657ca7ac463f13b7d779873">tune the frontend side of the Table of Contents</a> to work with both Markdown and Djot HTML, but that was ultimately a rather minor change. CSS was even smaller, with only a bit of reset styling around how browsers handle <code>section h1</code> elements.</p>
<p>And then I was done. As testified by this post being written in Djot.</p>
</section>
</section>
<section id="Plans-for-the-future-of-Djot-in-Elixir">
<h2>Plans for the future of Djot in Elixir</h2>
<p>Working with the MDEx API has been a joy, and working with the HTML output from <a>jotdown</a> has been a bit of a pain. Since jotdown has an event based system under the hood, I think a project I might explore over the next few months is to implement a Djot AST, similar to MDEx’s AST. It wouldn’t be 1:1, as Djot and Markdown have different structural components, but it would be similar enough to have the same good ergonomics. In particular I am a <em>massive</em> fan of how MDEx implements <a href="https://hexdocs.pm/elixir/Access.html">Access</a>, which leads to one of the most ergonomic tree manipulation experiences I’ve ever had in any language:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="comment"># Walk the MDEx AST, finding all MDEX.Image structs, and rewrite their URLs to netlify urls</span>
</span><span class="line" data-line="2"><span class="keyword-function">defp</span> <span class="function">do_netlify_images</span><span class="punctuation-bracket">(</span><span class="variable">pipe</span><span class="punctuation-bracket">)</span> <span class="keyword">do</span>
</span><span class="line" data-line="3">  <span class="variable">selector</span> <span class="operator">=</span> <span class="keyword">fn</span>
</span><span class="line" data-line="4">    <span class="punctuation-special">%</span><span class="module">MDEx.Image</span><span class="punctuation-bracket">{</span><span class="string-special-symbol">url: </span><span class="punctuation-bracket">&lt;&lt;</span><span class="string">&quot;/postimages/&quot;</span><span class="punctuation-delimiter">,</span> <span class="comment">_</span><span class="operator">::</span><span class="variable">binary</span><span class="punctuation-bracket">&gt;&gt;</span><span class="punctuation-bracket">}</span> <span class="operator">-&gt;</span> <span class="boolean">true</span>
</span><span class="line" data-line="5">    <span class="comment">_</span> <span class="operator">-&gt;</span> <span class="boolean">false</span>
</span><span class="line" data-line="6">  <span class="keyword">end</span>
</span><span class="line" data-line="7">
</span><span class="line" data-line="8">  <span class="module">Pipe</span><span class="operator">.</span><span class="function-call">update_nodes</span><span class="punctuation-bracket">(</span><span class="variable">pipe</span><span class="punctuation-delimiter">,</span> <span class="variable">selector</span><span class="punctuation-delimiter">,</span> <span class="keyword">fn</span> <span class="punctuation-special">%</span><span class="module">MDEx.Image</span><span class="punctuation-bracket">{</span><span class="string-special-symbol">url: </span><span class="variable">original_url</span><span class="punctuation-bracket">}</span> <span class="operator">=</span> <span class="variable">image</span> <span class="operator">-&gt;</span>
</span><span class="line" data-line="9">    <span class="punctuation-special">%</span><span class="module">MDEx.Image</span><span class="punctuation-bracket">{</span><span class="variable">image</span> <span class="operator">|</span> <span class="string-special-symbol">url: </span><span class="string">&quot;/.netlify/images?url=&quot;</span> <span class="operator">&lt;&gt;</span> <span class="variable">original_url</span><span class="punctuation-bracket">}</span>
</span><span class="line" data-line="10">  <span class="keyword">end</span><span class="punctuation-bracket">)</span>
</span><span class="line" data-line="11"><span class="keyword">end</span>
</span></code></pre>
<p>I’m also interested in moving Autumn based syntax highlighting over to the Rust side of things, so that it can occur with some native speed, without the serialization and HTML parsing costs. For Elixir Djot to be something usable in a dynamic setting, thats a bit more of a requirement.</p>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>Well, writing <em>this</em> post, but future ones will be in Djot as well, unless I have good reason to use Markdown.<a href="#fnref1" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</section>
</body></html> ]]></description>
    </item>
    <item>
       <title>Improving my HomeAssistant Automations with State Machines</title>
       <link>https://pdx.su/blog/2025-06-15-improving-my-homeassistant-automations-with-state-machines</link>
       <pubDate>Sun, 15 Jun 2025 01:13:44 -06</pubDate>
       <guid>https://pdx.su/blog/2025-06-15-improving-my-homeassistant-automations-with-state-machines</guid>
       <description><![CDATA[ <h1><a href="#improving-my-homeassistant-automations-with-state-machines" aria-hidden="true" class="anchor" id="improving-my-homeassistant-automations-with-state-machines"></a>Improving my HomeAssistant Automations with State Machines</h1>
<p>A little over a year ago, <a href="/blog/2024-06-09-migrating-my-homeassistant-automations-from-nodered-to-digital-alchemy/">I migrated the bulk of my HomeAssistant automations from NodeRED to DigitalAlchemy</a>, a TypeScript based automation system. Since then, I've slowly been adding more and more automations, of varying complexity. For simple ones, the built in event-based system works well enough, but as soon as you start having to track state across a few different entites, it becomes a big, unwieldly mess.</p>
<h2><a href="#state-machines" aria-hidden="true" class="anchor" id="state-machines"></a>State Machines?</h2>
<p>This is a problem encountered in a lot of other domains, and the solution is often to use what is known as a finite state machine. Finite State machines are a whole topic on their own, with plenty of ink spilled, so I will only skim the surface, but essentially they let you model a real world system as a series of states and transitions. States are things like &quot;on&quot;, &quot;off&quot;, &quot;open&quot;, &quot;closed&quot;, etc. Transitions are the &quot;in-betweens&quot;. &quot;turn on&quot; would be a transition from &quot;off&quot; to &quot;on&quot;, for example.</p>
<p>In the Elixir/Erlang world, a <em>lot</em> of things are modeled as state machines. GenServer, which a lot of people consider the building block of most Elixir/Erlang applications, is a kind of state machine. While it lacks the formalities that let you rigidly define transitions and states, the primitives are all there. There's even an erlang module called <a href="https://erlang.org/doc/man/gen_statem.html">gen_statem</a> which allows you to very easily build full fledged state machines.</p>
<p>Since DigitalAlchemy is typescript based, I can't use <code>gen_statem</code>, but there are a number of decent Javascript statemachine libraries out there. I settled on <a href="https://xstate.js.org">xstate</a>, which is rather mature, and ties into the <a href="https://stately.ai">stately.ai</a> platform, which has both a robust VSCode extension, and a decent web interface (more on this later)</p>
<h2><a href="#basic-xstate-usage" aria-hidden="true" class="anchor" id="basic-xstate-usage"></a>Basic XState usage</h2>
<p>XState is rather simple, once you get the hang of it. You define a state machine, and then create an actor from said state machine. The actor is an instance of the state machine. You can have multiple instances of the same state machine.</p>
<p>You trigger states by sending events to the actors. Depending on the current state, the events can trigger transitions, or do nothing at all. You can attach actions to transitions, as well as to entering/exiting a state. Using this, you can program in desired behavior, limiting potential states, and ignoring undefined behavior. For something like HomeAssistant, that means you don't have to explicitly code around &quot;undefined&quot; and other states that you don't care about.</p>
<h2><a href="#using-xstate-in-digitalalchemy" aria-hidden="true" class="anchor" id="using-xstate-in-digitalalchemy"></a>Using XState in DigitalAlchemy</h2>
<p>DigitalAlchemy doesn't really do anything out of the ordinary that would prevent you from using XState. In most cases, you just install it to your project, import it, write your state machine, and its off to the races.</p>
<h3><a href="#mailbox-monitor" aria-hidden="true" class="anchor" id="mailbox-monitor"></a>Mailbox monitor</h3>
<p>Recently, I built a simple mailbox notifier, using ESPHome. My mailbox has a top door and a bottom door, and I monitored both using reed switches. The mail carrier will always use the top door, as its the &quot;incoming&quot; mail, and I'll always use the bottom door to retrieve the mail, as its the locking mail bin.</p>
<p>Since the monitor is running on battery power, I used the deep sleep functionality of ESPHome. This means that the device is off most of the time. Since it takes a little bit of time to wake up and connect to HomeAssistant, there is a chance it misses a trigger. If the mail door is opened and closed before we can connect up to HomeAssistant, HomeAssistant will see the door as &quot;closed&quot;, and we won't know which door triggered. Since ESPHome has no concept of &quot;store and forward&quot; for events, we have to handle this in a round-about way. We add two additional binary sensors<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup>, one for each door, that listen to the main door sensors, and if they see them go true, they themselves go tru, and then <em>hold</em> that state until the next shutdown, which triggers at the end of the wakeup period. That way we can trigger off either the main door, or the &quot;sticky&quot; sensors. If you want to see the ESPHome YAML for the mailbox, its <a href="https://github.com/paradox460/HomeAssistantConfig/blob/main/esphome/mailbox-4559ac.yaml">available on my github</a></p>
<p>Over on the HomeAssistant side, we get a nice little device that has 4 entities we need to track:</p>
<ul>
<li><code>binary_sensor.mailbox_top_door</code>: the main top door sensor</li>
<li><code>binary_sensor.mailbox_bottom_door</code>: the main bottom door sensor</li>
<li><code>binary_sensor.mailbox_top_door_sticky</code>: the sticky top door sensor</li>
<li><code>binary_sensor.mailbox_bottom_door_sticky</code>: the sticky bottom door sensor</li>
</ul>
<p>We will also create a couple entities on the HomeAssistant side, to light up an icon on our dashboard indicating new mail, and to reset the state of our system, should something go wrong. We'll call one <code>binary_sensor.new_mail</code>, and the other <code>button.mailbox_reset</code>.</p>
<p>We can now actually createa a fairly simple state machine, <em>in home assistant</em>, without using DigitalAlchemy or xState, just by listening to the sensors above and using the <code>binary_sensor.new_mail</code> as our state tracker. For this case, it will work about the same. But we're going to use DigitalAlchemy and xState anyways, because for more complex cases, you can't just rely on a single state tracker, and it can get ugly quickly.</p>
<p>If we model the states of our mailbox monitor as a flow chart, we get something like this:</p>
<p><img src="/.netlify/images?url=/postimages/2025-06-15-improving-my-homeassistant-automations-with-state-machines/CleanShot%202025-06-14%20at%2023.47.48@2x.png" alt="Our Mailbox State Machine" /></p>
<p>We can see that the only way to get to <code>New Mail</code> is from the <code>Top Door Opened</code> event, and we can get from <code>New Mail</code> to <code>No Mail</code> from either the <code>Bottom door opened</code> event or the <code>Reset Button Pressed</code> event. Pressing the reset button, or opening the bottom door, while we are in a no-mail state does nothing, and so we don't have to deal with it. Our state machine will just ignore the event as an invalid transition</p>
<p>One of the coolest features of xState is that you can actually build your code using a flowchart. You can use the <a href="https://stately.ai">online editor</a>, or the <a href="https://marketplace.visualstudio.com/items?itemName=statelyai.stately-vscode">VSCode plugin</a>. I use the VSCode plugin, which is pictured in the above screenshot</p>
<p>On Each state, we encode some <em>actions</em> in the <code>entry</code>. Entry lets you say &quot;Any time this state becomes active, do this&quot;. For <code>New Mail</code>, we use Entry actions to send us notifications and to turn on our dashboard indicator. For <code>No Mail</code>, we use it to clear the notifications and turn our indicator off.</p>
<p>The final State Machine code looks like this:</p>
<pre class="athl"><code class="language-typescript" translate="no" tabindex="0"><span class="line" data-line="1"><span class="keyword">const</span> <span class="variable">machine</span> <span class="operator">=</span> <span class="function-call">setup</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="2">  <span class="variable-member">types</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="3">    <span class="variable-member">context</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span><span class="punctuation-bracket">&rbrace;</span> <span class="keyword-operator">as</span> <span class="punctuation-bracket">&lbrace;</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="4">    <span class="variable-member">events</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span><span class="punctuation-bracket">&rbrace;</span> <span class="keyword-operator">as</span>
</span><span class="line" data-line="5">      <span class="punctuation-delimiter">|</span> <span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">type</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;Top Door Opened&quot;</span> <span class="punctuation-bracket">&rbrace;</span>
</span><span class="line" data-line="6">      <span class="punctuation-delimiter">|</span> <span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">type</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;Bottom Door Opened&quot;</span> <span class="punctuation-bracket">&rbrace;</span>
</span><span class="line" data-line="7">      <span class="punctuation-delimiter">|</span> <span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">type</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;Reset&quot;</span> <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="8">  <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="9">  <span class="variable-member">actions</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="10">    <span class="function-method">notify</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span> <span class="operator">=&gt;</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="11">      <span class="function-call">notifier</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="12">    <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="13">    <span class="function-method">clearNotify</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span> <span class="operator">=&gt;</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="14">      <span class="function-call">clearNotifier</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="15">    <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="16">    <span class="function-method">indicatorOn</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span> <span class="operator">=&gt;</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="17">      <span class="variable">new_mail</span><span class="punctuation-delimiter">.</span><span class="variable-member">is_on</span> <span class="operator">=</span> <span class="boolean">true</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="18">    <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="19">    <span class="function-method">indicatorOff</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span> <span class="operator">=&gt;</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="20">      <span class="variable">new_mail</span><span class="punctuation-delimiter">.</span><span class="variable-member">is_on</span> <span class="operator">=</span> <span class="boolean">false</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="21">    <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="22">  <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="23"><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">.</span><span class="function-method-call">createMachine</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="24">  <span class="variable-member">context</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="25">  <span class="variable-member">id</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;Mailbox&quot;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="26">  <span class="comment">// initial: new_mail.is_on ? &quot;New Mail&quot; : &quot;No Mail&quot;,</span>
</span><span class="line" data-line="27">  <span class="variable-member">initial</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;No Mail&quot;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="28">  <span class="variable-member">states</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="29">    <span class="string">&quot;No Mail&quot;</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="30">      <span class="variable-member">on</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="31">        <span class="string">&quot;Top Door Opened&quot;</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="32">          <span class="variable-member">target</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;New Mail&quot;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="33">        <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="34">      <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="35">      <span class="variable-member">entry</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">[</span><span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">type</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;clearNotify&quot;</span> <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span> <span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">type</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;indicatorOff&quot;</span> <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="36">    <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="37">    <span class="string">&quot;New Mail&quot;</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="38">      <span class="variable-member">on</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="39">        <span class="string">&quot;Bottom Door Opened&quot;</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="40">          <span class="variable-member">target</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;No Mail&quot;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="41">        <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="42">        <span class="variable-member">Reset</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="43">          <span class="variable-member">target</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;No Mail&quot;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="44">        <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="45">      <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="46">      <span class="variable-member">entry</span><span class="punctuation-delimiter">:</span> <span class="punctuation-bracket">[</span><span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">type</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;notify&quot;</span> <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span> <span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">type</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;indicatorOn&quot;</span> <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="47">    <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="48">  <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="49"><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span></code></pre>
<p>In the <code>actions</code> block, we make a few calls to some functions we created for handling notifications, as well as setting some properties on DigitalAlchemy proxies. To tie our state machine into our actual monitor, we just need a bit of glue code, that takes state changes from HomeAssistant and uses them to trigger events on our state machine, which will trigger transitions.</p>
<pre class="athl"><code class="language-typescript" translate="no" tabindex="0"><span class="line" data-line="1"><span class="keyword">const</span> <span class="function">topDoorAction</span> <span class="operator">=</span> <span class="punctuation-bracket">(</span><span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">state</span><span class="punctuation-delimiter">:</span> <span class="variable-parameter">newState</span> <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">)</span> <span class="operator">=&gt;</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="2">  <span class="keyword-conditional">if</span> <span class="punctuation-bracket">(</span><span class="variable">newState</span> <span class="operator">==</span> <span class="string">&quot;on&quot;</span><span class="punctuation-bracket">)</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="3">    <span class="variable">mailboxActor</span><span class="punctuation-delimiter">.</span><span class="function-method-call">send</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">type</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;Top Door Opened&quot;</span> <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="4">  <span class="punctuation-bracket">&rbrace;</span>
</span><span class="line" data-line="5"><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="6">
</span><span class="line" data-line="7"><span class="variable">top_door</span><span class="punctuation-delimiter">.</span><span class="function-method-call">onUpdate</span><span class="punctuation-bracket">(</span><span class="variable">topDoorAction</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="8"><span class="variable">top_door_sticky</span><span class="punctuation-delimiter">.</span><span class="function-method-call">onUpdate</span><span class="punctuation-bracket">(</span><span class="variable">topDoorAction</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="9">
</span><span class="line" data-line="10"><span class="keyword">const</span> <span class="function">bottomDoorAction</span> <span class="operator">=</span> <span class="punctuation-bracket">(</span><span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">state</span><span class="punctuation-delimiter">:</span> <span class="variable-parameter">newState</span> <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">)</span> <span class="operator">=&gt;</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="11">  <span class="keyword-conditional">if</span> <span class="punctuation-bracket">(</span><span class="variable">newState</span> <span class="operator">==</span> <span class="string">&quot;on&quot;</span><span class="punctuation-bracket">)</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="12">    <span class="variable">mailboxActor</span><span class="punctuation-delimiter">.</span><span class="function-method-call">send</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">type</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;Bottom Door Opened&quot;</span> <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="13">  <span class="punctuation-bracket">&rbrace;</span>
</span><span class="line" data-line="14"><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="15">
</span><span class="line" data-line="16"><span class="variable">bottom_door</span><span class="punctuation-delimiter">.</span><span class="function-method-call">onUpdate</span><span class="punctuation-bracket">(</span><span class="variable">bottomDoorAction</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="17"><span class="variable">bottom_door_sticky</span><span class="punctuation-delimiter">.</span><span class="function-method-call">onUpdate</span><span class="punctuation-bracket">(</span><span class="variable">bottomDoorAction</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="18">
</span><span class="line" data-line="19"><span class="variable">reset_mail</span><span class="punctuation-delimiter">.</span><span class="function-method-call">onUpdate</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span> <span class="operator">=&gt;</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="20">  <span class="variable">mailboxActor</span><span class="punctuation-delimiter">.</span><span class="function-method-call">send</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">type</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;Reset&quot;</span> <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="21"><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span></code></pre>
<p>We create two anonymous functions, <code>topDoorAction</code> and <code>bottomDoorAction</code>, and then use them in the <code>onUpdate</code> handler for the 4 entities that can trigger events in our system. We also use a similar pattern for the reset button. Each handler function just calls our <code>mailboxActor</code> and sends it an event, which our above state machine definition defined. The state machine listens to those events, and if they can trigger a transition, they do, and we call the appropriate actions.</p>
<p>The full, working code example of the mailbox is <a href="https://github.com/paradox460/HomeAssistantConfig/blob/main/home_automation/src/mailbox.mts">here</a>, and the previous, non-state-machine driven version is <a href="https://github.com/paradox460/HomeAssistantConfig/blob/1221833326b58d03bc48a7211c6f631793dc8dbb/home_automation/src/mailbox.mts">here</a></p>
<h3><a href="#3d-printer-automation" aria-hidden="true" class="anchor" id="3d-printer-automation"></a>3D Printer Automation</h3>
<p>My 3D Printer is connected to a smart switch, which I use to turn off power to the printer when it's not in use. This is a mix of a safety precaution and a bit of energy saving. It's a safety precaution to prevent the printer from doing things without my initating them, and an energy saving meeasure to prevent the device from drawing power while idle. I want to have the switch turn the printer off after a few hours of inactivity. We can define inactivity as any time the printer is not printing or running a filament dryer.</p>
<p>Modeling our state machine in xState, we get something like this:</p>
<p><img src="/.netlify/images?url=/postimages/2025-06-15-improving-my-homeassistant-automations-with-state-machines/CleanShot%202025-06-15%20at%2001.02.11@2x.png" alt="3D printer state machine diagram" /></p>
<p>It's a lot more complicated than the Mailbox monitor, but can be broken down into a few main &quot;things&quot;</p>
<ul>
<li>The printer is idle when it is not printing or drying</li>
<li>The printer can be both printing and drying at the same time</li>
<li>Printing has multiple sub states that should be considered &quot;active&quot;</li>
<li>Drying has only one state that should be considered active, &quot;drying&quot;</li>
<li>Both printing and drying can be &quot;idle&quot;</li>
<li>We want to have an initial state that is unsynced, where we don't actually know the state of the printer</li>
<li>At any time, we can transition to <code>power_off</code>, because the real world has things like power failures or users hitting the e-stop button</li>
<li><code>power_off</code> can only transition to <code>idle</code>, because the printer has to boot up before it can resume a power outage print, or anything else</li>
</ul>
<p>Key things to call out in this state machine are the use of actions on transitions, guard clauses, &quot;after&quot; transitions, and parallel state machines.</p>
<p>Actions on transitions let us perform something whenever a particular transition is triggered, and <em>only</em> when that transition is triggered. On the <code>startPrinting</code> transition, we reset an energy meter, so I can see how much power the current print has used. Similarly, on the transitions out of the <code>printing</code> state, we typically fire off notification handlers. Note that we are not using any entry/exit actions in this state machine, they simply don't suit our needs here.</p>
<p>Guard clauses are used to check the status of both child state machines (<code>printing</code> and <code>drying</code>) on the <code>active</code> state machine. We have this guard clause set in what's known as an <code>always</code> transition, a transition that will fire on <em>every</em> single other transition involving the <code>active</code> state machine, which is all of the transitions of its children, and transitions on itself. The guard clause prevents it from firing when some condition isn't met. In this case, we have the guard clause set to check if both the <code>printing</code> and <code>drying</code> state machines are <code>idle</code>. If so, we can transition back to our parent machine's <code>idle</code> state.</p>
<p>&quot;After&quot; transitions let us fire a transition if a state machine has been in a particular state for a duration of time. We use it here to shut down our 3D printer's smart plug if we've been in the <code>idle</code> state for a few hours. If we transition out of the idle state at all, for any reason, then the timer is cancelled, and will start from the top next time we enter the idle state. This transition has an explicit action tied to it, which turns off our smart plug. This is the only time the state machine will turn off the smart plug</p>
<p>Finally, there's a top-level transition, which always listens for a <code>turnOff</code> event. Should our smart switch be turned off at any time, for any reason, we can transition our state machine to <code>power_off</code>, and have it match reality.</p>
<p>Before I moved this to a state machine, I had a fairly complicated bit of code to track the status of a 3D printer. Whenever the printer was idle, and the smart plug was on, I would start a timer. When the timer finished running, it would check the state of the 3D printer, and if it was in a &quot;good&quot; state (not printing, not drying), it would turn off. If I started a print job or a drying job, the timer would be cancelled, to be resumed later. This was rather fragile, as &quot;printing&quot; is a whole progression of states of the printer, and handling the corner cases, such as what happens when a print/dry job finishes while a longer print/dry job is still running, became maddening. There was also some speical logic around drying, as that gives us an end time, but it never really worked all that well.</p>
<p>You can see the full <a href="https://github.com/paradox460/HomeAssistantConfig/blob/main/home_automation/src/bambu.mts">state machine and associated digital-alchemy code on github</a>, and <a href="https://github.com/paradox460/HomeAssistantConfig/blob/c63072a792fe7a228e64999be65ae76326fafda3/home_automation/src/bambu.mts">what it looked like before</a>.</p>
<h2><a href="#that-looks-an-awful-lot-like-nodered" aria-hidden="true" class="anchor" id="that-looks-an-awful-lot-like-nodered"></a>That looks an awful lot like NodeRED</h2>
<p>Kind of! Flowchart based programming can be useful, particularly for things like state machines, but I still find it much easier to reason about this than NodeRED. NodeRED had some weird constructs, that I just haven't had to work around in this. Since there's always &quot;real code&quot; right there, I never felt as constrained as I did in NodeRED, where I'd frequently just toss a Javascript action in there to get something done when I couldn't figure it out.</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>We could have encoded both into a single sensor, using tricks like bitmasks, but it doesn't really cost anything extra to add another sensor, and the logic is much simpler to follow. <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
    <item>
       <title>How a simple chicken coop door opener became a huge project</title>
       <link>https://pdx.su/blog/2025-06-11-how-a-simple-chicken-coop-door-opener-became-a-huge-project</link>
       <pubDate>Wed, 11 Jun 2025 18:27:38 -06</pubDate>
       <guid>https://pdx.su/blog/2025-06-11-how-a-simple-chicken-coop-door-opener-became-a-huge-project</guid>
       <description><![CDATA[ <h1><a href="#how-a-simple-chicken-coop-door-opener-became-a-huge-project" aria-hidden="true" class="anchor" id="how-a-simple-chicken-coop-door-opener-became-a-huge-project"></a>How a simple chicken coop door opener became a huge project</h1>
<p><img src="/.netlify/images?url=/postimages/2025-06-11-how-a-simple-chicken-coop-door-opener-became-a-huge-project/PXL_20250428_011031144.MP.jpg" alt="The automatic opener, in all its glory" />
The door and opener.</p>
<p>I have hens. A decent amount of hens. And one thing you have to do when you have hens is secure them at night. Most people will agree that chicken is tasty, and most predators share that opinion. So a big hen house full of tasty birds is a tempting target for any number of raccoons, foxes, cats, opossums, and more.</p>
<p>When we first got hens, we kept them in a small coop we ordered online. It wasn't particularly well-made, and the doors were flimsy. But it was good enough for our first year of birds, and by year two we knew we needed something better. To build a better coop, I laid a concrete pad, stuck a Costco plastic shed on it, and cut a hole in the side for a door. Our local farm store sold a <a href="https://www.chickenguard.com/product/pro-door-kit/">simple automatic door</a>, which mounted over the hole, had a rudimentary set of programs, and did the job reasonably well. This unit gave us 3 good years of service, and would likely have given more. But there were a few issues that I wanted to address.</p>
<h2><a href="#the-problems" aria-hidden="true" class="anchor" id="the-problems"></a>The problems</h2>
<p>During the summer, we have a lawn care company come by every month to spray various treatments on the lawn. Things like fertilizer, pesticides, the usual suburban lawn treatments. They usually give me 24 hours notice they'll be heading out to our property, so the night before I can disable the next morning's auto open feature, keeping the hens inside for a reasonable time after whatever substances have been applied to the yard have had a chance to soak in. The door has no remote control features, all interaction has to be done at the control module. Its interface is reminiscent of early 2000s electronics, with lots of holding buttons down, navigating menus on a single line dot matrix display, and lots of frustrating repetition.</p>
<p>During the shoulder seasons, the weather can be unpredictable. Sometimes we get snow as early as September, other times we don't have snow on the ground until January. We like to let the birds out as much as possible, but don't want to wake up and have hens freezing in the snow. So we typically disable auto-open in mid-fall, and re-enable it in mid spring. But if we have a long, warm fall, or a warm, early spring, we have to manually open the door every day to let the birds out, until we decide to switch to automatic opening.</p>
<p>As far as programmable open times, the unit has a few modes, aside from manual opening. The obvious mode is time based, which uses a little RTC on the device to keep time, and opens or closes at preset times. Works well enough, even with clock drift. Chickens aren't particularly picky about time. The other mode is sensor based, which uses an internal light sensor to determine the sun rise and set times. We primarily used this one, as the rise/set time changes over the course of the summer, and having it set at something like 9 PM might be too late for the edges of the season, but too early for the middle. But since it's a light sensor, it depends on the ambient lighting. Stormy days, or even just heavily overcast days, would trigger the sensor early. The hens seem to know the difference between a cloudy sky and sunset, and so we'd sometimes find hens waiting patiently outside a locked door.</p>
<h2><a href="#things-the-existing-door-did-well" aria-hidden="true" class="anchor" id="things-the-existing-door-did-well"></a>Things the existing door did well</h2>
<p>Looking at how the existing automatic door opener worked, there are a few very clever choices that were made, that I wanted to keep. The controller and motor don't directly drive the door, rather they use a short length of cable to raise/lower the door. The door itself has a latching mechanism that engages when it hits the end of its downwards travel, and disengages when the cable begins to lift. So all the motor has to drive is a winch. An additional benefit of this is that there is minimal chance of hurting a chicken if she blocks the door. The door itself is rather light, a couple pounds at most, and so if the full weight of it lands on a bird she can shrug it off.</p>
<p>I've seen some other door opener designs that use direct-drive motors (like a garage door) or linear actuators, and then have to have added complexity in detecting door blockages and handling them.</p>
<h2><a href="#this-should-be-simple-right" aria-hidden="true" class="anchor" id="this-should-be-simple-right"></a>This should be simple, right?</h2>
<p>So, keeping what the existing system did both right and wrong in mind, the solution, to someone who has recently been a bit obsessed with Home Automation, particularly with <a href="https://esphome.io/">ESPHome</a>, was obvious. Build a new door opener, one that could be controlled via Home Assistant. Building atop this platform gives me full remote control, from a system I already use, as well as some robust scheduling primitives, like sunrise/sunset times, ambient temperature, and anything else you can think of.</p>
<p>To enable such an opener, I'd need a few things:</p>
<ul>
<li><strong>A control module:</strong> Some ESP32 variant. It would need to have an external Wi-Fi antenna, since this will sit out in my yard, and I wasn't sure about how strong my Wi-Fi signal would be that far. <a href="https://amzn.to/3HAfNEd">Seeed Studios Xiao ESP32C3</a> fit the bill nicely.</li>
<li><strong>A Motor:</strong> I probably overestimated the specs on the motor, but I wanted something that would turn reasonably fast, have a fair bit of torque, and use a gearbox to get as much power as possible. Amazon is covered in motor modules for DIYers, and so I picked <a href="https://amzn.to/4mXIYBn">this one</a>, based on little more than the fact that it was 12V, 100RPM, and small-ish.</li>
<li><strong>A way of controlling the motor:</strong> I know you can't drive that sort of motor directly from a microcontroller, and so I needed a way to control it. A simple on-off relay wouldn't work here either, as I needed to be able to reverse the polarity to drive the motor both clockwise and counter-clockwise. A <a href="https://amzn.to/3SNvHxl">h-bridge</a> fits the bill perfectly, and this particular unit also outputs 5V, for powering a microcontroller</li>
<li><a href="https://amzn.to/4jObC50"><strong>Wi-Fi Antenna:</strong></a> for obvious reasons</li>
<li><a href="https://amzn.to/4lncfUD"><strong>A weatherproof button:</strong></a>, to control the door manually</li>
<li><strong>A power supply:</strong> More on this one later, but I had to revisit this point a couple of times.</li>
<li><strong>An Enclosure:</strong> I have a 3D printer. I have lots of filament. This was probably the easiest part.</li>
<li><strong>A spool for the cable:</strong> Again, 3D printer</li>
</ul>
<p><img src="/.netlify/images?url=/postimages/2025-06-11-how-a-simple-chicken-coop-door-opener-became-a-huge-project/PXL_20250305_034604251.jpg" alt="ESP32, H-Bridge, and a Motor" />
Components!</p>
<h2><a href="#the-enclosure" aria-hidden="true" class="anchor" id="the-enclosure"></a>The enclosure</h2>
<p><img src="/.netlify/images?url=/postimages/2025-06-11-how-a-simple-chicken-coop-door-opener-became-a-huge-project/CleanShot%202025-06-11%20at%2019.10.31@2x.png" alt="Cross-section of the enclosure" /></p>
<p>The enclosure started off as a big box, and ended up as a big box. There's not that much you can do different in this space. But there are some subtleties.</p>
<p>The spool sits in its own little mini-enclosure in the box, with a hole for the cable to pass-through. While this won’t prevent everything from getting in, it does separate the electronics from the outside.</p>
<p>For all the external connections the unit will need, I created a pass-through for a <a href="https://amzn.to/4mP8J6K">Deutsch Connector</a>. I have a bunch of these on hand from other projects, and they're a personal favorite. Deutsch sells a bulkhead connector, but I didn't have any, so I made my own bulkhead, which can be anchored into the main enclosure using some M2.5 screws and a few heat-set inserts. For weatherproofing, a TPU gasket is used, that sits between the bulkhead and the enclosure. The antenna and the button both have pass-through holes in the side. The button has its own rubber gasket, while the antenna just uses an o-ring. I also added a little &quot;roof&quot; over each, to slightly increase the weatherization. Probably not necessary, but it looks nice. Finally, the top of the enclosure has a shadow line where it meets the top piece, with a gasket, to further aid in gasket. And then the top itself is held down by 4 M3 screws, engaging with heat-set inserts in the main enclosure.</p>
<p>Since I wasn't entirely sure about how well the 3D printed parts would fit the Deutsch connectors, I made that bulkhead its own part. While my initial print fit the 2-position connector I was initially using, having the bulkhead be a separate part proved to be unusually prescient.</p>
<h2><a href="#putting-it-all-together" aria-hidden="true" class="anchor" id="putting-it-all-together"></a>Putting it all together</h2>
<p>Getting everything put together on a breadboard was rather easy, and after a quick flashing of the ESP32 with ESPHome, a bit of yaml, and some inhalation of solder fumes, I had a primitive prototype. I'd click the door open button in HomeAssistant, the motor would turn on, spin for a bit, then turn off. Attaching a spool and string let it actually wind and unwind the string, and putting a 5lbs weight on the end let me estimate how much current it might draw in the wild. Everything was working far too well.</p>
<p>The initial ESPHome component I used was a <a href="https://esphome.io/components/cover/template">Template Cover</a>. The motor controller is tied to a couple GPIO pins, and so this template simply turns the appropriate pins on and off, with some <code>delay</code> actions to turn the respective pin off after a time delay. This is pretty much exactly how the existing door opener worked, but while mulling things over, I realized that I'd prefer to have some form of feedback from the door, so I can tell if it was open or closed. Adding this feedback to the system lets you use the aptly named <a href="https://esphome.io/components/cover/feedback">Feedback Cover</a>, which handles most of the automation I had written by hand in the template sensor, as well as letting you do things like open the door an arbitrary percentage. The feedback cover is still mostly time based, but lets you put some end stops in, that will stop the motor when it reaches the end of its travel, regardless of how long it took (within limits, I always left some upper level time limit on there to prevent suck doors from breaking things).</p>
<p>But adding endstops means I needed to add more components. I decided to use <a href="https://amzn.to/3HDul5T">magnetic contact sensors</a>, like you'd use for a door or window in a burglar alarm system. I could stick the magnet on the door, and stick the sensors on the rail it travels along, and get a good indication of when the door is open or closed. These sensors are simple, when a magnet is near, they complete a circuit. Wiring them up to everything is rather simple. One side connects to a GPIO pin, the other connects to ground, and then with a bit of ESPHome yaml, you now have a sensor. I wanted all the connections to be inside the weatherproof box, and so I needed a way to bring in 4 more wires. The 2-position Deutsch connector I was using wouldn't cut it. So I grabbed a 6 position connector, printed a new bulkhead plate, crimped some connectors on the end of the sensor lines, and wired their corresponding positions to the board, and now had feedback. The motor would spin until the endstop for the direction of travel closed, and then it would stop.</p>
<p>Everything worked, so I moved all the connections from a breadboard connection to a hard-solder connection, put everything in the enclosure, sealed it up, and then waited for the weather to warm up, so I could install it on the hen house.</p>
<h2><a href="#getting-power-to-it" aria-hidden="true" class="anchor" id="getting-power-to-it"></a>Getting power to it</h2>
<p><img src="/.netlify/images?url=/postimages/2025-06-11-how-a-simple-chicken-coop-door-opener-became-a-huge-project/PXL_20250422_205017567.jpg" alt="The empty trench" />
The trench, sitting empty.</p>
<p>I'd decided early on that this opener would run off mains power. I didn't want to have to deal with batteries, deep sleep, or anything else. I needed to get power out to the hen house, the old extension-cord across the yard wasn't going to cut it, for running a water heater in the winter, and a fan in the summer, and so this served as a good excuse. I have an exterior outlet with a junction box on the side of my house, and so getting power from the house across the yard to the henhouse was a fairly simple problem, with a simple solution: Dig a ditch, run some conduit in the bottom, and pull power.</p>
<p>Simple is only a conceptual word here, as the actual project proved a bit more involved. I rented a trenching machine to dig a 3' deep trench from the house to the coop, and in typical trenching machine fashion, the clutch was busted. Judicious use of a spring clamp and a piece of rebar managed to keep the clutch engaged, and within an hour I had a nice deep trench across my yard. And, of course, I hit some sprinkler lines. 3 of them to be exact. Fortunately, I didn't hit the sprinkler control wires, just a main water line and two zone lines. I keep the system depressurized when it's not in use, and so fixing the breaks wasn't complex, but it was a dirty project that took the better part of an afternoon. Once the sprinkler line was fixed, laying the flexible conduit was trivial, and filling in the trench wasn't much harder.</p>
<p><img src="/.netlify/images?url=/postimages/2025-06-11-how-a-simple-chicken-coop-door-opener-became-a-huge-project/PXL_20250427_000731822.jpg" alt="The filled in trench" />
Filled in, but still ugly. Grass will eventually move in.</p>
<p>Pulling 3 conductors through the conduit was the usual pain, fish tape and pulling lubricant made it a bit easier, but I'm never a fan of pulling wire through conduit. Beats pulling it through walls, but not by much. I also ran a line of CAT-8, unterminated, if I ever want to add a Wi-Fi extender, cameras, or anything else out at the coop. At the coop side, I put in a large box, put a few outlets inside it, and then I put a small box with an in-use cover on the outside, so powering heaters and fans is a matter of plug and play. Connecting up the wires at the house side, everything read green.</p>
<h2><a href="#mounting-the-opener-and-power-troubles" aria-hidden="true" class="anchor" id="mounting-the-opener-and-power-troubles"></a>Mounting the opener, and power troubles</h2>
<p>Mounting the opener was easy enough, I just unscrewed the old one, screwed the new one in, tied the cable to the door, ran the power lines from the large electrical box, where a wall-wart transformer turned the 120VAC to 12VDC, and voilà, a working chicken door opener.</p>
<p>Or so I thought. The door closed just fine the first night, and opened just fine the next morning. But the next evening, the door sat firmly open past sunset. Checking the device in HomeAssistant, it showed several log entries of the device rebooting. At first, I worried it might be an issue with an electrical short, so I manually closed the door, and checked the wiring the next day. Everything was fine, and so I enabled some debug logging, specifically setting up a text sensor to tell me the previous reboot reason, and just left the unit to do its thing. The door opened and closed just fine for a few days, and then failed to open one morning. Checking the logs, I saw the same rebooting issue, and this time the debug sensor told me the unit rebooted due to brownout.</p>
<p>The cheap wall wart power supply I used, which claimed to deliver 30W of power was capping out at 15.6W of power, meaning it was seeing voltage drop whenever the motor tried to move. Frustrated, I grabbed a spare <a href="https://amzn.to/4jMLwPY">50W Meanwell power supply</a> I had left over from some <a href="https://pdx.su/blog/2024-08-10-diy-permanent-xmas-lights/">xmas light projects</a>, wired it up, and put everything back together. Since then the door hasn't given me any troubles.</p>
<h2><a href="#the-code" aria-hidden="true" class="anchor" id="the-code"></a>The Code</h2>
<p>I've more or less glossed over the code up to this point, as its only marginally interesting. The full version is <a href="https://github.com/paradox460/HomeAssistantConfig/blob/main/esphome/chicken-coop-be4e8c.yaml">on my github</a>, which should always be updated, but here are some highlights I thought could be useful to others.</p>
<p>The primary motivational factor of this whole project was being able to turn off auto open at any time. This is handled through a simple template switch, which shows up in HomeAssistant:</p>
<pre class="athl"><code class="language-yaml" translate="no" tabindex="0"><span class="line" data-line="1"><span class="property">switch</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="2">  <span class="punctuation-delimiter">-</span> <span class="property">platform</span><span class="punctuation-delimiter">:</span> <span class="string">template</span>
</span><span class="line" data-line="3">    <span class="property">id</span><span class="punctuation-delimiter">:</span> <span class="string">auto_open</span>
</span><span class="line" data-line="4">    <span class="property">name</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;Automatic Open&quot;</span>
</span><span class="line" data-line="5">    <span class="property">optimistic</span><span class="punctuation-delimiter">:</span> <span class="boolean">true</span>
</span><span class="line" data-line="6">    <span class="property">restore_mode</span><span class="punctuation-delimiter">:</span> <span class="string">RESTORE_DEFAULT_OFF</span>
</span></code></pre>
<p>This switch's value is checked during the auto-open automation on the <code>sun</code> component, which also handles a delay. Since the hens don't always go to bed exactly at astronomical sunset (when the sun is just below the horizon), I wanted to add a bit of delay to the door closing, so that the birds get in there. Initially, I hardcoded this delay, but after a few tweaks, I got tired of having to recompile and reflash every time I needed to tune it. So I added a few number inputs, which let me set the delay in HomeAssistant, and then have the ESPHome use them for delays when sun events happen:</p>
<pre class="athl"><code class="language-yaml" translate="no" tabindex="0"><span class="line" data-line="1"><span class="property">number</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="2">  <span class="punctuation-delimiter">-</span> <span class="property">platform</span><span class="punctuation-delimiter">:</span> <span class="string">template</span>
</span><span class="line" data-line="3">    <span class="property">name</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;Open Delay&quot;</span>
</span><span class="line" data-line="4">    <span class="property">id</span><span class="punctuation-delimiter">:</span> <span class="string">open_delay</span>
</span><span class="line" data-line="5">    <span class="property">min_value</span><span class="punctuation-delimiter">:</span> <span class="number">0</span>
</span><span class="line" data-line="6">    <span class="property">max_value</span><span class="punctuation-delimiter">:</span> <span class="number">600</span>
</span><span class="line" data-line="7">    <span class="property">unit_of_measurement</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;min&quot;</span>
</span><span class="line" data-line="8">    <span class="property">mode</span><span class="punctuation-delimiter">:</span> <span class="string">box</span>
</span><span class="line" data-line="9">    <span class="property">step</span><span class="punctuation-delimiter">:</span> <span class="number">1</span>
</span><span class="line" data-line="10">    <span class="property">optimistic</span><span class="punctuation-delimiter">:</span> <span class="boolean">true</span>
</span><span class="line" data-line="11">    <span class="property">restore_value</span><span class="punctuation-delimiter">:</span> <span class="boolean">true</span>
</span><span class="line" data-line="12">    <span class="property">initial_value</span><span class="punctuation-delimiter">:</span> <span class="number">30</span>
</span><span class="line" data-line="13">    <span class="property">icon</span><span class="punctuation-delimiter">:</span> <span class="string">mdi:weather-sunset-up</span>
</span><span class="line" data-line="14">  <span class="punctuation-delimiter">-</span> <span class="property">platform</span><span class="punctuation-delimiter">:</span> <span class="string">template</span>
</span><span class="line" data-line="15">    <span class="property">name</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;Close Delay&quot;</span>
</span><span class="line" data-line="16">    <span class="property">id</span><span class="punctuation-delimiter">:</span> <span class="string">close_delay</span>
</span><span class="line" data-line="17">    <span class="property">min_value</span><span class="punctuation-delimiter">:</span> <span class="number">0</span>
</span><span class="line" data-line="18">    <span class="property">max_value</span><span class="punctuation-delimiter">:</span> <span class="number">600</span>
</span><span class="line" data-line="19">    <span class="property">unit_of_measurement</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;min&quot;</span>
</span><span class="line" data-line="20">    <span class="property">mode</span><span class="punctuation-delimiter">:</span> <span class="string">box</span>
</span><span class="line" data-line="21">    <span class="property">step</span><span class="punctuation-delimiter">:</span> <span class="number">1</span>
</span><span class="line" data-line="22">    <span class="property">optimistic</span><span class="punctuation-delimiter">:</span> <span class="boolean">true</span>
</span><span class="line" data-line="23">    <span class="property">restore_value</span><span class="punctuation-delimiter">:</span> <span class="boolean">true</span>
</span><span class="line" data-line="24">    <span class="property">initial_value</span><span class="punctuation-delimiter">:</span> <span class="number">15</span>
</span><span class="line" data-line="25">    <span class="property">icon</span><span class="punctuation-delimiter">:</span> <span class="string">mdi:weather-sunset-down</span>
</span></code></pre>
<p>Finally, the <code>sun</code> component is where the actual automation happens. The <code>on_sunset</code> event is fairly simple, and fires unconditionally. If the door is <em>ever</em> open, I want it closed at sunset (accounting for the delay, of course). The <code>on_sunrise</code> event has a logic check to see if automatic open is on, and if so, it waits for the delay from the number component, then closes</p>
<pre class="athl"><code class="language-yaml" translate="no" tabindex="0"><span class="line" data-line="1"><span class="property">sun</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="2">  <span class="property">latitude</span><span class="punctuation-delimiter">:</span> <span class="type">!secret</span> <span class="string">latitude</span>
</span><span class="line" data-line="3">  <span class="property">longitude</span><span class="punctuation-delimiter">:</span> <span class="type">!secret</span> <span class="string">longitude</span>
</span><span class="line" data-line="4">  <span class="property">on_sunrise</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="5">      <span class="punctuation-delimiter">-</span> <span class="property">then</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="6">        <span class="punctuation-delimiter">-</span> <span class="property">if</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="7">            <span class="property">condition</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="8">              <span class="property">lambda</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;return id(auto_open).state;&quot;</span>
</span><span class="line" data-line="9">            <span class="property">then</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="10">              <span class="punctuation-delimiter">-</span> <span class="property">delay</span><span class="punctuation-delimiter">:</span> <span class="type">!lambda</span> <span class="string">&#39;return id(open_delay).state * 60000;&#39;</span>
</span><span class="line" data-line="11">              <span class="punctuation-delimiter">-</span> <span class="property">cover.open</span><span class="punctuation-delimiter">:</span> <span class="string">chicken_coop_door</span>
</span><span class="line" data-line="12">  <span class="property">on_sunset</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="13">    <span class="punctuation-delimiter">-</span> <span class="property">then</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="14">        <span class="punctuation-delimiter">-</span> <span class="property">delay</span><span class="punctuation-delimiter">:</span> <span class="type">!lambda</span> <span class="string">&#39;return id(close_delay).state * 60000;&#39;</span>
</span><span class="line" data-line="15">        <span class="punctuation-delimiter">-</span> <span class="property">cover.close</span><span class="punctuation-delimiter">:</span> <span class="string">chicken_coop_door</span>
</span></code></pre>
<p>For diagnostic information, and because it's fun to see, I also added a couple text sensors that return the next open/close time. These are a wee bit more complex, as they have to translate the text timestamps the <code>sun</code> text_sensor component returns into epoch time, do some time math, and then convert them back to timestamps, but it's still fairly basic:</p>
<pre class="athl"><code class="language-yaml" translate="no" tabindex="0"><span class="line" data-line="1"><span class="property">text_sensor</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="2">  <span class="punctuation-delimiter">-</span> <span class="property">platform</span><span class="punctuation-delimiter">:</span> <span class="string">sun</span>
</span><span class="line" data-line="3">    <span class="property">name</span><span class="punctuation-delimiter">:</span> <span class="string">Next Auto Open</span>
</span><span class="line" data-line="4">    <span class="property">type</span><span class="punctuation-delimiter">:</span> <span class="string">sunrise</span>
</span><span class="line" data-line="5">    <span class="property">format</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;%Y-%m-%d %H:%M:%S&quot;</span>
</span><span class="line" data-line="6">    <span class="property">entity_category</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;diagnostic&quot;</span>
</span><span class="line" data-line="7">    <span class="property">update_interval</span><span class="punctuation-delimiter">:</span> <span class="string">10min</span>
</span><span class="line" data-line="8">    <span class="property">filters</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="9">      <span class="property">lambda</span><span class="punctuation-delimiter">:</span> <span class="string"><span class="punctuation-delimiter">|-</span></span>
</span><span class="line" data-line="10"><span class="string">        const std::string input_time(x);</span>
</span><span class="line" data-line="11"><span class="string"></span>
</span><span class="line" data-line="12"><span class="string">        ESPTime parsed_time;</span>
</span><span class="line" data-line="13"><span class="string">        ESPTime::strptime(input_time, parsed_time);</span>
</span><span class="line" data-line="14"><span class="string"></span>
</span><span class="line" data-line="15"><span class="string">        parsed_time.recalc_timestamp_local();</span>
</span><span class="line" data-line="16"><span class="string">        parsed_time = ESPTime::from_epoch_local(parsed_time.timestamp + id(open_delay).state * 60);</span>
</span><span class="line" data-line="17"><span class="string"></span>
</span><span class="line" data-line="18"><span class="string">        char buffer[64];</span>
</span><span class="line" data-line="19"><span class="string">        parsed_time.strftime(buffer, sizeof(buffer), &quot;%Y-%m-%d %H:%M:%S&quot;);</span>
</span><span class="line" data-line="20"><span class="string">        return std::string(buffer);</span>
</span><span class="line" data-line="21">
</span><span class="line" data-line="22">  <span class="punctuation-delimiter">-</span> <span class="property">platform</span><span class="punctuation-delimiter">:</span> <span class="string">sun</span>
</span><span class="line" data-line="23">    <span class="property">name</span><span class="punctuation-delimiter">:</span> <span class="string">Next Auto Close</span>
</span><span class="line" data-line="24">    <span class="property">type</span><span class="punctuation-delimiter">:</span> <span class="string">sunset</span>
</span><span class="line" data-line="25">    <span class="property">format</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;%Y-%m-%d %H:%M:%S&quot;</span>
</span><span class="line" data-line="26">    <span class="property">entity_category</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;diagnostic&quot;</span>
</span><span class="line" data-line="27">    <span class="property">update_interval</span><span class="punctuation-delimiter">:</span> <span class="string">10min</span>
</span><span class="line" data-line="28">    <span class="property">filters</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="29">      <span class="property">lambda</span><span class="punctuation-delimiter">:</span> <span class="string"><span class="punctuation-delimiter">|-</span></span>
</span><span class="line" data-line="30"><span class="string">        const std::string input_time(x);</span>
</span><span class="line" data-line="31"><span class="string"></span>
</span><span class="line" data-line="32"><span class="string">        ESPTime parsed_time;</span>
</span><span class="line" data-line="33"><span class="string">        ESPTime::strptime(input_time, parsed_time);</span>
</span><span class="line" data-line="34"><span class="string"></span>
</span><span class="line" data-line="35"><span class="string">        parsed_time.recalc_timestamp_local();</span>
</span><span class="line" data-line="36"><span class="string">        parsed_time = ESPTime::from_epoch_local(parsed_time.timestamp + id(close_delay).state * 60);</span>
</span><span class="line" data-line="37"><span class="string"></span>
</span><span class="line" data-line="38"><span class="string">        char buffer[64];</span>
</span><span class="line" data-line="39"><span class="string">        parsed_time.strftime(buffer, sizeof(buffer), &quot;%Y-%m-%d %H:%M:%S&quot;);</span>
</span><span class="line" data-line="40"><span class="string">        return std::string(buffer);</span>
</span><span class="line" data-line="41"><span class="string"></span>
</span></code></pre>
<p>Finally, the interface for controlling this is just a simple card on my dashboard, making use of a pop-up for the more involved settings. You can open, close, toggle auto-open, and view all the information in one place. And its already proven its worth. As I was writing this article up, I got a message from the lawn-care company stating they'll be out tomorrow to spray, and so I turned off the auto-open, which will keep the door shut tomorrow until I remotely trigger the opening.</p>
<p>****<img src="/.netlify/images?url=/postimages/2025-06-11-how-a-simple-chicken-coop-door-opener-became-a-huge-project/CleanShot%202025-06-11%20at%2020.02.31@2x.png" alt="The main dashboard controls" />
Controls on my main dashboard. A toggle for auto-open, then Open, Stop, and Close controls</p>
<p><img src="/.netlify/images?url=/postimages/2025-06-11-how-a-simple-chicken-coop-door-opener-became-a-huge-project/CleanShot%202025-06-11%20at%2020.02.37@2x.png" alt="The detailed pop up" />
Clicking the icon on the main controls pops up this dialog, giving you a bit more information.</p>
<p>I've thought about adding some more &quot;smarts&quot; on the HomeAssistant side, like disabling auto-open if the temperature is too low, or if its raining, but those are projects for another day.</p>
<p><img src="/.netlify/images?url=/postimages/2025-06-11-how-a-simple-chicken-coop-door-opener-became-a-huge-project/PXL_20230901_164421732.jpg" alt="The chickens" />
The chickens themselves.</p>
<h2><a href="#updates" aria-hidden="true" class="anchor" id="updates"></a>Updates</h2>
<h3><a href="#2025-06-22" aria-hidden="true" class="anchor" id="2025-06-22"></a>2025-06-22</h3>
<p>I was having a few issues with brown-outs, even after upgrading the power supply (as mentioned earlier). They were fewer and further between, but for an appliance like this, we don't want <em>any</em> issues. It had yet to leave the door <em>open</em> at night, but there were a few mornings when I'd look outside and not see any hens running about. From what I can gather online, the H-Bridges I use, the ones with the built-in 12V-5V converter, occasionally have issues where the converter will output less than 5V intermittently. It seems to be hit or miss if you get one that has this problem, so I'll continue to recommend them, but if you do get one that has these issues, then you can get a <a href="https://amzn.to/4kSS5BK">simple little buck converter</a>, wire it in series with the H-Bridge unit, and connect your ESP32 to the output of the buck converter instead.</p>
<p>I've been running this setup for a few days now, some of the hottest we've had this year, and it's yet to give me any trouble.</p> ]]></description>
    </item>
    <item>
       <title>DIY overengineered fridge/freezer monitor</title>
       <link>https://pdx.su/blog/2025-05-10-diy-overengineered-fridge/freezer-monitor</link>
       <pubDate>Sat, 10 May 2025 10:39:06 -06</pubDate>
       <guid>https://pdx.su/blog/2025-05-10-diy-overengineered-fridge/freezer-monitor</guid>
       <description><![CDATA[ <h1><a href="#diy-overengineered-fridgefreezer-monitor" aria-hidden="true" class="anchor" id="diy-overengineered-fridgefreezer-monitor"></a>DIY overengineered fridge/freezer monitor</h1>
<p>I've got a chest freezer that I like to keep an eye on. It's never failed me, but it has several thousand dollars worth of foods and meats in it, and sits out of the way, so it's not something you check every day. If it did fail, it could be catastrophic. Since I have Home Automation, this was entirely unacceptable. So I set out to monitor it.</p>
<p><img src="/.netlify/images?url=/postimages/2025-05-10-diy-overengineered-fridge-freezer-monitor/PXL_20240416_190841246.jpg" alt="Fridge/freezer monitor setup" /></p>
<h2><a href="#state-of-the-market-2024" aria-hidden="true" class="anchor" id="state-of-the-market-2024"></a>State of the market, 2024</h2>
<p>At the time of this project, there were a variety of fridge/freezer products on the market, some of which even had remote monitoring, which was a requirement for this project. But, of the ones I'd found, there weren't any that were HomeAssistant or &quot;DIY-er&quot; friendly.</p>
<p>Both ThermoWorks and FireBoard offer fridge/freezer monitoring tech, and I like products from both companies. But their monitoring tech, at the time, was unsuitable. Both companies products required internet connections; there was no LAN capability, and both, being commercial oriented, were not meant for 24/7 monitoring<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup></p>
<p>Looking outside them for a while, a common approach many people seemed to take was to shove a simple Zigbee temperature probe, the kind you'd leave outside, in their fridge. This, of course, barely works in a small apartment, and in my big ol' house, wouldn't work at all. The signal attenuation through the walls of a big chest freezer alone means you have to put a repeater practically on the other side of the walls, and the cold environment isn't kind to cheap batteries. I probably could have made it work, but it seemed unreasonably complex.</p>
<h2><a href="#when-they-wont-build-it-you-have-to" aria-hidden="true" class="anchor" id="when-they-wont-build-it-you-have-to"></a>When they won't build it, you have to</h2>
<p>Dissatisfied with what I saw on the market, I went about building my own. I laid down some requirements first:</p>
<ul>
<li>Must be local only</li>
<li>Must tie into HomeAssistant</li>
<li>Must have replaceable and removable remote probes, ideally K-Type thermocouples.</li>
</ul>
<p>With these in mind, the obvious candidate was to use an ESP32 device with <a href="https://esphome.io/">ESPHome</a>. At the time, the only ESPHome project I'd done was the <a href="https://pdx.su/blog/2024-01-19-fixing-a-broken-smart-cat-feeder-with-esp32/">cat feeder</a>, which was mostly just a bit of GPIO and some timing. This would be more complicated.</p>
<p>The ESPHome platform supports use of external modules to translate the weak signals from a K-Type thermocouple to something the ESP32 can use (i.e. a temperature). The module I wound up using was the MAX31855-based module, specifically <a href="https://www.adafruit.com/product/269">this</a> unit off Adafruit. <a href="https://esphome.io/components/sensor/max31855">ESPHome supports this natively</a>, and so it was a shoo-in.</p>
<p>Wanting to <em>also</em> monitor the conditions of my Garage, I grabbed a <a href="https://www.adafruit.com/product/5181">AM2301B ATH based Temp/Humidity sensor</a>. These are supported in ESPHome via the <a href="https://esphome.io/components/sensor/aht10">AHT10</a> module.</p>
<p><img src="/.netlify/images?url=/postimages/2025-05-10-diy-overengineered-fridge-freezer-monitor/PXL_20240416_190826636.jpg" alt="The thermoworks food simulant probe" /></p>
<p>Finally, I needed the probe itself. ThermoWorks sells a probe called a <a href="https://www.thermoworks.com/ths-113-350/">food simulant probe</a>, which is exactly what it sounds like. It's a square block of plastic, with a probe embedded in the middle. You stick it in your fridge/freezer, and instead of just getting the point reading of metal probe, you get a &quot;diffuse&quot; reading, what a piece of food in the freezer would actually measure as a temp.</p>
<p>Other odds and ends, of minimal importance to the project, were the power supply, which was just a cheap multi-voltage one I picked up at radioshack, <a href="https://www.adafruit.com/product/3931">the enclosure</a>, and a <a href="https://amzn.to/3RXuEKQ">K-Type extension cord</a>, which was used as the &quot;terminal&quot; for the connection to the PCB</p>
<h2><a href="#assembly-and-testing" aria-hidden="true" class="anchor" id="assembly-and-testing"></a>Assembly and testing</h2>
<p>Assembly was extremely straightforwards, basically electronics lego. Just solder some headers on a protoboard, solder some headers on the MAX31855 and ESP32 (mine came without them), solder leads from the power cord to the VCC and GND of the protoboard, and then solder the leads from the AHT20 to the protoboard. The K-Type extension cord got its male end terminal removed, exposing the bare wires, which happily set into the screw terminals on the MAX31855.</p>
<p>Once it's all assembled, take your ESP32 over to your computer, flash it with the loader, pair it with HomeAssistant, and write a <a href="https://github.com/paradox460/HomeAssistantConfig/blob/main/esphome/freezer-monitor-95cc6c.yaml">short bit of ESPHome yaml</a> to configure it. Then mount it on your protoboard, and close up your case.</p>
<p>I glued the AHT module to the side of the case, and put some magnets on the back, so I could stick it to the side of my freezer.</p>
<p>I'd previously stuck the food simulant probe in my chest freezer, and fished the cord out through the back, so it wouldn't interfere with normal operation of the lid. I'd let it sit for 24 hours, so it was good and cold, and measuring it with a normal, non-connected thermometer reader (A ThermoWorks ThermaQ, in this case), saw my freezer was hovering around -1ºF, where I like to keep it. Disconnecting from the ThermaQ, and connecting to the extension that connects back to the MAX, I <em>should</em> have been good to go.</p>
<h2><a href="#calibration" aria-hidden="true" class="anchor" id="calibration"></a>Calibration</h2>
<p>Unfortunately, the temps I was seeing in my HomeAssistant were wrong. But they were <em>consistently</em> wrong at the temperature ranges I wanted to read. I didn't need to calibrate for a large set of different temperatures, my probe was likely to only be reading temps around -5ºF to +5ºF, and so testing and finding the inaccuracies at this range is a lot easier.</p>
<p>I used a K-Type thermocouple simulator I spent far too much money on to generate temps around this level, and noted the difference that HA read from what the sim was generating.</p>
<p>Once I'd measured a decent enough cloud of points, I took the average of the offsets, which turned out to be -1º. Fortuitous.</p>
<p>ESPHome has a few different ways to calibrate a device. All the calibration systems use the filters system, which is a way to change or modify how a sensor in ESPHome works. There are linear calibrations, logarithmic calibrations, and more. They all take some data points, and use them to shift the signal accordingly. For my use case, since I was really just happy shifting the temp by a degree, I could just use a <a href="https://esphome.io/components/sensor/#offset">simple <code>offset</code> filter</a>.</p>
<p>For <em>all</em> the sensors according to this board, I didn't want them to update too frequently, as you'd get noise on the graph. I had the update intervals set to 10 and 20 seconds, for various sensors, but even thats a little more frequent than I cared about. One nice way to smooth out a signal, and get accurate readings, is to use an exponential moving average, which <a href="https://esphome.io/components/sensor/#exponential-moving-average">ESPHome provides as a nice filter</a>! Installing it on each module was simple, and I set it to basically average all the reads from the sensor over a minute. I had to tune the alpha factor a bit for each of them, but that was mostly trial-and-error until I got a result that I liked.</p>
<h2><a href="#state-of-the-market-2025" aria-hidden="true" class="anchor" id="state-of-the-market-2025"></a>State of the market 2025</h2>
<p>Since building this, there are a few new products on the market that might be interesting to people wanting to do the same thing, without going the DIY approach. Sonoff has come out with a Zigbee based temperature probe, the <del><a href="https://amzn.to/43fhZbz">SNZB-02LD</a></del><a href="https://sonoff.tech/en-us/products/sonoff-snzb-02ld-ip65-zigbee-lcd-smart-thermometer-probe-version">SNZB-02LD</a><sup class="footnote-ref"><a href="#fn-2" id="fnref-2" data-footnote-ref>2</a></sup> that has a <em>remote</em> probe. You'd stick the transmitter/screen on the wall or side of your fridge, and the probe inside. No clue how well it works, but it's an option if you want something easy, and already are invested in the Zigbee universe.</p>
<p>You'll want to make sure you get one that has the external probe. The amazon listing seems to be an &quot;updated&quot; use of an old listing, for a probe-less model.</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>Fireboard <em>does</em> offer a 24/7 recording service, but its paid. <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="fn-2">
<p><strong>2025-08-20</strong>: Amazon no longer lists the <code>02LD</code> model, listing only the <code>02D</code> model, which <em>does not</em> have a remote probe. <a href="#fnref-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
    <item>
       <title>Trying, and failing, to build an ESPHome powered irrigation system</title>
       <link>https://pdx.su/blog/2025-03-28-trying-and-failing-to-build-an-esphome-powered-irrigation-system</link>
       <pubDate>Fri, 28 Mar 2025 12:53:45 -06</pubDate>
       <guid>https://pdx.su/blog/2025-03-28-trying-and-failing-to-build-an-esphome-powered-irrigation-system</guid>
       <description><![CDATA[ <h1><a href="#trying-and-failing-to-build-an-esphome-powered-irrigation-system" aria-hidden="true" class="anchor" id="trying-and-failing-to-build-an-esphome-powered-irrigation-system"></a>Trying, and failing, to build an ESPHome powered irrigation system</h1>
<p><img src="/.netlify/images?url=/postimages/2025-03-28-trying-and-failing-to-build-an-esphome-powered-irrigation-system/frontyard.jpg" alt="sprinklers running over green grass" /></p>
<p>I've been enthusiastic about <a href="https://esphome.io">ESPhome</a> for a while now. It seems to be the perfect medium between turn-key home-automation appliances, and 100% DIY stuff. I've used it to measure the temperature of my chest freezer, to <a href="/blog/2024-01-19-fixing-a-broken-smart-cat-feeder-with-esp32/">fix up a cat feeder no longer supported by the manufacturer</a>, and to add an optical rain sensor to my weather station.</p>
<p>So naturally, when reading through the documentation, and coming across the Sprinkler controller, I thought &quot;This would be an excellent project.&quot;</p>
<h2><a href="#enter-rainmachine" aria-hidden="true" class="anchor" id="enter-rainmachine"></a>Enter RainMachine</h2>
<p>Back in 2020, when we purchased this house, I did a bit of research, and came across two real contenders for the irrigation controller I wanted. RainMachine, and <a href="https://opensprinkler.com">OpenSprinkler</a>. Both were aimed more at the diy-ish type, but the RainMachine was glossier than the OpenSprinkler, and seemed the better investment. Their software was slicker, and the controller seemed just smart enough to do what I wanted, but not get in the way.</p>
<p>Others, such as Rachio, or the big names like Orbit's B-Hyve, Rainbird's Wi-Fi enabled one, and offerings from K-Rain and Hunter, just didn't appeal to me. All were either dumb controllers with some smarts crudely bolted on, cloud-locked, or some similar combination of negatives.</p>
<p>I wasn't looking for a tremendous amount of intelligence in my sprinkler controller, rather, I wanted a way to just set the schedule from a website or my phone, have it adjust the watering depending on how much summer rain we get, and allow me to start and stop stations remotely, probably from my phone, so I can work on the sprinklers as problems come up. HomeAssistant integration was an added bonus, but ultimately not something I was terribly concerned with at the time.</p>
<h2><a href="#exit-rainmachine" aria-hidden="true" class="anchor" id="exit-rainmachine"></a>Exit RainMachine</h2>
<p>The RainMachine has served me well for the last 5 years, but over the last few, the company behind it has seemingly been in rather dire straits, and, as far as I can tell, has gone out of business now.</p>
<p>A few years ago, they transitioned their online &quot;cloud&quot; services to a premium plan. The sprinkler controller still works completely fine without their cloud, but the cloud gives you nice things like no-fuss remote access, more weather providers, and (purportedly) priority support. And the price for it wasn't too bad, so I paid for it for a couple of years. I could have had my own remote access working fine; I've got tailscale running on my home network, but if a few bucks a year kept the company going, I was willing to pay for it.</p>
<p>Sadly, it didn't keep them going. Over time, the connections to the controller became more and more unreliable. Sometimes the app would just sit there, loading infinitely, without connecting. It didn't matter if I was connected to the LAN the controller was on, or was remote, it just wouldn't load. Sometimes even attempting to load the (lan IP) website in a browser wouldn't work, requiring a trip to the controller in the garage to reboot it. And even when it did work, you'd randomly get sluggish behavior, such as <em>not</em> turning on a zone when given a command to do so, or worse, not turning off a zone when instructed to.</p>
<p>Finally, in late summer of 2024 (last year), my grass began to die off in places. At first, I dismissed it as the usual hot-summer browning, confident the grass would turn back to green when the temp fell a bit. But one night, up late, I noticed I didn't hear the sprinkler running. Checked the app. Non-responsive. Checked the website. Non-responsive. Went out to the physical controller. Completely locked up. Had to power cycle it, and then I found out that it hadn't watered in <em>a week</em>. It had failed receiving some weather update, and just crashed and not recovered.</p>
<p>I was unable to renew my cloud subscription earlier that summer, and now it appears that the controller wasn't reliable at all.</p>
<p>I could have just taken the device completely offline, firewalled it to not connect to the internet, and pushed weather updates into it from HomeAssistant, but I was already a bit tired of some of its other drawbacks, and so wanted to explore other options.</p>
<h2><a href="#getting-started-with-a-diy-controller" aria-hidden="true" class="anchor" id="getting-started-with-a-diy-controller"></a>Getting started with a DIY controller</h2>
<p><img src="/.netlify/images?url=/postimages/2025-03-28-trying-and-failing-to-build-an-esphome-powered-irrigation-system/esp-controller.jpg" alt="ESPHome-powered irrigation controller" /></p>
<p>I was very interested in making a sprinkler controller with ESPHome. As I mentioned in the opening paragraph, I've used it for a few tasks around the house, and have been rather impressed. And so it seemed like the perfect fit.</p>
<p>I went online and ordered a <a href="https://www.kincony.com/kc868-e16s-hardware-design-details.html">relay board, specifically a KinCony KC868-e16s</a>. This board has an ESP32, 16 relays, and some IO multiplexers to allow easy control of all the relays and inputs. It seemed like a perfect fit for an irrigation controller; just wire up 24VAC to the relays, connect each station to the other side of the NO contact, and then connect the stations common wire to the other terminal of the transformer. Presto, nice simple irrigation controller.</p>
<p>Software wasn't too much harder to connect up. The <a href="https://devices.esphome.io/devices/KinCony-KC868-E16S">ESPHome Devices</a> page has a nice sample of what a basic configuration for this board would look like, and so I just started with it, editing the parts I needed. Initial testing was promising; I was able to switch every relay on and off from HomeAssistant, trigger the piezoelectric buzzer, and listen to inputs on the various input pins.</p>
<p>Basic relay clicking working, I set up the <a href="https://esphome.io/components/sprinkler">sprinkler</a> component, and got the various stations set up. I wanted to have two different &quot;controllers&quot;, one that would run my high-pressure rotor sprinklers, and one that would run the low pressure drip lines. One of the more disappointing parts of my RainMachine was that it had a singular master controller, so if you wanted to control a pump AND a master valve, it was all or nothing. For the low pressure drip lines, the supply water pressure would be strong enough to give them what they need, and the boost pump was unnecessary. With the rain machine, I had to install pressure regulators at the head of each dripline, just to avoid blowing out the drip plugs.</p>
<p>Depending on how you set up your controllers, you can have as many pumps, master valves, whatever, on any zone, and there's not actually a need to split them up into separate controllers. As the system progresses through a &quot;program&quot;, it will turn on and off the various pumps and valves as needed. However, I wanted to have different run cycles for these, as well as running multiple short drip lines at once, to cut down cycle time, and so separate controllers made the most sense for me.</p>
<p>After a bit of YAML, I had what I thought would be a passible configuration. Flashing the device, I opened up HomeAssistant to test, and started a program that would run through each station, 5 seconds per station. Station 1 and 2 fired off just fine, but when it came time for Station 3, the &quot;pump&quot; relay switched off, then the station switched off, while leaving the master valve on, and then the whole thing rebooted.</p>
<p>I fiddled with my configuration a few times, figuring one of the proxy switches I'd made to turn on/off both the pump and master valve from a single switch was the culprit. But even removing it and going down to a single, physical master, I was still having the reboot issues.</p>
<p>Connecting up a serial terminal, so I could actually watch the logs, not whatever the ESPHome sent over the network, I tried to run another cycle, and saw something that made my heart sink. Every time it got to the third station, it had a panic, and rebooted.</p>
<p>I'm not tremendously well versed in embedded software, and so was well out of my depth here. So I collected some logs, opened an <a href="https://github.com/esphome/issues/issues/6872">issue</a> with what I'd found, and essentially consigned the controller to the &quot;future project&quot; parts bin.</p>
<p>I could have removed the sprinkler component, and just used a bunch of switches to control the zones. There are <a href="https://github.com/rgc99/irrigation_unlimited">homeassistant addons</a> that can automate sprinkler systems that are little more than collections of switches. But the itch at the back of my head was saying &quot;this is becoming a fractal of complexity&quot;.</p>
<p>I want my irritation system to be something that I can tinker with when I want to, but will more or less work unless I actively break it. And tying the whole thing to HomeAssistant seems to be introducing an unnecessary point of failure. I like using HomeAssistant to <em>augment</em> systems that already exist, to make them better. Any time I have absolute reliance on it, I feel a little uneasy. It hasn't let me down in <em>years</em>, but I started HomeAutomation in the dark ages, when things were extremely fragile, and so I remain wary of those.</p>
<h2><a href="#opensprinkler" aria-hidden="true" class="anchor" id="opensprinkler"></a>OpenSprinkler</h2>
<p>Since my RainMachine was essentially non-viable at this point<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup>, and I needed a new controller, I looked to <a href="https://opensprinkler.com">OpenSprinkler</a>. They offer a few different models, but for my needs the basic, 24VAC one, without latching DC solenoids, was what I needed. Since I've got 14 zones, I also needed to buy one of the expansion boards. The base OpenSprinkler controller only has 8 controllable zones, and they solve this limitation by selling expansion boards; little boxes that just have more zone terminals on them, that connect up to the main controller via a short ribbon cable and use I2C.</p>
<p>OpenSprinkler lets you do <em>nearly everything</em> I've wanted with my irrigation systems. It lets you have 2 &quot;master&quot; controllers, I'm using one for a master valve and one for a pump. Each zone can use one, both, or neither master control. It lets you group your zones into 5 &quot;groups&quot; for exclusivity control; all the zones in one group run sequentially, but you can run zones from different groups in parallel. There are 4 main groups and one special, &quot;parallel&quot; group, in which all zones can run parallel to any other zones, including others in the parallel group.</p>
<p>I was able to quickly get my zones set up, a few basic watering programs set, and so far have yet to encounter any actual limitations that matter to me.</p>
<p>There is a <a href="https://github.com/vinteo/hass-opensprinkler">3rd party homeassistant integration</a>, which I'm running; it brings a ton of control into HomeAssistant, which is useful for a few of the programs I've used in the past. During the summer, I have a little automation set that runs a few zones for a short period of time, 5 minutes or so, when the outdoor temperature gets above 90º. This gives my chickens a bit of mud to go sit in and cool off, and seems to keep them pretty happy. The HomeAssistant integration allows me to fire off individual zones OR a program, using the Actions system, so I've got some flexibility on how I approach it.</p>
<p>The OpenSprinkler system isn't without its flaws, and some of them are rather notable to me. But none of them are dealbreakers, or even really things that prevent me from doing what I want to do, they just make a few things more complex than I feel they should be.</p>
<p>The UI isn't particularly nice to look at. Its serviceable, and by no means non-functional, but compared to the gloss of something like the RainMachine, it's not as good. That said, it <em>actually works</em> consistently, and fast, which isn't something I can say for the RainMachine. Taking the theme of UI considerations, there's a bit of an inversion when it comes to splitting up a run cycle<sup class="footnote-ref"><a href="#fn-2" id="fnref-2" data-footnote-ref>2</a></sup>. With both the RainMachine and the ESPHome sprinkler component, you would set your <em>total</em> run time for a zone in a program, and then use a <em>divider</em> to choose how many cycles there were. A zone set to run for 30 minutes, with a divider of 2, would run for 15 minutes in each cycle, with the cycles typically being back to back. OpenSprinkler does the inverse of this, they use a <em>multiplier</em> approach. You set the run time of each zone, and then set a cycle count and delay time. Frustratingly enough, the delay time lacks a &quot;resume after the first cycle has completed&quot; feature. This isn't an issue if you have your zones set to the same exclusivity group, their run times will just queue up, but if you make heavy use of parallel groups, and allow for weather influenced changes to cycle times, you could get overlap</p>
<p>The weather system for OpenSprinkler is rather powerful, but getting your own weather sources, such as my own personal weather station, into the system isn't terribly convenient. You need to run a weather data provider program on your own server, and use that to push data into the controller. OpenSprinkler provides one that uses several online data sources, and swapping over to your own isn't terribly hard, but compared to the RainMachine, which allows for a simple HTTP push of weather data, which can be triggered by things such as WeeWX <em>or</em> HomeAssistant, it does stick out.</p>
<p>Fortunately, all the weather system is used for is doing weather level adjustments. You can actually set these to completely manual adjustments, and then use something like the <a href="https://jeroenterheerdt.github.io/HAsmartirrigation/">smart-irrigation hacs</a> package to calculate and push adjustments from HomeAssistant.</p>
<p>But other than those little bits of discomfort, I'm overwhelmingly impressed by OpenSprinkler. Let's hope I stay impressed throughout the year.</p>
<p>And just because I had a bad experience with a particular ESPHome system, doesn't mean I'm swearing off it either. I've got another project coming along soon, and when finished I'll get a blog post about it up.</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>I could have kept using it, as the failure case appeared to be a one-off. But I still didn't feel comfortable with it after that <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="fn-2">
<p>You can increase water retention in soil by spreading out the amount of water you put on it over a short period of time. Instead of dumping all the water for a zone run in a single pass, you split it up, allowing the ground to rest and absorb some water. This prevents run-off, and generally leads to more efficient use of water. <a href="#fnref-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
    <item>
       <title>Co-op campaigns are a rarity these days, and that should change</title>
       <link>https://pdx.su/blog/2025-01-31-co-op-campaigns-are-a-rarity-these-days-and-that-should-change</link>
       <pubDate>Fri, 31 Jan 2025 20:27:05 -07</pubDate>
       <guid>https://pdx.su/blog/2025-01-31-co-op-campaigns-are-a-rarity-these-days-and-that-should-change</guid>
       <description><![CDATA[ <h1><a href="#co-op-campaigns-are-a-rarity-these-days-and-that-should-change" aria-hidden="true" class="anchor" id="co-op-campaigns-are-a-rarity-these-days-and-that-should-change"></a>Co-op campaigns are a rarity these days, and that should change</h1>
<p>As I've aged, my interest in <em>competitive</em> multiplayer games has waned. I find that I have no interest in fighting other players online, that I don't really care about the score I get, and that having those things present in a game tends to pair me up with players I'd rather not play with. At the same time, my interest in PvE and Co-Op games has <em>grown</em> substantially. While recent games have been rather good on the PvE front, they're lacking in co-op campaigns. And I wish that wasn't the case. Many modern games feature stories that lend themselves particularly well to having a &quot;squad&quot; of Protagonists; rarely is your character alone in any of the missions. Yet, nearly all of these games won't allow you to replace one of your AI squadmates with a real flesh and blood human.</p>
<h2><a href="#co-op-is-a-rather-recent-invention" aria-hidden="true" class="anchor" id="co-op-is-a-rather-recent-invention"></a>Co-op is a rather recent invention</h2>
<p>We can call this the &quot;history&quot; section of the article. Back in the sixth generation of game consoles (Xbox, PS2, GameCube) was really when we saw the beginnings of true co-op, and they were all offline affairs. I wasn't aware of any games having meaningful online co-op at the time, and didn't play them. But this wasn't a terribly big deal, <em>online</em> play was almost exclusively for things like deathmatch, or MMOs like EverQuest. And even then, online play was generally the domain of PC games, not consoles.<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup></p>
<p>What was rather common on games of this era was <em>split-screen</em>, both for competitive multiplayer, and for co-op. Halo: CE shipped with split-screen co-op, and I spent many a night replaying the same levels, over and over again, with my friends, clustered around a tiny 13-inch Emerson TV. Yeah, the split screen co-op of these early games was generally limited to two players, but it was something to do together, a nice pace change from the 4 player deathmatch games.</p>
<p>This, and the subsequent generation of game consoles, really is the heyday of co-op. We saw entire games spend a substantial amount of development time figuring out how to make the game more co-op friendly. Games like Gears of War designed entire levels around having multiple people playing them together, with AI following a scripted path for single play.</p>
<p>In this generation, we also saw the rise of online co-op. Gears of War, as far as I remember, had online co-op from the get-go. Halo 3, which came out a year after Gears of War, had 4 player online co-op, but only 2 player local.<sup class="footnote-ref"><a href="#fn-2" id="fnref-2" data-footnote-ref>2</a></sup> You could play your favorite games with your friends, working together to beat a difficult level, playing along with someone on their first playthrough, or just goofing off and trying to break the game in ways that the developers never accounted for.</p>
<p>Things were good for a while, but around the time the 8th generation arrived, online co-op began to fade. Games like Dead Space 3 were extensively built around co-op gameplay, and critics felt the single player experience suffered because of this. By the time the ninth generation (the current one) arrived, online co-op was a rarity, and often delivered well after the game launched, as was the case with Halo: Infinite. Couch co-op is almost completely dead, with rare exceptions being more &quot;casual&quot; oriented games, such as Untitled Goose Game.</p>
<h2><a href="#why-cant-my-buddy-be-my-squad-mate" aria-hidden="true" class="anchor" id="why-cant-my-buddy-be-my-squad-mate"></a>Why can't my buddy be my squad mate?</h2>
<p>Since modern game AI has improved tremendously, we're seeing many modern games move towards having a &quot;squad,&quot; one member being the player's character. These squad mates exist to help drive the story, provide guard-rails to assist players who might not know what they're doing, and make the game feel more &quot;alive.&quot;</p>
<p>In almost every case, you can't replace the AI squadmate with your buddy. Games like Call of Duty: Black Ops 6 feature expansive campaigns, where your character is generally paired up with a partner, at the very least. Driving around desert-storm era Iraq in a humvee full of soldiers, I wondered why my buddy couldn't step in behind the eyes of one of them, and play a bit of the campaign with me.</p>
<p>Other games, such as S.T.A.L.K.E.R. 2, or Starfield, are much more &quot;single player&quot; oriented, but still feel as if co-op would dramatically improve them. Starfield has the player running about with companions, unless they choose to forgo them, so why can't I run around with my friends? Stalker 2, you are a &quot;loner&quot; yes, but walk anywhere in the game and you're bound to encounter small squads of NPCs, two to four of them, moving through the zone, same as you do. This would be great if your buddy could drop into your game and squad up with you.</p>
<h2><a href="#arguments-against-co-op" aria-hidden="true" class="anchor" id="arguments-against-co-op"></a>Arguments against co-op</h2>
<p>A common argument behind co-op is that it's technically demanding. While it does come with its own set of headaches, yes, its more or less a solved problem in most game engines nowadays. If you use Unreal, there are primitives ready to go for online co-op, that handle synchronization of data and events between participants. If you use other engines, these primitives likely exist as well. If you're worried about players venturing too far apart and causing too much of the game world to be loaded into memory, there are solutions to that problem as well, some of which are 24 years old. You can put a leash between players, so they don't have the ability to wander too far apart, or choose &quot;softer&quot; options, like the teleporting Halo has done since Halo: CE (with the exception of Halo: Infinite).</p>
<p>Another argument I've heard is that the game would become unbalanced should you allow players to team up. This is an argument entirely without merit. If the players want to team up to beat something too difficult for one or both of them, that's their prerogative. Make the game too hard, and players will drop off and lose interest, regardless of how much trash-talk and &quot;get gud&quot; crap one might see on message boards. Modern games have received criticism of being &quot;too easy,&quot; but they almost all feature a difficulty slider, and accessibility features that allow a wider range of players to enjoy the game. And even still, ignoring these factors, it's a simple matter of just scaling internal difficulty systems up should you toss in more players. Make encounters longer, spawn more enemies, spawn higher health enemies, etc.</p>
<p>Finally, there <em>are</em> games where co-op doesn't make sense. The recent Indiana Jones video game (Indiana Jones and the Great Circle) wouldn't make sense to have two Indies cavorting around, and the previous attempts of the franchise to introduce sidekicks (Crystal Skull) have fallen flat.</p>
<h2><a href="#closing" aria-hidden="true" class="anchor" id="closing"></a>Closing</h2>
<p>We have these amazing single player experiences that almost scream to be enjoyed co-operatively, with squad based mechanics or amazingly stunning gameplay. Yet they very rarely are. I would love to run through the zone in STALKER 2, trading artifacts with my buddy, or setting up complex ambushes of enemies, but I can't. I would love to have my friend explore remote planets in Starfield with me, but I can't. I would love to have a fellow bandit ride along with me in Red Dead Redemption 2, but I can't<sup class="footnote-ref"><a href="#fn-rdr" id="fnref-rdr" data-footnote-ref>3</a></sup></p>
<p>I've some ideas what a &quot;proper&quot; online co-op experience should have, but at this point I'll take whatever I can get, since its so rare. But I'd love to see the following features:</p>
<ul>
<li><strong>Drop-in-drop-out co-op.</strong> Since it's a more &quot;casual&quot; experience, and most of my online game friends are adults as well, we can't always carve out large blocks of time. As such, I should be able to join in my friends game at any point, help them along, and then leave whenever I have to go.</li>
<li><strong>An inventory that follows me.</strong> If I find rare artifacts while playing in my friend's game, those shouldn't just be forfeit because I wasn't host. Similarly, if I have something rather powerful that could help them out, or multiple of said something, I should be able to bring it with me, and use it or give it to them.</li>
<li><strong>Instanced loot.</strong> When you're fighting some boss, or looking for loot, or whatever else, and you find 1 really high tier item, it becomes somewhat of an issue of <em>who</em> gets it. Does the host get priority? What if they're the better player? Does the guest? What if they already have it in their play? The best solution to this is instancing the loot, so <em>each</em> player has the option to grab it.</li>
<li><strong>Progression tracking.</strong> If I finish missions in a friend's game that I haven't finished in mine, I should still be able to take credit for them. With more linear campaigns this can become an issue, so it's probably the weakest of the options here, but it's nice for the type of game that can use it</li>
<li><strong>Separate difficulty sliders for each player.</strong> Gears 5 does this particularly well, and older entities have had it as well. Each player in the lobby can choose what difficulty they are playing on. So if you're a seasoned player, you can dial up the difficulty, and vice-versa.</li>
</ul>
<p>For some games, some or even all of these suggestions won't work. Some games that are far more story driven allow you to choose a save slot as your gameplay, and save any progress you make to that save slot. If the save slot is at a certain point, or you start the game from a particular level, then that can be saved to the slot.</p>
<p>But as I said earlier, this is a wishlist. I'd settle for <em>just</em> having some co-op at all.</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>Xbox Live launched in 2002, but didn't really find its legs till Halo 2 added many &quot;standard&quot; QoL features in 2004. Early XBL enabled games, like MechAssault, were <em>rough</em>. <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="fn-2">
<p>The three player glitch notwithstanding. <a href="#fnref-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
</li>
<li id="fn-rdr">
<p>Red Dead Redemption 2 <em>did</em> have an Online play mode, but it's a rather different experience than the campaign, and is poisoned by its heavy reliance on microtransactions and inability to make &quot;private&quot; lobbies with just you and people you wish to play with. <a href="#fnref-rdr" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="3" aria-label="Back to reference 3">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
    <item>
       <title>Using Kagi for a bit over a year now, here&#39;s what I think</title>
       <link>https://pdx.su/blog/2024-12-29-using-kagi-for-a-bit-over-a-year-now-heres-what-i-think</link>
       <pubDate>Sun, 29 Dec 2024 16:00:46 -07</pubDate>
       <guid>https://pdx.su/blog/2024-12-29-using-kagi-for-a-bit-over-a-year-now-heres-what-i-think</guid>
       <description><![CDATA[ <h1><a href="#using-kagi-for-a-bit-over-a-year-now-heres-what-i-think" aria-hidden="true" class="anchor" id="using-kagi-for-a-bit-over-a-year-now-heres-what-i-think"></a>Using Kagi for a bit over a year now, here's what I think</h1>
<p>I've been using the <a href="https://kagi.com/">Kagi</a> search engine for a bit over a year now, and while it's not fundamentally changed my life, it has made me reflect and think about some things, mainly where the internet has gone, where it was, and what we've lost along the way. It's safe to say that most of us spend most of our waking hours doing <em>something</em> with the internet. It's in our pockets, on our wrists, in our ears, in front of our eyes, and sometimes, quaintly enough, on our computers. Yet, as many people have noticed, the internet has gotten really irritating to use over the last few years.<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup></p>
<p>Part of what makes it immensely irritating is that the tools we used to rely on for navigation through this wilderness, the search engines and content crawlers, have become actively hostile towards us as users. Google no longer tries to show you what you're looking for, instead it tries to show you things that it wants you to see. And what Google wants you to see is ads. Additionally, Google is under the somewhat perverse incentive <em>not</em> to give you what you want, as the more time you spend searching, the more ads you see. Couple that with the amount of shit they shove at the top of a search result page, and it's a miserable experience.</p>
<p>Gone are useful tools like the older knowledge graph; it's been replaced by a cyborg facsimile, an AI box that hallucinates its own insane, twisted reality. From things like telling people to put glue on pizza to eating rocks to just flat out inventing things that don't exist.</p>
<p>And it's been trending downwards for a while now, seemingly gaining steam and becoming shittier as time goes on.</p>
<h2><a href="#enter-kagi" aria-hidden="true" class="anchor" id="enter-kagi"></a>Enter Kagi</h2>
<p>Mid-2023 I started hearing some interesting things about a new search engine. Comments on HackerNews, reddit, and in some private chatrooms, were talking about a new search engine, called <a href="https://kagi.com/">Kagi</a>. At first, I ignored these discussions, figuring it was another flash-in-the-pan startup (remember <a href="https://en.wikipedia.org/wiki/Cuil?useskin=vector">Cuil</a>?). But over the summer of 2023, the talk about this tool didn't wane, as usually happens with these things, but rather intensified.</p>
<p>Finally, in September, I'd repeatedly heard enough good things from enough different sources, I decided to check it out. At first, I wasn't terribly impressed. A paid search engine? Come on. Who the hell is going to pay for search? Yeah, Google isn't great, but Bing is marginally better, and Bing <em>pays you to use it</em><sup class="footnote-ref"><a href="#fn-2" id="fnref-2" data-footnote-ref>2</a></sup>. But Kagi, and commenters across the internet, implored me to try it, to use the 300 free searches, and see what it was all about. If I didn't like it after the first 300, I could go back and not pay a cent.</p>
<p>So I tried it. And I loved it. At first, I just treated it like another search engine. I put in my query, got back results. Boring, right? Well, yeah, boring by 2007 standards. But by 2023 standards? The results were <em>actually what I wanted</em>. And things that Google and others long since cast aside, like boolean operators? Available, in all their glory. I found myself doing something I hadn't done in nearly a decade: aimlessly browsing the web, finding new and exciting pages, written by small bloggers, authors, whatever; people who were passionate about their work, and not underwritten by some faceless content mill.</p>
<p>I burned through my 300 searches in about a week. The moment I hit my limit, I jumped and bought one of their ultimate plans, for a year. I have no regrets on that purchase. Looking at my usage page, I average about 950 searches a month, with some outliers being as low as 400 a month or as high as 2000 a month. And this doesn't include searches that use DDG style <code>!bang</code>s, which Kagi not only supports, but lets you add your own custom ones<sup class="footnote-ref"><a href="#fn-3" id="fnref-3" data-footnote-ref>3</a></sup>. Yeah, you can add custom search engines to most browsers worth their salt these days, but the bangs somehow feel more useful. Maybe It's that you can shove em at the end of a search to change what the search is doing.</p>
<p>Over the year, my usage of Kagi changed as well. At some point, I discovered the <code>?</code> bang, which will force Kagi to generate an AI chatbot answer at the top of your search results. At first, when I read about this, I was dismissive. The Google and Bing versions of this weren't terribly useful, often providing worse than wrong answers, but I went in open-minded, and was impressed. While I don't use it all the time, It's nice to be able to search something like <code>When did Duke Ellington die ?</code> or <code>Is Atomic Heart on Xbox Gamepass?</code> and get the answer right there, complete with citations to check its work.</p>
<p>I still only use the AI chat feature sparingly. Kagi gives you access to most of the big models, through their own interface, which is useful when trying to research something, prototype a bit of code, or whatever else, but I still find them only about as reliable as a precocious teenager, happy to make up things to make themselves sound smarter than they actually are. I do enjoy that I have them, in a nice interface, that lets me run the same query against different models and compare the results. I do enjoy that, being run through Kagi, instead of a user account associated with me, they're effectively washed in a big stew of all the other AI queries other Kagi users are making, bringing about a thin layer of anonymity. But ultimately I don't see them as being hugely useful. A novelty, for sure, that comes in handy, but if it was turned off tomorrow I probably wouldn't miss it.</p>
<p>And you might be asking, in that whole year (and then some) of using Kagi, how often was it <em>unable</em> to find what you're looking for. And the answer is a bit more complicated than it should be. Most of the time, if I couldn't find it on Kagi, it was a flaw in my search terms, that more refinement was able to sort out. Every time Kagi couldn't find something, using another search engine proved equally fruitless. Since Google and Bing are a single bang away, if something wouldn't turn up, I'd try them, but ultimately stopped doing so, as they never returned what I needed.</p>
<p>The two areas where Kagi is weak, and where I <em>do</em> shell out to other engines for more information, are local (maps) and images. Google Maps is a juggernaut in the local and maps space, and is extremely hard to dethrone. I like that Kagi lets me use Apple Maps and OpenStreetMap<sup class="footnote-ref"><a href="#fn-4" id="fnref-4" data-footnote-ref>4</a></sup>, and give it a good college try every time I need to find something and have a bit of time to suss through the results and find what I want. But if I need something <em>now</em>, like closing times for a restaurant, I just immediately append the <code>!gm</code> bang to search in Google Maps.</p>
<p>Image search in Kagi is <em>good</em>, but not great. Image search seems to be rather tricky to conduct properly, as <em>no engine</em> gets it right entirely. Depending on context, I typically find myself using Bing image search or Yandex Image search. Google Images <em>used</em> to be good, but ever since Google got a bug up their ass about content in their search results, it's been rather useless.<sup class="footnote-ref"><a href="#fn-i" id="fnref-i" data-footnote-ref>5</a></sup></p>
<p>And that leads to what was ultimately the most revealing revelation about Kagi, and how it changed how I search the web. Google is <em>all too happy</em> to color their results to align with their particular corporate biases. If you are in agreement with the bland &quot;safe&quot; Bay Area values that Google's corporate structure holds, you probably won't notice this, but the moment you step out of line, on any subject, you'll suddenly find that Google won't show you what you're looking for. Inconvenient facts are buried in a manner that Orwell would dismiss as too heavy handed to be believable. Search results and suggestions will serve to nudge you back onto the &quot;safe&quot; path, and when that nudging fails, just refuse to show you what you're asking, instead just vomiting out SEO garbage or counterpoints that align with the Google values.</p>
<p>Kagi doesn't do that. If you search for disgusting content, and turn off safe search, you get what you ask for. It treats you like the adult using the product that you are, not like some petulant child asking inconvenient questions. And once you get used to the freedom of <em>having a search engine respect your wishes</em>, returning to the same old is an exercise in frustration.</p>
<hr />
<p>Anyway, that's my pro-Kagi rant for the end of the year. If you haven't tried it, I'd encourage you to try it. Kagi doesn't do affiliates or referrals, so I gain nothing either way, but I want to see them succeed, so they can stay in business and keep making the best search engine I've used in the last decade.</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>Ed Zitron covered it <em>far better</em> than I could hope to say in his article <a href="https://www.wheresyoured.at/never-forgive-them/">Never Forgive Them</a> <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="fn-2">
<p>Bing &quot;pays&quot; you through Microsoft reward points, which accrue slowly but steadily. With somewhat regular usage, I was able to get Xbox Gamepass for free every month. <a href="#fnref-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
</li>
<li id="fn-3">
<p>I've got custom bangs for things like HomeAssistant integrations, 3D models for printing, fish shell commands, and more. Things I look up somewhat frequently. <a href="#fnref-3" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="3" aria-label="Back to reference 3">↩</a></p>
</li>
<li id="fn-4">
<p>This isn't really Kagi, Apple, or OpenStreetMap's fault. I really respect what each organization is trying to do, notably the latter most. OSM is an amazing project, and I've contributed a small number of edits and waypoints to it, but the task is absurdly huge, and Google has a massive first-mover advantage in the space. <a href="#fnref-4" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="4" aria-label="Back to reference 4">↩</a></p>
</li>
<li id="fn-i">
<p>I'm hoping that computer vision and the other associated &quot;AI&quot; technologies can really make image search better, but unfortunately what seems to be happening is that instead of finding the image that is what you want, they're just going to generate an ugly, shitty knockoff of said image, and tell you that there were always 5 lights. <a href="#fnref-i" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="5" aria-label="Back to reference 5">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
    <item>
       <title>Integrating old GE Interlogix Burglar Alarm sensors into HomeAssistant with SDR</title>
       <link>https://pdx.su/blog/2024-10-20-integrating-old-ge-interlogix-burglar-alarm-sensors-into-homeassistant-with-sdr</link>
       <pubDate>Sun, 20 Oct 2024 13:03:12 -06</pubDate>
       <guid>https://pdx.su/blog/2024-10-20-integrating-old-ge-interlogix-burglar-alarm-sensors-into-homeassistant-with-sdr</guid>
       <description><![CDATA[ <h1><a href="#integrating-old-ge-interlogix-burglar-alarm-sensors-into-homeassistant-with-sdr" aria-hidden="true" class="anchor" id="integrating-old-ge-interlogix-burglar-alarm-sensors-into-homeassistant-with-sdr"></a>Integrating old GE Interlogix Burglar Alarm sensors into HomeAssistant with SDR</h1>
<p>Previously, when I was setting up <a href="/blog/2024-03-17-reading-my-electric-meter-with-rtlsdr/">SDR to read my utility meters</a>, while doing research into various tools for reading data with an SDR, I stumbled across a description of some home burglar alarm systems, and how they send data in an easily readable format. This piqued my interest, but I ultimately ignored it at the time, as I was already down a rabbit hole with the utility meters, and wanted to finish that project first. Now, its several months later, and I dove back down that rabbit hole, and within a few hours was able to get a system working quite well.</p>
<p>My house was built in the mid-90s, when Home Automation meant a mixture of an intercom system, burglar alarm, and maybe some X10 stuff (I didn't find any of that, sadly). State-of-the-art burglar alarm systems at the time used cheap little radio transmitters, to avoid the need of running wires all over a house.<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup> These devices typically take the form of a little box next to a door or window, with a reed switch that is triggered by a magnet attached to the door or window. And my house is chock-full of them.</p>
<p><img src="/.netlify/images?url=/postimages/2024-10-20-integrating-old-ge-interlogix-burglar-alarm-sensors-into-homeassistant-with-sdr/PXL_20241020_173540372.jpg" alt="This unobtrusive little box contains a door sensor" /></p>
<p><img src="/.netlify/images?url=/postimages/2024-10-20-integrating-old-ge-interlogix-burglar-alarm-sensors-into-homeassistant-with-sdr/PXL_20241020_182129577.jpg" alt="Cover off, we can see it's a fairly simple little PCB" /></p>
<p>However, looking at these sensors from the outside, they are somewhat hard to decipher. Although their immediate function is readily clear to most people who would care about this sort of thing, how to get the information out of them is somewhat more obscure. Fortunately, smart people on the internet have <a href="https://github.com/merbanan/rtl_433/blob/master/src/devices/interlogix.c">documented the messages these little boxes put out</a>, and written tools that can decipher them.</p>
<p>One such tool is the excellent <a href="https://github.com/merbanan/rtl_433/">rtl_433</a>. This tool is a little Swiss army knife of SDR. You can use it for all sorts of things, like reading cheap wireless temperature sensors, using cheap keyfob remotes for input devices, and more. You'll need an SDR, and the <a href="https://amzn.to/3Q4y8KZ">RTLSDR</a> I used before sadly won't cut it this time. These sensors transmit at 319.5MHz, and the <a href="https://amzn.to/3Q4y8KZ">RTLSDR</a> just couldn't receive them. Fortunately, there's a low-priced and <em>excellent</em> SDR that <em>can</em> read them: The <a href="https://amzn.to/4fc3Jo5">Nooelec NESDR</a>.</p>
<h2><a href="#setup" aria-hidden="true" class="anchor" id="setup"></a>Setup</h2>
<p>The <a href="https://amzn.to/4fc3Jo5">nesdr</a> is more or less plug and play. Once you have <a href="https://github.com/merbanan/rtl_433/">rtl_433</a> installed, It's pretty much just a matter of calling the right program with the right arguments, and you should immediately start seeing results.</p>
<p>On the physical side, your <a href="https://amzn.to/4fc3Jo5">nesdr</a> may come with a few different lengths of antennas. This little device can be used to listen in on a very wide variety of frequencies, and so the antennas bundled with it have different uses. To find the ideal, or near ideal, antenna length for a frequency, we use a bit of simple math to get the quarter-length antenna, which tells us we need an antenna about 9.2 inches long. My <a href="https://amzn.to/4fc3Jo5">nesdr</a> came with one about this length, so I used that one</p>
<p>Software wise, it couldn't be easier. Assuming you have <a href="https://github.com/merbanan/rtl_433/">rtl_433</a> installed, simply fire it up, listening on 319.5MHz, with the protocol set to <code>100</code><sup class="footnote-ref"><a href="#fn-3" id="fnref-3" data-footnote-ref>2</a></sup>:</p>
<pre class="athl"><code class="language-bash" translate="no" tabindex="0"><span class="line" data-line="1"><span class="function-call">rtl_433</span> <span class="variable-parameter">-f</span> <span class="variable-parameter">319.5M</span> <span class="variable-parameter">-R</span> <span class="number">100</span>
</span></code></pre>
<p>After firing up this, you'll get a console that seems rather quiet, after the initial messages. Go over and trigger one of these sensors (open a door). You should see a message appear in the console, describing a sensor, several switches, and maybe even a battery state. When you close the door, you should see a similar message, but one of the switches should have changed state. That's your door sensor. If you don't see anything, check the battery on the unit. Many of my units <em>still</em> have good batteries in them, despite the batteries being well over 10 years old.</p>
<p>Now we need to get that information into HomeAssistant.</p>
<h2><a href="#mqtt" aria-hidden="true" class="anchor" id="mqtt"></a>MQTT</h2>
<p><a href="https://github.com/merbanan/rtl_433/">rtl_433</a> has <em>built in</em> MQTT support, which is the best way to get the information from the sensors into HomeAssistant. You'll need to have an MQTT broker system running, which is beyond the scope of this article, but not terribly complex. Assuming you do have one running, simply modify the command we ran previously to resemble this one:</p>
<pre class="athl"><code class="language-bash" translate="no" tabindex="0"><span class="line" data-line="1"><span class="function-call">rtl_433</span> <span class="variable-parameter">-f</span> <span class="variable-parameter">319.5M</span> <span class="variable-parameter">-R</span> <span class="number">100</span> <span class="variable-parameter">-F</span> <span class="variable-parameter">mqtt://your-broker-hostname:1883,user=broker_user,pass=broker_pass</span>
</span></code></pre>
<p>Fire up a tool like <a href="https://mqtt-explorer.com">MQTT Explorer</a>, and trigger one of the sensors. You should see a message fairly quickly</p>
<p><img src="/.netlify/images?url=/postimages/2024-10-20-integrating-old-ge-interlogix-burglar-alarm-sensors-into-homeassistant-with-sdr/CleanShot%202024-10-20%20at%2012.17.28@2x.png" alt="My network is rather noisy, with many sensors happily reporting away" /></p>
<p>Once you see those messages on your MQTT network, we can get them into HomeAssistant. In your HomeAssistant configuration, you'll need to add something similar to this yaml:</p>
<pre class="athl"><code class="language-yaml" translate="no" tabindex="0"><span class="line" data-line="1"><span class="property">mqtt</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="2">  <span class="property">cover</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="3">    <span class="punctuation-delimiter">-</span> <span class="property">name</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;Front Door&quot;</span>
</span><span class="line" data-line="4">      <span class="property">unique_id</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;front_door&quot;</span>
</span><span class="line" data-line="5">      <span class="property">device_class</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;door&quot;</span>
</span><span class="line" data-line="6">      <span class="property">state_topic</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;rtl_433/server/devices/Interlogix-Security/contact/af42c7/switch5&quot;</span>
</span><span class="line" data-line="7">      <span class="property">state_closed</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;CLOSED&quot;</span>
</span><span class="line" data-line="8">      <span class="property">state_open</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;OPEN&quot;</span>
</span><span class="line" data-line="9">      <span class="property">device</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="10">        <span class="property">name</span><span class="punctuation-delimiter">:</span> <span class="string">RTL433</span>
</span><span class="line" data-line="11">        <span class="property">identifiers</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;RTL433 sensor&quot;</span>
</span></code></pre>
<p>You'll want to add one of those for each unit you want to detect. I recommend watching the network with <a href="https://mqtt-explorer.com">MQTT Explorer</a> and go around triggering each sensor, noting down its ID, and then setting up each cover entry.</p>
<p>Once that's all done, reload your HomeAssistant config, and you're done! You now have your door, window, heat, glass break, and any other sensors you cared to map in HomeAssistant.
<img src="/.netlify/images?url=/postimages/2024-10-20-integrating-old-ge-interlogix-burglar-alarm-sensors-into-homeassistant-with-sdr/front%20door%20history.jpg" alt="HomeAssistant history for my front door sensor" /></p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>They <em>still</em> use radio transmitters for modern burglar alarm systems, but most of them have moved away from simple systems like this to things such as Zigbee or Z-Wave. A large number of the &quot;DIY-ish&quot; ones also rely on Wi-Fi as their network. <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="fn-3">
<p>The <a href="https://github.com/merbanan/rtl_433/">rtl_433</a> docs tell us that protocol 100 is GE Interlogix. This is who made my sensors. If you're using different wireless sensors, you may have to use a different protocol. <a href="#fnref-3" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
    <item>
       <title>Moving from GitHub Pages to Netlify, and adding some statistics</title>
       <link>https://pdx.su/blog/2024-08-18-moving-from-github-pages-to-netlify-and-adding-some-statistics</link>
       <pubDate>Sun, 18 Aug 2024 12:36:19 -06</pubDate>
       <guid>https://pdx.su/blog/2024-08-18-moving-from-github-pages-to-netlify-and-adding-some-statistics</guid>
       <description><![CDATA[ <h1><a href="#moving-from-github-pages-to-netlify-and-adding-some-statistics" aria-hidden="true" class="anchor" id="moving-from-github-pages-to-netlify-and-adding-some-statistics"></a>Moving from GitHub Pages to Netlify, and adding some statistics</h1>
<p>When building this site, one thing I wanted to do was do it as cheaply and easily as possible. I didn't want to have to go out of my way to write for it, deploy it, update it, or anything else. And I certainly didn't want to pay for it. As such, I built it in a very particular way.</p>
<p>The site is built using the <a href="https://github.com/elixir-tools/tableau">tableau static site generator</a>, by Mitch Hanberg, and the <em>entire site</em> is <a href="https://github.com/paradox460/pdx.su">open source</a>. Assets are handled using [ESbuild], and consist mostly of some CSS niceties, and the occasional javascript component written in [lit]. Posts are written in markdown <sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup>, in VSCode, and deployments are simply done as part of a CI/CD process; I push a blogpost up, and the blog updates.</p>
<h2><a href="#the-past" aria-hidden="true" class="anchor" id="the-past"></a>The past</h2>
<p>Initially, the site was <a href="/blog/2023-11-14-migrating-to-tableau-ssg/">built using vuejs, but I migrated over to tableau</a>. The details as to why I did the migration are in the linked blogpost, but suffice to say all these months later I don't regret the migration to tableau.</p>
<p>GitHub pages were a natural fit for what I was trying to do. They're free, easy to use, support basically anything that can generate plain HTML, support and automate HTTPS, and more or less just work. You miss out on some fancier features, but at the time I didn't really need those (more on that later).</p>
<p>Deploying to ghp in an automated fashion was rather simple. Just use GitHub actions, and the built-in deploy to GitHub pages action, and you're good to go. You can see how I used to do it <a href="https://github.com/paradox460/pdx.su/blob/117a0c6fd35f160a5ec0a9702555d952078669c9/.github/workflows/ssg.yml">here</a>. This workflow builds the elixir site, builds the assets, minifies the HTML files, and deploys them to GitHub pages. Simple, easy to reason about, and free.</p>
<h2><a href="#analytics" aria-hidden="true" class="anchor" id="analytics"></a>Analytics</h2>
<p>For a long time, I didn't really care about how much traffic my site got. Sure, I wanted to have people read it, but I didn't really care how much read it, or where they came from. I've got a search keyword monitor for links back to my site, and so would see when various things were published to various discussion forums and other blogrolls, and where appropriate I'd join the conversation. But recently, I had a <a href="/blog/2024-03-17-reading-my-electric-meter-with-rtlsdr/">post</a> get a decent amount of attention, being featured on sites like [Hackaday], and suddenly I found I wanted to know where my visitor traffic was coming from.</p>
<p>I'm generally wary of most analytics systems. They're a nasty part of the surveillance reality we all live in, and most of them are horribly intrusive when it comes to privacy. They're also essential parts of web advertising, and so they want to track where a user goes <em>across the web</em>. I use a handful of systems to block as many trackers as is reasonable, and so was hesitant to add any to my personal site, as it felt a bit hypocritical.</p>
<p>Over the past few years, I'd been hearing good things about <a href="https://plausible.io/">plausible analytics</a>. I first heard about them through an elixir based discussion, although I can't remember which. They're an open-source analytics platform, that you can run <em>entirely yourself</em>, as well as a hosted option that, while not free, is well within affordable. They have a strong focus on privacy, so much so that they use <em>no persistent tracking mechanisms at all</em>.</p>
<p>If I were hosting my site on a more traditional webserver, like Nginx or Apache, I could analyze the logs to generate site data. Back in the days of Drupal, Joomla, and e107, I did this a lot for my pet sites, and its honestly astonishing how much information you can get just by scanning the logs and comparing them to something like the Maxmind database. Since I'm not, I don't get that data for free. But using something like plausible seems a good compromise.</p>
<p>And so this site now uses plausible for its analytics. You can view the analytics the site collects (probably rather unimpressive) at any time <a href="https://plausible.io/pdx.su/">here</a>, which is also available in the footer of every page. There is <em>still</em> no persistent tracking mechanisms on this site, and you can double-check that (no cookies, no localstorage, nada).</p>
<h2><a href="#adblock" aria-hidden="true" class="anchor" id="adblock"></a>Adblock</h2>
<p>One of the problems with <em>any</em> third party analytics script is that some popular blocklists take a stance that <em>no tracking at all is permissible</em>. This is fine, and it's the right of users to decide what connections their computer does and doesn't make, but it throws a wrench into the works when trying to get something that resembles accurate analytics, even if they're non-invasive, particularly for more technically oriented sites like this one. <a href="https://plausible.io/blog/google-analytics-adblockers-missing-data">Plausible themselves estimate that on techincal sites, up to 60% of viewers may block their tracker.</a> Not great for me.</p>
<p>The simplest way around this is to proxy the analytics script through a server you control. Great idea, but I don't control my server. GitHub does. And GitHub pages offer <em>nothing</em> when it comes to things like this, which is fair, as GitHub pages is more or less just serving up HTML and related assets.</p>
<p>The lazy thing to do was to just stick with GitHub pages, and accept that only 40% or so of my viewers would be accounted for in my analytics. That's fine, but I was feeling motivated, so I started investigating my options, and very quickly settled on Netlify.</p>
<h2><a href="#netlify" aria-hidden="true" class="anchor" id="netlify"></a>Netlify</h2>
<p>[Netlify] is one of a handful of companies that have cropped up to cater to the &quot;new&quot; web, where sites are built using technologies like JAMstack, and served as more-or-less static assets, with very lightweight server computational requirements, if any. Essentially, they work like most other static site serving services (i.e. GitHub pages), but offer a few extra features. One such feature was configurable proxies and rewrites 👐. This is exactly what I needed for more accurate analytics.</p>
<p>Setting up an account with them was, expectedly, pretty simple. Click a few buttons, fill out a form, and you've got an account, ready to deploy some sites. And that's where things get complicated.</p>
<p>I initially tried to deploy the site using their automatic GitHub linking, and the first build failed almost immediately. Looking through the logs of the build/deploy, It's pretty clear as to why. Their builder saw that I've got a <code>package.json</code> in the root of my site<sup class="footnote-ref"><a href="#fn-2" id="fnref-2" data-footnote-ref>2</a></sup>.</p>
<p>Digging into their builder a bit more, it quickly proved to be unsuitable for what I needed. Doing builds that involve differing languages, such as nodejs and elixir, are possible, but tricky, and from what I could find, poorly documented. Doing builds that use modern versions of Elixir and Erlang were even more difficult; they are running 1.9 for Elixir, which came out in 2019, and so would require me to figure out a way to install newer versions on their build system.</p>
<p>Ultimately, I wound up keeping the build system I was using for GitHub pages, but instead of pushing to GitHub pages, pushing to Netlify instead.</p>
<p><a href="https://github.com/paradox460/pdx.su/blob/3ee2b1a7e603911d084dd9d520d5798ed821d70f/.github/workflows/ssg.yml">The build script itself</a> is rather straightforwards. The assets and the elixir site are built separately, to get a performance boost through parallelism, and both make use of the <a href="https://github.com/actions/upload-artifact">upload-artifact</a> action to store their generated outputs. Then another job, dependent on both previous builds, downloads these assets using <a href="https://github.com/actions/download-artifact">download-artifact</a>, merges them together, runs minification, installs the Netlify cli, and deploys.</p>
<p>Simple.</p>
<p>Moving to Netlify also netted me a few other positives. Overall global performance has improved, slightly, although one of my friends in India reported that their latency went up a bit, as Netlify no longer has any servers there. And I can now finally modernize a bit of how images are served. Previously, all the images in the site were served verbatim, as they appeared in the source, which usually meant as a Jpeg. Now, thanks to Netlify, images are compressed a bit better, and served depending on what the user's platform supports, be it avif, or webp. Maybe in the future we can serve JpegXL.</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>I have plans on moving my writing language over to [djot]. I've written a <a href="https://github.com/paradox460/djot">djot package for elixir</a>, and so just need to patch <a href="https://github.com/elixir-tools/tableau">tableau</a> to allow for different formats in the default page and post generator <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="fn-2">
<p>This particular <code>package.json</code> is <em>not</em> where I define the assets my site uses, those live in one under the <code>assets</code> directory. Instead, it simply exists to <em>force</em> yarn to run through a corepack version, so VSCode and other editors don't try and use npm. <a href="#fnref-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
    <item>
       <title>DIY Permanent Xmas lights</title>
       <link>https://pdx.su/blog/2024-08-10-diy-permanent-xmas-lights</link>
       <pubDate>Sat, 10 Aug 2024 21:43:16 -06</pubDate>
       <guid>https://pdx.su/blog/2024-08-10-diy-permanent-xmas-lights</guid>
       <description><![CDATA[ <h1><a href="#diy-permanent-xmas-lights" aria-hidden="true" class="anchor" id="diy-permanent-xmas-lights"></a>DIY Permanent Xmas lights</h1>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/houselights.jpg" alt="My house with Red White and Blue lights for the fourth of july" /></p>
<p>Here in Utah, an increasingly common sight is houses with permanent &quot;Christmas&quot; lights. These are usually installed using aluminum channels, along the roofline, and make use of color-changing LEDs. Several of my neighbors have had them installed professionally, and I've always wanted something similar. However, the prices, and limitations, that come with professional installations, are often prohibitive.</p>
<h2><a href="#professional-installations" aria-hidden="true" class="anchor" id="professional-installations"></a>Professional installations</h2>
<p>Several companies have nation-wide and local footprints for installing these lights. Companies such as Jellyfish lighting, everlights, and trimlight seem to be the biggest players in the space, but I'm sure most roof and gutter companies will offer similar products as a service. They have the installations of these things down to a science; a contractor will come out, measure the areas you want lit, and leave. A while later, usually about a week, they'll come back with some workers, precut channels and lights, and a control system, and get the whole thing installed in an afternoon. Their products will usually come with a warranty, and will generally work pretty reliably. If you just want some lights <em>now</em>, they're pretty much unbeatable in that regard.</p>
<p>But all that convenience comes with some very major limitations. Limitations that, for a tinkerer like myself, are game breakers. First, they typically only offer <em>one</em> type of lighting, and that's pixel lighting. Generally these pixels can <em>only</em> do RGB, no white channel, meaning any whites you get are the odd looking not-white-white we see from other RGB leds, such as those in gaming keyboards and mice. Second, they very rarely have open control software. All of my neighbors installs are limited to <em>only</em> the official app, and require internet access to work. Some of them have mumblings about APIs on their website, but a few hours of research turned up very little more than &quot;contact us for API questions.&quot;<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup> You can forget about controlling those lights with HomeAssistant.</p>
<p>Finally, they are all absurdly costly. Several thousand dollars at the bottom end, up to tens of thousands at the upper. I can do better, for less.</p>
<h2><a href="#the-state-of-diy-led-control-circa-2024" aria-hidden="true" class="anchor" id="the-state-of-diy-led-control-circa-2024"></a>The state of DIY LED control circa 2024</h2>
<p>Fortunately, unlike a lot of other DIY projects, this is a firmly entrenched, established, and catered-to market. There is a massive nation-wide (and international) community of people who do large holiday light shows, complete with music synchronization, permanent led installs, and more. There are many vendors, and products, targeting this segment, and most of them are open, ranging between just using standardized control protocols, to being fully open-source, with the ability to literally build your own controllers using simple, off the shelf parts, like ESP32s.</p>
<p>The biggest utilities in this space are a few software packages, that drive everything else; <a href="https://xlights.org/">xLights</a>, <a href="https://kno.wled.ge/">WLED</a>, and <a href="https://github.com/forkineye/ESPixelStick">ESPixexStick</a>. The former is useful for sequencing light shows, and is beyond the scope of this article, but well worth a look if you're into that thing. The latter two are the most interesting. <a href="https://kno.wled.ge/">WLED</a> is ultimately what I wound up using, as <a href="https://github.com/forkineye/ESPixelStick">ESPixelStick</a> is meant more for interfacing with an xLights defined show.</p>
<p>For control hardware, there are a variety of options to choose from, but by far the most widespread and established are the Dig series of controllers from <a href="https://quinled.info">Intermittent Tech</a>. These packages provide a nice set of common requirements for LED control, built around an ESP32. You flash them with either WLED or ESPixelStick, depending on your needs.</p>
<p>As for the lights themselves, you have a dizzying amount of options. There are the common WS281x series LED packages, which are what commonly drives the pixels, packages that support RGBW, such as the SK6812, COB packages (aka LED Neon, very cool), strips, and even newer ones that not only have white LEDs on-board, but multiple white LEDs in addition to multiple color LEDs, so you can adjust the temperature of the white light as well. They all come in various packages too, from simple strips, to strings of &quot;pucks&quot;, to stringers (hanging bulbs).</p>
<p>For attaching them to structures, there are nearly as many options as there are light configuration. If you're using pixels, one of the best options is probably <a href="https://permatrack.us/">PermaTrack</a>, although plenty of other track systems for pixels exist. If you're doing strips, you can buy channels with diffusers that sit over them, spreading the lights out. And some don't even need a channel, such as the larger pucks, which can be screwed directly into a soffit.</p>
<h2><a href="#my-setup" aria-hidden="true" class="anchor" id="my-setup"></a>My Setup</h2>
<h3><a href="#what-i-wanted-to-accomplish" aria-hidden="true" class="anchor" id="what-i-wanted-to-accomplish"></a>What I wanted to accomplish.</h3>
<p>At the outset, I had a few goals in mind. I wanted lights along the eaves of the <em>entire</em> front of my house. I didn't need lights along the windows, garage doors, or pillars, just trim. Same as if I were brave enough to hang up old-fashioned C9s every year. They had to have a white light channel, in warm white (3000k or lower), as I frequently just want to light up the trim with simple white lights, and they had to be supported by WLED. You might think this constrained me, but in reality, it didn't. This is a fairly common ask, and nearly every single LED configuration out there has some capacity to be used in this.</p>
<p>Initially I wanted to use strips. I wasn't a tremendous fan of the bullet &quot;pixel&quot; look that the commercial installers went with, and thought having a higher density of lights might look better. I'd seen some youtubers I follow talk about how they liked using strips as well, and so it seemed a forgone conclusion for me. I bought a roll of COB LEDs, just to see how they looked, and while they look very cool, the color density was still somewhat low, with one color every 7cm or so.</p>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/cob_led.jpg" alt="COB LEDs showing the great color, but 7cm wide bands" /></p>
<p>I've kept these for a future project, as they just look amazing.</p>
<p>From there, I needed a controller, and so I purchased a <a href="https://quinled.info/quinled-dig-octa/">Dig-Octa</a>. This neat little board is stackable, meaning you can put multiple power routing boards alongside multiple LED controller boards, and have everything working in a nice package. I only needed the capacity of one &quot;brain board&quot; and one power board.</p>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/dig_octa.jpg" alt="Dig-Octa on my test bench" /></p>
<p>Finally, I needed some power supplies. I bought a pair of <a href="https://amzn.to/3yoNkgB">Mean Well UHP-350-5 350W 5vdc</a> power supplies. These were based on a back-of-the-envelope calculation on how much power I'd need at peak, based on some <a href="https://quinled.info/2020/03/12/digital-led-power-usage/">power tables</a> published by the guy behind the <a href="https://quinled.info/quinled-dig-octa/">Dig-Octa</a>.</p>
<p>All in all, here's what I initially planned to use:</p>
<ul>
<li>55m of SK6812 RGBWW (warm white) strips</li>
<li><a href="https://amzn.to/3yoNkgB">2 power supplies</a></li>
<li>1 <a href="https://quinled.info/quinled-dig-octa/">Dig-Octa</a> brain board and power board</li>
<li>Aluminum channels and diffusers for the strips to be screwed to my soffits</li>
<li><a href="https://amzn.to/3X0ciwb">Plastic enclosure purchased from amazon</a></li>
</ul>
<h3><a href="#changing-tracks" aria-hidden="true" class="anchor" id="changing-tracks"></a>Changing tracks</h3>
<p>Wanting to make sure I was doing everything right, I joined a community for this, and asked some questions there. I relayed what I was planning to do, and was quickly advised against using 5v and using strips. Strips are no-good because they are rather difficult to work with, particularly up on a roof, and tend to have somewhat high failure rates. If you get an IP68 rated strip, which you <em>should</em> if you're using them outside, splicing out a bad pixel is a tedious affair, that involves razors, soldering, wire cutters, a hot glue gun, and heat shrink tubing. And using 5v, while functional, means you have to do a lot more power injection<sup class="footnote-ref"><a href="#fn-2" id="fnref-2" data-footnote-ref>2</a></sup> than you would with higher voltages.</p>
<p>When asking what I should do instead, I was advised to check out a particular LED puck product, that came with channels and pucks. These pucks <em>are</em> available in RGBWW, have 3 LEDs per puck, and run at 12V. The 3 LEDs per puck makes these suckers bright, and the packages themselves are fairly easy to work with. And the channels are a fairly simple aluminum affair, with a backplate that you attach to your soffit, and then a front, where the pixels snap into holes, that snaps into the back plate.</p>
<p>Fortunately, I hadn't bought much more than a single strip of LEDs and the two power supplies, and so this pivot wasn't too difficult. I couldn't use either for this project, but thats acceptable, as they were useful enough they wouldn't spend too long in the spare parts bin.</p>
<p>So, with these changes, my new parts list wound up looking like this:</p>
<ul>
<li>55m of <a href="https://www.aliexpress.us/item/3256807344866216.html">LED pucks</a></li>
<li>55m of <a href="https://www.aliexpress.us/item/3256804734568668.html">aluminum channels</a></li>
<li><a href="https://amzn.to/3Am4PyD">1 12v power supply</a></li>
<li>1 <a href="https://quinled.info/quinled-dig-octa/">Dig-Octa</a> brain board and power board</li>
<li>1 <a href="https://amzn.to/3X0ciwb">Plastic enclosure purchased from amazon</a></li>
</ul>
<p>I placed the order, and awaited delivery, which came a surprisingly short period later.</p>
<h3><a href="#staging-everything" aria-hidden="true" class="anchor" id="staging-everything"></a>Staging everything</h3>
<p>Upon receipt of the huge box of tracks, pucks, and other assorted items, I set to work on the floor of the living room. I flashed an updated version of <a href="https://kno.wled.ge/">WLED</a> to my dig-octa, and started testing each string of LEDs I'd received.</p>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/dig_octa_and_psu.jpg" alt="Dig-Octa and the PSU as part of a &quot;test bench&quot;" /></p>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/testing_leds.jpg" alt="Testing a string of LED pucks" /></p>
<p>Once they were tested, my wife and I installed them into the aluminum tracks</p>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/lights_in_tracks.jpg" alt="Lights tested and installed in aluminum tracks" /></p>
<p>While testing them, I read more about the <a href="https://quinled.info/quinled-dig-octa/">Dig-Octa</a>, and found out that it supports a relay and an auxillary power source. This lets you turn off the big 12v 600W power supply, and thus turn off the LEDs, while still keeping the brain board active. The brain board will actuate the relay whenever it is toggled.<sup class="footnote-ref"><a href="#fn-3" id="fnref-3" data-footnote-ref>3</a></sup></p>
<p>I purchased a bunch of generic Chinese <a href="https://amzn.to/3WYHJXt">solid-state &quot;relays&quot;</a> off amazon, and got one of them wired up to toggle the power supply on and off:</p>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/relay.jpg" alt="Relay getting wired up" /></p>
<p>Finally, when all the LEDs were tested, I moved everything into an enclosure, and got it all bolted down nice and secure. This enclosure will keep things dry and clean, although I mounted it in a location where there wouldn't be much risk of dust or water ingress</p>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/box.jpg" alt="Enclosure with PSU and Brain Board" /></p>
<p>A period after taking this picture, I actually wired in a small plug <em>inside</em> the box<sup class="footnote-ref"><a href="#fn-mini-psu" id="fnref-mini-psu" data-footnote-ref>4</a></sup>, to plug a USB module into, so as to minimize the amount of wires going in and out of the box.</p>
<p>Finally, it was time to start the installation</p>
<h3><a href="#installation" aria-hidden="true" class="anchor" id="installation"></a>Installation</h3>
<p>Installation was fairly simple, albeit somewhat labor intensive. We started off by taking the backing of the tracks outside, lining them up along the roof, climbing up on a ladder, and screwing them into the soffit using self-cutting screws. I have metal soffits, so this worked reasonably well, but if I'd wooden soffits it would have been even easier. For corners, where a single 1m long track wouldn't fit, we cut the tracks, and the channels that snapped into them, with a miter saw. Aluminum is a nice soft material.</p>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/miter_saw.jpg" alt="Cutting track with a miter saw" /></p>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/tracks.jpg" alt="Tracks on the soffit, no LEDs yet" /></p>
<p>Once we got all the back tracks installed, it was time to install the front tracks. Given we'd need a couple power-injections for the longer runs, we started on the far end, and worked our way back. As before, we laid the tracks out, and where necessary, attached a power injection using <a href="https://amzn.to/4dHCpgZ">heat-shrink solderless connectors</a>. Soldering and then applying heat shrink isn't a terribly large deal, but doing it all in one pass is efficient, and the gel these connectors use to secure themselves around the wire adds extra waterproofking, akin to filling a heat shrink tube with hot glue or grease, and so its hard to beat.</p>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/track_and_heat_gun.jpg" alt="Tracks laid out and heat gun" />.</p>
<p>For the segments that were less than 1m long, such as the corners, we popped the LEDs out of the tracks, using a <a href="https://makerworld.com/en/models/468510#profileId-377651">little tool I 3D printed</a>, cut the tracks, and either wrapped the string of pixels around the bend, or cut it and attached a connector. More heat-shrink solder splices were involved in this process.</p>
<p>Finally, when all was done, we turned it on, and tested it. They are <em>very bright</em>, and you could see them in the middle of the day, at middling brightness:</p>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/bright.jpg" alt="Lower level done, showing LEDs on, at decent brightness" /></p>
<p>And at night, they look amazing in both color (the first image in this post) and white:</p>
<p><img src="/.netlify/images?url=/postimages/permanent_leds/white.jpg" alt="All pixels lit up in warm white" /></p>
<h2><a href="#closing" aria-hidden="true" class="anchor" id="closing"></a>Closing</h2>
<p>This was an amazingly rewarding project, and I cannot state how good they look at night. Some of my neighbors, who previously balked at the prices commercial operations have charged, are now interested in DIY. Ultimately I paid just shy of $2000 for this project, which includes all the components AND bringing out a contractor to help with some of the upper roof segments (I'm not comfortable on that tall of a ladder). Since <em>everything</em> about the control side of this is open-source, I have it wired up into HomeAssistant, and already have some automations set up, such as automatically turning them off <em>around</em> midnight, the ability to turn them on automatically around sunset/sunrise, and voice control (&quot;Hey google, turn on the roof trim&quot;). I plan to set up even more automations, such as turning them on in various team colors should they win a game in their associated sports, as well as a calendar integration, so I can schedule them to turn on at arbitrary dates, without worrying about doing it manually.</p>
<p>And I've already had to deal with a single puck failing, which caused all the subsequent ones to start behaving erratically. Fixing this one failed puck was rather simple; I just detached the segment with the faulty puck, took it inside, cut the bad puck out, spliced a new one in, and was up and running again in under an hour. Had I gone with strips, I couldn't have brought it inside to do that, and would have had to sit there splicing with a headlamp as my sole illumination, atop a ladder.</p>
<h2><a href="#updates" aria-hidden="true" class="anchor" id="updates"></a>Updates:</h2>
<ul>
<li>Linkrot ate the original link to the pixels I used, so I've updated it. The new link is <a href="https://www.aliexpress.us/item/3256807344866216.html">https://www.aliexpress.us/item/3256807344866216.html</a></li>
</ul>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>It's my opinion that any API that requires contacting someone to get set up is not an open API, and shouldn't be relied on unless there is a contractual guarantee of said API's availability. Think SLAs. <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="fn-2">
<p>With lower voltages, you lose more energy across longer runs of wire to heat, due to the resistance of said wire. This is known as voltage drop. When you have a load on that wire, such as LED lights, they also consume energy, and towards the end of a longer run of LEDs, you wind up with weird behaviors, such as bulbs lighting intermittently, bulbs lighting the wrong color, not responding to data codes, and so forth. You solve this by periodically <em>injecting</em> power. <a href="#fnref-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
</li>
<li id="fn-3">
<p>Additionally, when RGB LEDs of this type are &quot;off&quot;, they're only off in the most technical of senses. Off is just a 0 value on all the channels a &quot;light&quot; listens to. Power is still flowing through them, but the chip that controls an LED or group of LEDs is choosing not to illuminate them. If you use a relay to control the power supply, off is off, no power will flow. <a href="#fnref-3" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="3" aria-label="Back to reference 3">↩</a></p>
</li>
<li id="fn-mini-psu">
<p>Since building this box, I've built a couple more, for other projects, and have figured out better ways to power things. I've largely switched to using <a href="https://amzn.to/3Ya8Kb3">these</a> little tiny power supplies for the 5vdc the ESP32 requires. They're extremely small, and fairly reliable. <a href="#fnref-mini-psu" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="4" aria-label="Back to reference 4">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
    <item>
       <title>Running a minecraft server on fly.io</title>
       <link>https://pdx.su/blog/2024-07-08-running-a-minecraft-server-on-fly-io</link>
       <pubDate>Mon, 08 Jul 2024 13:37:07 -06</pubDate>
       <guid>https://pdx.su/blog/2024-07-08-running-a-minecraft-server-on-fly-io</guid>
       <description><![CDATA[ <h1><a href="#running-a-minecraft-server-on-flyio" aria-hidden="true" class="anchor" id="running-a-minecraft-server-on-flyio"></a>Running a minecraft server on fly.io</h1>
<p>Running a minecraft server can quickly become an expensive endeavor. Even a small server needs a reasonably powerful machine to run on, and with the landscape of hosting providers, that can quickly rack up in costs. Ideally, we'd run a server when we want to play, and not when we don't, minimizing the amount of useless time we pay for. And Fly.io makes it possible to do <em>just that</em>.</p>
<h2><a href="#flyio" aria-hidden="true" class="anchor" id="flyio"></a>Fly.io</h2>
<p>Fly.io is my favorite of the &quot;new&quot; cloud companies. They let you quickly spin up and down virtual machines to do what you want, have a <em>great</em> networking stack, and aren't the most expensive. I use them a <em>lot</em> for little applications, as well as some of my professional endeavors on Elixir. They've kind of moved into the hole that Heroku left when they went fully corporate.</p>
<p>One of the particularly nice features of Fly is that you can make a server automatically start and stop, depending on traffic. So you can have a minecraft server that boots up when you want to play, shuts down when you don't, and is still publicly accessible.</p>
<h2><a href="#complications-of-running-on-fly" aria-hidden="true" class="anchor" id="complications-of-running-on-fly"></a>Complications of running on Fly</h2>
<p>Running a minecraft server on a more traditional VPS, like DigitalOcean, is fairly straightforwards. You just boot up the machine, shell into it, install minecraft, and configure things. This is great for how easy it is to get started, but its a very manual, interactive process. You have to do everything by hand, and then rely on the persistence (storage volume) to maintain your changes across reboots, with the general assumption that your virtual private server will always exist <sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup></p>
<p>With Fly, or any other similar hosting provider, you <em>don't</em> have that same persistence. <em>Every single boot of your Minecraft server will be &quot;different&quot;</em>. Sure, when you mount a volume, which you need to do for world and other persistent data, it comes along for the ride, and you <em>can</em> fake a persistent style manual setup. But you shouldn't have to. If you can extract all the configuration out to a file, or set of files, that can be applied to the server at boot, your server becomes more resillient; instead of having to remember how you edited what file when, you just have it in your configuration. This is the same philosophy behind systems like <a href="https://nixos.org">Nix</a></p>
<p>Other complications arise when we want to tell Fly <em>how</em> to run our server. The easiest way to get arbitrary binaries up on Fly is by providing a docker image. We could build our own Docker image, that contains a minecraft server and some other stuff, but that's a whole endeavor of its own, one that we might not wish to undertake. Fortunately, there's an absolutely excellent docker image for running minecraft servers already.</p>
<h2><a href="#enter-docker-minecraft-server" aria-hidden="true" class="anchor" id="enter-docker-minecraft-server"></a>Enter docker-minecraft-server</h2>
<p><a href="https://docker-minecraft-server.readthedocs.io/en/latest/">docker-minecraft-server</a> is an amazing docker setup for running a minecraft server. It's got a ton of features, including automatic plugin installation, config patching, auto-stop, and more. It's perfect for what we want to do.</p>
<p>Getting it installed is rather straight forwards, and the docs are excellent, but I've made <a href="https://github.com/paradox460/minecraft-dedi-server">a repository that reflects the way I got it set up.</a></p>
<h3><a href="#my-changes" aria-hidden="true" class="anchor" id="my-changes"></a>My changes</h3>
<p>If you looked through my repository, you might notice that I make my own docker container, based off the one created by itzg. Minecraft still requires a bit of manual finagling, and so I wanted to make the ecosystem within my server's deployments more pleasurable to ssh into. So I add a few utilities to the base image, set up my config patches, so they can be versioned with the git repo, and tweak a few other system settings. Most of the changes are simple things that just fit <em>my</em> workflow better; you probably don't need them and can run the pure itzg container.</p>
<h2><a href="#building-my-flytoml-dynamically" aria-hidden="true" class="anchor" id="building-my-flytoml-dynamically"></a>Building my fly.toml dynamically</h2>
<p>To deploy on Fly.io, you use a configuration file called <code>fly.toml</code>. This file contains almost all the information your server needs to run, barring a few things like secrets.</p>
<p>The trouble with writing a <code>fly.toml</code> by hand is that certain niceties are absent, notably when dealing with environment variables.</p>
<p>docker-minecraft-server makes use of envars to configure many aspects of how it runs and boots, including where and which plugins it downloads and installs. You specify these as either a newline or comma separated list of URLs or other references, which are picked up at boot, installed, and synchronized. TOML allows for multiline strings, so configuring the list of plugins isn't terribly difficult, however more dynamic lists, such as the <code>SPIGET_RESOURCES</code> variable, which points to resources hosted on Spigot plugin repos, are cumbersome to use.</p>
<p>Specifically, <code>SPIGET_RESOURCES</code> wants a comma-separated list of id numbers, and <em>nothing else</em>. And spigot resource urls are rather descriptive, but the id number is not. I wanted a solution that would let me use the &quot;full&quot; spigot urls, but take advantage of the spiget downloader feature, which manages version updates for me.</p>
<p>Finally, I wanted to make use of fly's PROXY_PROTOCOL support. PROXY_PROTOCOL allows passing of proxy information to servers and other applications, and is relevant to our usecase here because, if turned off, all incoming connections to our minecraft server won't resolve as their &quot;real&quot; IP, but rather a fly internal IP. But I wanted to be able to turn this off and on, and it requires configuration in a few places to do so.</p>
<p>Aiming to solve all these problems, I wrote a <a href="https://github.com/paradox460/minecraft-dedi-server/blob/f8646c983d9264c16b7dfd5f9f76f72ed1015a63/fly.ts">simple little deno script</a>. This script is fairly simple, and mostly just does some string concatenation, but it <em>does</em> let me do things that the plain old TOML wouldn't.</p>
<p>I can set a single value, enableProxy, and have it set up both the fly port setup AND the envar, which is used by a patch to enable the proxy support on paper, and by docker-minecraft-server, to enable the auto-stop system to monitor our server.</p>
<p>I can also take full spigot URLs, strip the non-numeric-id portions, and render them out to a format that the docker container is happy to use.</p>
<p>I don't make use of this feature, but since this is <em>just</em> a script file, I could also move the configuration to be generated in a much more composable manner, or to use local .env files, or any number of things.</p>
<p>Since I used deno, we also get the advantage of the script being &quot;self contained&quot;. By using a custom shebang, I've made my script executable, so to build a new toml file you just have to run <code>./fly.ts</code>. No need to install packages (other than Deno), no need to remember which runner to use.</p>
<h2><a href="#deploying-and-things-to-note" aria-hidden="true" class="anchor" id="deploying-and-things-to-note"></a>Deploying, and things to note</h2>
<p>Once the new fly.toml is generated, deployment and running the server is an absolute breeze.</p>
<p><code>./fly.ts &amp;&amp; fly deploy</code> gets the server up and running, and I can connect to it in Minecraft, as expected. All the plugins and configuration I've specified in config files have been loaded onto the server, and any appropriate config patches have been applied.</p>
<p>Manual configuration, as always with Minecraft, takes a long time, but isn't tremendously difficult, just tedious. Things like LuckPerms, WorldEdit, WorldGuard, and your &quot;basic&quot; plugin of choice (I use <a href="https://www.zrips.net/cmi/">CMI</a>) need to be configured, same as always. You can do these configurations in a variety of ways; adding them as custom <code>COPY</code> commands to the Dockerfile, using docker-minecraft-server's <a href="https://docker-minecraft-server.readthedocs.io/en/latest/configuration/interpolating/#patching-existing-files">patching</a> system, or just by shelling into the server (via <code>fly ssh console</code>) and editing them by hand.</p>
<p>If you use premium plugins, you won't be able to automatically download them, as they likely require authentication to download. You can make use of the <a href="https://docker-minecraft-server.readthedocs.io/en/latest/mods-and-plugins/#optional-plugins-mods-and-config-attach-points">/plugins attach point</a> to load these plugins into your docker container, at which point the minecraft scripts will pick them up and put them in the right place.</p>
<p>The server makes use of a few values that shouldn't be exposed in your plaintext config, but rather set as &quot;secrets&quot;. Fly has a feature for this, where you simply set them via a command</p>
<pre class="athl"><code class="language-bash" translate="no" tabindex="0"><span class="line" data-line="1"><span class="function-call">fly</span> <span class="variable-parameter">secrets</span> <span class="variable-parameter">set</span> <span class="variable-parameter">RCON_PASSWORD=</span><span class="punctuation-special">$(</span><span class="function-call">openssl</span> <span class="variable-parameter">rand</span> <span class="variable-parameter">-base64</span> <span class="number">32</span><span class="punctuation-special">)</span>
</span></code></pre>
<p>This will be exposed as an envar in the container, which handles all the RCON stuff for you, and works nicely</p>
<h3><a href="#autostarting-and-why-i-didnt-set-it-up" aria-hidden="true" class="anchor" id="autostarting-and-why-i-didnt-set-it-up"></a>AutoStarting, and why I didn't set it up</h3>
<p>One of the more powerful things about this config is that you can make use of both autostart and autostop.</p>
<p>Autostop is handled by the minecraft server container; it monitors connections to minecraft, and kills the process after a configured duration, causing the Fly vm to shut down. This is &quot;safe&quot;, and is what I'm using. It lets you quit the game and not rack up a big bill because you forgot to quit the server. When configured with powerful enough anti-afk features in Minecraft itself, you can prevent issues where a player causes the server to be up endlessly through negligence.</p>
<p>However, AutoStart is a more complicated beast. AutoStart exists as a part of <em>fly's</em> systems, not as something that's minecraft aware. It works fine with minecraft, but has one very large caveat: <em>any TCP traffic on your server's exposed port(s) will boot the server</em>. If you're trying to save money, this isn't great, because it means a random server scraper, a nefarious script, or even someone just letting the minecraft server listing sit open, will keep booting your server again and again and again.</p>
<p>Because of this, I elected to manually start my servers, and let autostop handle the rest. Starting is trivial enough, simply run <code>fly apps restart</code> and your server boots almost immediately.</p>
<p>If you run your servers <em>entirely</em> on a private network, then this isn't so much of an issue, as you have less risk of bad actors. However, you <em>still</em> have risk, as negligence on behalf of one of your players could keep the server alive for a very costly period of time.</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>I'm purposely ignoring things like actual physical machine migrations, etc, because they're largely irrelevant when running something like a minecraft server. <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
    <item>
       <title>Making HomeAssistant automatically trigger libvirtd USB device mounts</title>
       <link>https://pdx.su/blog/2024-06-30-making-homeassistant-automatically-trigger-libvirtd-usb-device-mounts</link>
       <pubDate>Sun, 30 Jun 2024 00:53:25 -06</pubDate>
       <guid>https://pdx.su/blog/2024-06-30-making-homeassistant-automatically-trigger-libvirtd-usb-device-mounts</guid>
       <description><![CDATA[ <h1><a href="#making-homeassistant-automatically-trigger-libvirtd-usb-device-mounts" aria-hidden="true" class="anchor" id="making-homeassistant-automatically-trigger-libvirtd-usb-device-mounts"></a>Making HomeAssistant automatically trigger libvirtd USB device mounts</h1>
<p>If you run HomeAssistant in a libvirt-based VM, such as in a qemu backed system, and you want to forward USB dongles (such as for z-wave or zigbee) from the host to the guest, you might run into issues where the dongle doesn't reconnect when the VM restarts, or when the host restarts. This can be quite frustrating, as any devices and automations you have that are tied to that dongle <em>will not work</em> until you manually reconnect it to the guest.</p>
<h2><a href="#manually" aria-hidden="true" class="anchor" id="manually"></a>Manually</h2>
<p>For a while, I was just doing the reconnects manually, via a simple little script:</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1">#! /usr/bin/env fish
</span><span class="line" data-line="2">
</span><span class="line" data-line="3">if test (whoami) != &quot;root&quot;
</span><span class="line" data-line="4">  echo &quot;Must run as root&quot; &gt;&amp;2
</span><span class="line" data-line="5">  exit 1
</span><span class="line" data-line="6">end
</span><span class="line" data-line="7">
</span><span class="line" data-line="8">set -l cyme &quot;/home/jeffs/.local/share/mise/installs/rust/latest/bin/cyme&quot;
</span><span class="line" data-line="9">set -l serials XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
</span><span class="line" data-line="10">
</span><span class="line" data-line="11">for serial in $serials
</span><span class="line" data-line="12">  set -l temp (mktemp /tmp/hass.XXXXXXXXXX)
</span><span class="line" data-line="13">  set -l usb_info ($cyme --filter-serial $serial --json)
</span><span class="line" data-line="14">  set -l busnum (jq -r &#39;.[].location_id.bus&#39; (echo $usb_info | psub))
</span><span class="line" data-line="15">  set -l devnum (jq -r &#39;.[].location_id.number&#39; (echo $usb_info | psub))
</span><span class="line" data-line="16">
</span><span class="line" data-line="17">  echo -e &quot;
</span><span class="line" data-line="18">&lt;hostdev mode=&#39;subsystem&#39; type=&#39;usb&#39;&gt;
</span><span class="line" data-line="19">  &lt;source&gt;
</span><span class="line" data-line="20">    &lt;address type=&#39;usb&#39; bus=&#39;$busnum&#39; device=&#39;$devnum&#39; /&gt;
</span><span class="line" data-line="21">  &lt;/source&gt;
</span><span class="line" data-line="22">&lt;/hostdev&gt;&quot; &gt; $temp
</span><span class="line" data-line="23">
</span><span class="line" data-line="24">  while true
</span><span class="line" data-line="25">    virsh detach-device hass $temp; or break
</span><span class="line" data-line="26">  end
</span><span class="line" data-line="27">  virsh attach-device hass $temp
</span><span class="line" data-line="28">end
</span><span class="line" data-line="29">
</span></code></pre>
<p>This script isn't really anything special; it uses <a href="https://github.com/tuna-f1sh/cyme">cyme</a> instead of <code>lsusb</code>, as a previous incarnation of the script proved brittle around parsing the output of <code>lsusb</code>, and attempts to get the bus and device # of my two dongles (z-wave and zigbee), writes a quick temporary xml file for <code>virsh</code> to consume, and then asks virsh to detach any devices at that bus/device number, and then asks it to reattach them.</p>
<p>It runs through the detach step multiple times, as I've had issues with virsh getting <em>multiple</em> attachments to a single bus/dev# in the past, exhausting the number of device passthroughs, and getting into an error state. It's probably not necessary, but I keep it around, as its harmless if it does nothing.</p>
<p>Running this script manually <em>still works</em>, but I don't want to have to shell into the host and run it every time there's a reboot to either the host or the guest, and so I looked at automating it</p>
<h2><a href="#udev-rules" aria-hidden="true" class="anchor" id="udev-rules"></a>UDEV rules</h2>
<p>Initially, I tried running a variation of this script via udev rules, with the following udev rules file:</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1"># ZIGBEE
</span><span class="line" data-line="2">SUBSYSTEM==&quot;usb&quot;, ATTRS&lbrace;serial&rbrace;==&quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;, RUN+=&quot;/usr/sbin/hass-reattach-usb.fish&quot;
</span><span class="line" data-line="3"># ZWave
</span><span class="line" data-line="4">SUBSYSTEM==&quot;usb&quot;, ATTRS&lbrace;serial&rbrace;==&quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;, RUN+=&quot;/usr/sbin/hass-reattach-usb.fish&quot;
</span></code></pre>
<p>Those rules would dispatch the following script</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1">#! /usr/bin/env fish
</span><span class="line" data-line="2">
</span><span class="line" data-line="3">set -l logFile &quot;/var/log/hass-usb-reattach.log&quot;
</span><span class="line" data-line="4">set -l virshDomain &quot;hass&quot;
</span><span class="line" data-line="5">set -l tmpfile (mktemp)
</span><span class="line" data-line="6">
</span><span class="line" data-line="7">set -l deviceType
</span><span class="line" data-line="8">
</span><span class="line" data-line="9">switch $ID_SERIAL_SHORT
</span><span class="line" data-line="10">  case &quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;
</span><span class="line" data-line="11">    set deviceType &quot;Zigbee&quot;
</span><span class="line" data-line="12">  case &quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;
</span><span class="line" data-line="13">    set deviceType &quot;Z-Wave&quot;
</span><span class="line" data-line="14">  case &quot;*&quot;
</span><span class="line" data-line="15">    exit 0
</span><span class="line" data-line="16">end
</span><span class="line" data-line="17">
</span><span class="line" data-line="18">if not set -q BUSNUM; or test -z &quot;$BUSNUM&quot;; or not set -q DEVNUM; or test -z &quot;$DEVNUM&quot;
</span><span class="line" data-line="19">  echo &quot;NO SUCH LUCK BUDDY&quot; &gt;&gt; $logFile
</span><span class="line" data-line="20">  exit 0
</span><span class="line" data-line="21">end
</span><span class="line" data-line="22">
</span><span class="line" data-line="23">
</span><span class="line" data-line="24">set -l hostdev &quot;
</span><span class="line" data-line="25">&lt;hostdev mode=&#39;subsystem&#39; type=&#39;usb&#39;&gt;
</span><span class="line" data-line="26">  &lt;source&gt;
</span><span class="line" data-line="27">    &lt;address type=&#39;usb&#39; bus=&#39;$BUSNUM&#39; device=&#39;$DEVNUM&#39; /&gt;
</span><span class="line" data-line="28">  &lt;/source&gt;
</span><span class="line" data-line="29">&lt;/hostdev&gt;
</span><span class="line" data-line="30">&quot;
</span><span class="line" data-line="31">set hostdev (string trim $hostdev)
</span><span class="line" data-line="32">
</span><span class="line" data-line="33">printf $hostdev &gt; $tmpfile
</span><span class="line" data-line="34">
</span><span class="line" data-line="35">echo (date) &quot;: (Re-)attaching $deviceType (B:$BUSNUM D:$DEVNUM)&quot; &gt;&gt; $logFile
</span><span class="line" data-line="36">
</span><span class="line" data-line="37">virsh detach-device &quot;$virshDomain&quot; &quot;$tmpfile&quot; &amp;&gt;1 &gt;&gt; $logFile || true
</span><span class="line" data-line="38">virsh attach-device &quot;$virshDomain&quot; &quot;$tmpfile&quot; &amp;&gt;1 &gt;&gt;
</span><span class="line" data-line="39">
</span><span class="line" data-line="40">rm $tmpfile
</span><span class="line" data-line="41">
</span><span class="line" data-line="42">exit 0
</span></code></pre>
<p>This worked in the case that the USB devices were added <em>after</em> the VM was running. But it didn't work for initial boot of the host. Which was the biggest and originating problem I was trying to solve.</p>
<h2><a href="#libvirt-hooks" aria-hidden="true" class="anchor" id="libvirt-hooks"></a>libvirt hooks</h2>
<p>libvirt has a <a href="https://libvirt.org/hooks.html">hooks feature</a>, where it will run certain hook scripts on the <em>host</em> OS at various points. Of interest to us is the <code>started</code> point, for qemu.</p>
<p>I wrote up a variation of the first script, with some wrapper code to only dispatch on <code>started</code> events. Unfortunately, it never actually did what I needed it to do.</p>
<p>The script would run, and attempt to mount a device to the guest OS. But it would run too soon after the guest was started, and was unwilling to take new devices, and so would just hang there, eventually timing out, and not adding the new device, requiring a manual intervention.</p>
<p>I meant to post the script example I used here, but found out that I actually deleted it in frustration when it didn't work. You're not missing out on much, as the script was mostly just an if test to see if we're in a <code>started</code> event, an if test to see if the domain is one we care about, and then a dispatch to the manual script.</p>
<h2><a href="#attaching-the-devices-when-homeassistant-is-online" aria-hidden="true" class="anchor" id="attaching-the-devices-when-homeassistant-is-online"></a>Attaching the devices when HomeAssistant is online</h2>
<p>If we look at what we're <em>actually</em> trying to do here, we're trying to get these devices mounted in HomeAssistant. We don't really give a hoot about the underlying state of the guest OS, and so a libvirt status of <code>started</code>, the equivalent to a power light being illuminated on a physical machine, is of no real importance. If we could get HomeAssistant to somehow tell the host OS when it was ready, we could then run the attach script, and have everything just work.</p>
<p>A lot of people will use SSH to make their HomeAssistant guests talk to the host. I was a bit squeamish about this, it seemed like an easy way to open an attack vector, should my HomeAssistant installation get compromised somehow. I could ssh into a user with limited permissions on the host, but &quot;limited&quot; is a misnomer, as the user would still need access to libvirt, via the <code>virsh</code> command and the ability to query all the USB devices on the system. Not exactly a light set of permissions.</p>
<p>I also thought about setting up a small http server, which would handle dispatching the script when HomeAssistant calls it; essentially a webhook. While this would undoubtably be far more secure than full-blown SSH access, it wasn't really something I wanted to muck about with. I've had enough experience trying to tighten down a webhook to only respond to a &quot;real&quot; client, and ignore fake clients, that I didn't really want to muck with it for what should just be a simple process.</p>
<p>Finally, I realized that HomeAssistant's <a href="https://www.home-assistant.io/integrations/mqtt/">MQTT integration</a> sends <a href="https://www.home-assistant.io/integrations/mqtt/#birth-and-last-will-messages">birth and last-will messages</a> to <code>homeassistant/status</code>. I use <a href="/blog/2024-03-17-reading-my-electric-meter-with-rtlsdr">MQTT to monitor my power and gas meters</a>, as well as bringing real-time data from my WeeWX weather station into HomeAssistant, and so using it for something else was great. Having the <code>homeassistant/status</code> messages automatically emitted from HomeAssistant itself means I wouldn't have to create an automation to emit a message at boot, simplifying the number of moving parts.</p>
<p>Running a simple MQTT client, to listen to a particular topic, and run a small program when a message comes in on that topic, is fairly trivial. I used the <a href="https://mosquitto.org/man/mosquitto_sub-1.html">mosquitto_sub</a> program, which comes with the <code>mosquitto-clients</code> on most linux distros. This program simply connects to a server, listens to a topic, and emits messages to stdout. A script to consume these messages, and dispatch the attach script, was rather simple:</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1">#! /usr/bin/env fish
</span><span class="line" data-line="2"># You can run this by hand, but it expects to be run by a daemon
</span><span class="line" data-line="3"># see hass-device-passthrough-listener.service
</span><span class="line" data-line="4">set -l host homeassistant.local
</span><span class="line" data-line="5">set -l username vm-host
</span><span class="line" data-line="6">set -l password &#39;xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&#39;
</span><span class="line" data-line="7">set -l topic homeassistant/status
</span><span class="line" data-line="8">
</span><span class="line" data-line="9">echo &quot;Starting listener service&quot;
</span><span class="line" data-line="10">
</span><span class="line" data-line="11">mosquitto_sub -R -h $host -t $topic -u $username -P $password | while read -l line
</span><span class="line" data-line="12">  echo &quot;[MSG] $line&quot;
</span><span class="line" data-line="13">  if test $line = &quot;online&quot;
</span><span class="line" data-line="14">    echo &quot;Attempting to run passthrough script&quot;
</span><span class="line" data-line="15">    /home/jeffs/homeassistant/device-passthrough.fish
</span><span class="line" data-line="16">  end
</span><span class="line" data-line="17">end
</span><span class="line" data-line="18">
</span></code></pre>
<p>Running the script, and sending fake online messages with the HomeAssistant MQTT publish service, worked beautifully. Since I wanted this to run <em>all the time</em>, from boot to shutdown, I whipped up a systemd service file, that starts it and keeps it running:</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1">[Unit]
</span><span class="line" data-line="2">Description=&quot;Hass device passthrough listener&quot;
</span><span class="line" data-line="3">Requires=libvirtd.service
</span><span class="line" data-line="4">Requires=network-online.target
</span><span class="line" data-line="5">After=libvirtd.service
</span><span class="line" data-line="6">
</span><span class="line" data-line="7">[Service]
</span><span class="line" data-line="8">Restart=always
</span><span class="line" data-line="9">RestartSec=30
</span><span class="line" data-line="10">ExecStart=/home/jeffs/homeassistant/mqtt-listener.fish
</span><span class="line" data-line="11">
</span><span class="line" data-line="12">[Install]
</span><span class="line" data-line="13">WantedBy=multi-user.target
</span></code></pre>
<p>After getting this loaded and started with HomeAssistant, I brought down the VM by hand, and watched via <code>journalctl</code> as the script received a message, kicked off the attach script, and then went back to idling. After this test was successful, I restarted the whole host, and saw it work with success as soon as HomeAssistant was ready.</p> ]]></description>
    </item>
    <item>
       <title>Migrating my HomeAssistant automations from NodeRED to Digital Alchemy</title>
       <link>https://pdx.su/blog/2024-06-09-migrating-my-homeassistant-automations-from-nodered-to-digital-alchemy</link>
       <pubDate>Sun, 09 Jun 2024 21:24:13 -06</pubDate>
       <guid>https://pdx.su/blog/2024-06-09-migrating-my-homeassistant-automations-from-nodered-to-digital-alchemy</guid>
       <description><![CDATA[ <h1><a href="#migrating-my-homeassistant-automations-from-nodered-to-digital-alchemy" aria-hidden="true" class="anchor" id="migrating-my-homeassistant-automations-from-nodered-to-digital-alchemy"></a>Migrating my HomeAssistant automations from NodeRED to Digital-Alchemy</h1>
<p>For my home automation needs, I use HomeAssistant. And for more complex automations, I used to use <a href="https://community.home-assistant.io/t/home-assistant-community-add-on-node-red/55023">NodeRED</a>. Recently I undertook some effort to move over to <a href="https://docs.digital-alchemy.app">DigitalAlchemy</a>, which lets you write automations in Typescript. Here's what I learned in the process.</p>
<h2><a href="#home-automations-via-homeassistant" aria-hidden="true" class="anchor" id="home-automations-via-homeassistant"></a>Home Automations via HomeAssistant</h2>
<p>The platform I use for most of my home automation things is <a href="https://www.home-assistant.io">HomeAssistant</a>. It's a powerful platform, and will probably show up in other blogposts in the future. HomeAssistant is great because it lets me integrate many services into a single automation framework. It's easy to extend, and serves my needs well. I moved over to it from HomeSeer a few years ago, and the migration has been for the better in almost all regards.</p>
<p>Out of the box, HomeAssistant has a fairly powerful automation system built in. It is comparable to that of HomeSeer, or more widely known systems such as Tasker on Android and Shortcuts on iOS. Essentially, you have a collection of triggers, conditions, and steps for your automation.</p>
<p>For example, if you wanted to turn on a light every time the sun came up, you can do that fairly easily. You'd set a trigger that listens for a state change in the <code>sun</code> integration, and when the trigger sees that <code>sun</code> has changed to <code>up</code>, you'd fire your automation to turn on the light.</p>
<p>You can do far more complicated automations with the built-in system, although it tends to get somewhat unwieldly. Automations are, at the core, a set of discrete steps that flow one into the next. You can add parallelism with some useful constructs, but they very much adopt a trigger-condition-action structure, and breaking out of this becomes confusing quickly.</p>
<h2><a href="#enter-nodered" aria-hidden="true" class="anchor" id="enter-nodered"></a>Enter NodeRED</h2>
<p>Many people in the HA community make use of an excellent package called <a href="https://community.home-assistant.io/t/home-assistant-community-add-on-node-red/55023">NodeRED</a>. NodeRED aims to make more powerful and complex automations far easier. It works through a flowchart paradigm. You drag out blocks that represent either triggers, actions, or primitive control logic, and literally &quot;wire&quot; them together, via lines in the UI, to do what you want. Automations that are hopelessly complex in pure HomeAssistant become simpler in NodeRED.</p>
<p>However, it isn't without its flaws. The developers and community around NodeRED seem averse to introducing too many &quot;programmer-y&quot; constructs. There are no primitive boolean gates, if/else objects, or simple comparators. You <em>can</em> do all these things, but it isn't immediately obvious, particularly if you come from a programming background.</p>
<p>Additionally, there aren't many ways to &quot;dry&quot; up a NodeRED flow. Say you have many similar, but ultimately different, flows. In programming, you'd just make a common function that handles what it can, and use it where appropriate. You can try to make a sub-flow, but this is limited, and often winds up being more complicated than just copy+pasting nodes everywhere.</p>
<p>Variables are kind of a mess; each flow can have its own, and there are global variables as well. They work well enough, but they're not really surfaced too much by the UI, and so if you don't remember (or leave a note) where a variable comes from or goes, it is largely opaque.</p>
<p>Finally, NodeRED flows are difficult to store in a version management system. They're not generally human-readable when serialized out to JSON, and ultimately insignificant edits to the &quot;shape&quot; of a flow can result in massive diffs.</p>
<h2><a href="#digitalalchemy-cometh" aria-hidden="true" class="anchor" id="digitalalchemy-cometh"></a>DigitalAlchemy Cometh</h2>
<p>For a few years now, I've shrugged my shoulders and accepted that NodeRED was the best we could get. There are other script-based automation platforms based around Python, but I really don't care for python, and so don't want to use it for my HomeAutomation if I can avoid it.</p>
<p>Earlier this year, a developer named Zoe released <a href="https://docs.digital-alchemy.app">DigitalAlchemy</a>, which strives to be a powerful automation framework for HomeAssistant, centered around use of TypeScript for its automation.</p>
<p>I don't mind JavaScript and its friends. It's not my favorite languate family, not by a long shot, but its not repellent to me either. For something like home automation, it's event oriented system, easy async, and friendlyness to functional programming paradigms make it an obvious choice.</p>
<p>DigitalAlchemy is a wonderful little framework, that uses many of the features TypeScript exposes to give you a very powerful way of writing and maintaining automations. It generates TypeScript types for <em>all</em> of your HomeAssistant integrations, so when you're editing your scripts, you get both code completion, but also sanity checks. No more trying to pass a fan to the Light: Turn On service.</p>
<p>And since its <em>real</em> TypeScript, you can use <em>real</em> JavaScript techniques when programming. Want to use a powerful state machine to handle your automations? You can do that, trivially. Just include one of the ones you find on npm, write the glue code, and away you go.</p>
<p>Over the course of a month, I ported all of my automations out of NodeRED and into DigitalAlchemy. The process was rather straightforwards, and the resultant automations are <em>far</em> simpler than their predecessor. I can open one up in VSCode and immediately start working, without having to remember too many weird quirks about how the system works. Feedback is fast, and so iteration times are equally fast. There's even a repl for testing ideas out on the fly.</p>
<p>And the lead developer, Zoe, has been spectacularly responsive. Questions and suggestions get met with near-immediate fixes or implementations, sometimes multiple iterations within the hour of first being brought up. They've recently been working on some tremendous improvements to the DigitalAlchemy synapse system, which lets you create and expose virtual entities in HomeAssistant <em>from</em> your DigitalAlchemy scripts, allowing a more two-way flow of control for scripts. It's really quite powerful; I've used it in several of my scripts to provide &quot;circuit breakers,&quot; so I can do things like turn off the smart-away automations when I've got guests over.</p>
<h3><a href="#use-syncthing-to-make-editing-far-more-enjoyable" aria-hidden="true" class="anchor" id="use-syncthing-to-make-editing-far-more-enjoyable"></a>Use SyncThing to make editing far more enjoyable</h3>
<p>If you are running Digital Alchemy through the HomeAssistant platform (quickstart), you might get tired of editing your automations in the browser based VSCode interface. While you can use systems like SMB or SSHFS to mount your HomeAssistant filesystem for editing, this does impose a performance penalty.</p>
<p>Instead, I suggest using the <a href="https://github.com/Poeschl/Hassio-Addons/tree/main/syncthing">SyncThing</a> addon for HomeAssistant, and setting up a sync between your development machine and your HomeAssistant install. You can then edit and build your automations locally, and just restart the DA addon in HomeAssistant when you're ready to run them.</p>
<h2><a href="#addendum" aria-hidden="true" class="anchor" id="addendum"></a>Addendum</h2>
<p>If you're interested in seeing <em>how</em> DigitalAlchemy scripts look in practice, I encourage you to check out <a href="https://github.com/paradox460/HomeAssistantConfig/tree/main/typescript">my implementations</a>.</p> ]]></description>
    </item>
    <item>
       <title>Thoughts on Hoselink Garden Hose Reel</title>
       <link>https://pdx.su/blog/2024-04-16-thoughts-on-hoselink-garden-hose-reel</link>
       <pubDate>Tue, 16 Apr 2024 16:55:14 -06</pubDate>
       <guid>https://pdx.su/blog/2024-04-16-thoughts-on-hoselink-garden-hose-reel</guid>
       <description><![CDATA[ <h1><a href="#thoughts-on-hoselink-garden-hose-reel" aria-hidden="true" class="anchor" id="thoughts-on-hoselink-garden-hose-reel"></a>Thoughts on Hoselink Garden Hose Reel</h1>
<p>I recently needed a new garden hose reel for the front yard, and wanted something a bit more convenient than what I had before. I've had a hose cart I bought from Home Depot years ago, and its worked reasonably well, but its ugly as sin. I've had rather good luck with an <a href="https://www.eleyhosereels.com">Eley hose reel</a> in the back, where I need 200 feet of hose, but don't want something that large and cumbersome for my smaller front yard. Enter the <a href="https://www.hoselink.com">Hoselink</a> hose reel.</p>
<h2><a href="#what-i-wanted" aria-hidden="true" class="anchor" id="what-i-wanted"></a>What I wanted</h2>
<p>I didn't really want too much more than my old hose reel provided, and a lot of these things are what I feel should be standard for <em>any</em> hose reel. It shouldn't leak. It should be easy to spool hose out, and easy to reel it back in. If it comes with a hose, the hose should be durable and &quot;feel&quot; nice. Finally, the physical construction of the reel should inspire confidence; it shouldn't feel rickety and cheap.</p>
<p>Features in the &quot;would be really nice to have&quot; column are things like the ability to mount the reel on the wall, while being able to easily bring it into the garage during winter, and a self-retracting hose reel. The last one is a point of contention, as (before I bought the HoseLink) I've never found one that held up particularly well, and was quite averse to them, despite wanting them to work.</p>
<p>Visiting big box stores, the options are scarce. There are usually a few cheap, plastic hose reels for sale, and maybe a single metal one. None of them inspire any confidence. The plastic ones look like they'd crack after a single summer in the sun, and the metal ones look as if they'd rust up in a few months. The winding mechanisms look labor-intensive, not something a small child could handle. And, more than anything, they're all fantastically ugly. I don't really expect too much in the way of appearance from something as utilitarian as a hose reel, but it shouldn't look like a piece of cheap trash either.</p>
<p>So I looked online. Loads of wall mount, self retracting reels exist, but most of them have mediocre to terrible reviews. But one does stand out. The HoseLink reel. Youtubers such as <a href="https://www.youtube.com/watch?v=OKnZG2o97H4">SilverCymbal rave about the quality of the HoseLink reel</a>, and his taste has generally been good in the past, so I decided to try one out. They are somewhat expensive, nearly the same price as my all-metal Eley, so I was hoping it would impress me with its quality.</p>
<h2><a href="#the-reel-itself" aria-hidden="true" class="anchor" id="the-reel-itself"></a>The Reel itself</h2>
<p>Ordering was simple enough, and the package came with the usual expediency of modern shipping. A large, somewhat heavy reinforced cardboard box arrived, and had my hoselink, wall-mount kit, a sprayer attachment, Y-fitting, a few quick disconnects, and a well written and easy to follow manual inside. The box immediately became a play fort for my daughter, while I was mounting the unit to the outside wall.</p>
<p>Mounting was easy enough; there's an included template, some brick anchors, and simple instructions. I have brick walls, and so set up with a fresh masonry bit and a hammer drill, and went to town. The anchors worked better than some I have used; I didn't have to inject any epoxy into the holes to ensure fastness. Part of what makes mounting so easy is that you don't actually have the big heavy hose reel hanging off the wall while you mount it. You mount an L-Shaped bracket, and the HoseLink has a post that slots into the bracket, allowing for 180º of movement.</p>
<p><img src="/.netlify/images?url=/postimages/hoselink.jpg" alt="The reel mounted on the wall" /></p>
<p>The reel itself is rather spartan and unobtrusive. Nothing screams &quot;fancy hose reel&quot;, which is nice. It looks far better than what was for sale at the big box stores. The lead hose is a bit long for my use case, but I'm sure that's attractive when you don't have a faucet so conveniently located. There are no external protuberances on the reel itself either, it's a very compact, clean affair.</p>
<p>The back of the reel, above the mounting pin, has a fold down handle, which is useful when lifting the reel out of the bracket for storage. When not in use, the handle practically disappears up against the body of the unit.</p>
<p>The hose is of decent quality, with brass fittings at either end, and no visible leaks from anywhere. A large ball is mounted about a foot from the business end of the hose, ostensibly to prevent it from retracting all the way into the reel should you wind it back up without an attachment.</p>
<p>It works pretty much as you'd expect it to. Grab the hose, pull out the length you need, and it retracts a couple inches and then locks in place. When it's time to reel it back in, just give it a gentle tug, and the reel will start to wind the hose up, like the cord on a vacuum cleaner. To stop it at any length along the way, just pull it back out a little, and the latch will reengage and leave you with a shorter length of hose. This is quite useful when transitioning between watering tasks, as you can easily prevent coils of hose from just sitting out.</p>
<h2><a href="#the-sprayer-and-quick-disconnects" aria-hidden="true" class="anchor" id="the-sprayer-and-quick-disconnects"></a>The Sprayer and Quick Disconnects</h2>
<p>HoseLink's website gushes about their quick disconnects and the included sprayer wand, both of which I was skeptical about when purchasing, but since they were add-ins at no extra cost, I didn't quibble.</p>
<p><img src="/.netlify/images?url=/postimages/hoselink-sprayer.jpg" alt="The Sprayer, with an Eley quick disconnect barb attached" /></p>
<p>The sprayer is well-made, and feels nice in your hand. The shut-off valve sits comfortably under your thumb, and spray patterns are the usual ones you'd expect to find. The shower is a bit intense for very delicate plants, but that isn't unique to the HoseLink sprayer; for seedlings my wife and I like to use a <a href="https://amzn.to/3W2slJU">Dramm spray head</a>, which generates an exceptionally gentle stream.</p>
<p><img src="/.netlify/images?url=/postimages/hoselink-qd.jpg" alt="Quick Disconnect, showing the water shut off valve" /></p>
<p>The quick disconnects are well-made too, although they aren't my favorite. They work using a symmetric bayonet style latch, locking together when you line up the barbs and give a quarter turn. They didn't leak while I was using them, but I did find the bayonet design somewhat cumbersome; once I thought I'd seated it well, only to have it spray water everywhere, as one barb never seated into its channel on the opposing face. There is a raised indicator in the plastic for lining up the two halves before locking together, but it's something you'd have to watch and feel for, not something you can just do blind.</p>
<p>I did like that they included a quick disconnect that has a water shut-off valve built in. I've had some <a href="https://amzn.to/3W2sHQK">brass shut off valves</a> living on the end of my other hoses for a few years now, and find having the ability to control the amount of water at the business end of the hose invaluable. I would have moved one over to the new hose reel, but in the few years I've had it, it managed to permanently attach itself to the end of the old hose, so a new one was in order.</p>
<p><img src="/.netlify/images?url=/postimages/eley-swivel-and-qd.jpg" alt="Eley swivel and quick disconnect" /></p>
<p>I bought the <a href="https://www.eleyhosereels.com/products/garden-hose-quick-connectors">somewhat expensive Eley quick disconnects</a> and an <a href="https://www.eleyhosereels.com/products/garden-hose-shut-off-valve-with-swivel-fitting">Eley shutoff+swivel</a>. While these are far more expensive than the competition, even ones from Dramm, they are made <em>far better</em> than any I have ever used. They are big, well-made brass units, with <em>full</em> flow-through, so you don't impede the water flow, and the quality of the machining is apparent, their operation is incredibly smooth. You can connect and disconnect attachments without having to look and watch what you're doing, one-handed. The plug end is somewhat demure, adding very little bulk to whatever it's attached to, and slotting into the socket end extremely securely. You can see the plug attached to the sprayer in the earlier image. When attached, they kind of act like a swivel, although they aren't 100% free spinning, there's a bit of friction that makes a swivel a nice accessory.</p>
<p>The eley shutoff is equally well-made, and very easy to thumb on and off. It just works so well its unremarkable, and therefore difficult to write about.</p> ]]></description>
    </item>
    <item>
       <title>Reading my electric meter with RTLSDR</title>
       <link>https://pdx.su/blog/2024-03-17-reading-my-electric-meter-with-rtlsdr</link>
       <pubDate>Sun, 17 Mar 2024 14:18:46 -06</pubDate>
       <guid>https://pdx.su/blog/2024-03-17-reading-my-electric-meter-with-rtlsdr</guid>
       <description><![CDATA[ <h1><a href="#reading-my-electric-meter-with-rtlsdr" aria-hidden="true" class="anchor" id="reading-my-electric-meter-with-rtlsdr"></a>Reading my electric meter with RTLSDR</h1>
<p>As I dive deeper into HomeAssistant automations, one thing has been nagging at me: my energy usage statistics. I have a solar array, battery, and associated systems installed on my house, and they integrate somewhat well with HomeAssistant. However, due to the way my house was wired, the solar/backup system is only aware of power usage for roughly half the house; the air conditioner and some other heavy appliances are on a separate circuit, that I chose not to have backed up due to their heavy loads. As such, any statistics in HomeAssistant related to power were only partially true; they were missing an entire chunk of my annual power consumption, leading to the values reported in HA differing from the values reported on my bill. I wasn't terribly happy with this, and so over time I've been investigating solutions.</p>
<p><img src="/.netlify/images?url=/postimages/energy-dashboard.png" alt="My Energy Dashboard for March 17, 2024" /></p>
<h2><a href="#background" aria-hidden="true" class="anchor" id="background"></a>Background</h2>
<h3><a href="#homeassistants-energy-dashboard-and-integrations" aria-hidden="true" class="anchor" id="homeassistants-energy-dashboard-and-integrations"></a>HomeAssistant's energy dashboard and integrations</h3>
<p>Back in 2021, <a href="https://www.home-assistant.io/blog/2021/08/04/home-energy-management/">HomeAssistant added a pretty robust energy integration</a>. Their system could integrate with gas and electric systems commonly found throughout Europe, via a standard called P1. Unfortunately, we don't have that standard here in the USA. When that post came out, I looked it over with interest, saw that it wasn't applicable to the USA, and filed it away under the &quot;some day&quot; file.</p>
<p>There were approaches for pulling power usage information from US systems; some electric utilities provide an API for access to their data, and there are many such integrations built into HomeAssistant, ripe for the pickings. Other people use current-sensing clamps, placed around the power as it enters their breaker box, or, for more granular data, around each circuit as it leaves the breaker box. Still other solutions exist, such certain smart home products that integrate and try to read electrical usage signatures to determine what is using power, or OCR systems that use a camera to watch their electric meter directly. But none of these really appealed to me; I didn't want to shove a bunch of current sensing clamps around each wire in my breaker box<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup>, and my meter is visible from the street, so a camera in a place that could accurately read the meter was a non-starter. So my usage mostly just stuck with the half accurate measures I was getting.</p>
<h3><a href="#smart-meters-in-the-usa" aria-hidden="true" class="anchor" id="smart-meters-in-the-usa"></a>Smart Meters in the USA</h3>
<p>In the USA, most smart meters send data along the ISM band, in a protocol called Encoder Receiver Transmitter, by Itron corporation. This applies to electric, gas, and water meters. Said protocol has a variety of message formats, but they're all fairly easy to work with. The meters generally have a barcode or similar tag on them, usually near the word/logo for &quot;Itron&quot;, with an 8 digit numeric ID.</p>
<h3><a href="#rtlsdr" aria-hidden="true" class="anchor" id="rtlsdr"></a>RTLSDR</h3>
<p>I've been aware of RTLSDR for a while, always minorly interested in it, but never actually doing anything with it. I'd heard rumblings over the years that people were using it for all sorts of interesting use cases, such as modernizing old 433MHz burglar alarm systems, reading data from weather stations, and so forth. But what really piqued my interest was reports of people using it to read data sent from smart meters.</p>
<p>Basically, you use a cheap radio dongle, like a DVB-T television tuner, as the raw radio, and then software (the S in SDR) to process the data you get and make it useful.</p>
<p>Since the smart meter is going to send the data regardless of who is reading it, and it's the <em>actual data the electric company uses for billing</em>, it seemed a prime candidate for my usage.</p>
<h3><a href="#rtlamr" aria-hidden="true" class="anchor" id="rtlamr"></a>RTLAMR</h3>
<p>There's an excellent GitHub project, which is the backbone of my setup, called <a href="https://github.com/bemasher/rtlamr/">rtlamr</a>. It works with data output via <a href="https://osmocom.org/projects/rtl-sdr/wiki/Rtl-sdr">rtl_tcp</a> from the rtl-sdr project, to talk to one of the aforementioned cheap dongles, listening in for messages from meters, optionally filtering by meter IDs. When it sees a message it understands, it decodes it, and prints it out as a formatted JSON object.</p>
<p>If you're not sure of your meter, you can just listen for <em>all</em> packets, and then pick yours out of the soup you get back.</p>
<p>Taking the outputted JSON data and sending it to HomeAssistant is a mostly trivial step, and there are already a few projects that do it.</p>
<h2><a href="#my-setup" aria-hidden="true" class="anchor" id="my-setup"></a>My Setup</h2>
<h3><a href="#prerequisites" aria-hidden="true" class="anchor" id="prerequisites"></a>Prerequisites</h3>
<p>First things first, you'll need a HomeAssistant installation running. You can still graph and chart all the data provided by rtlamr without it, but you're on your own.</p>
<p>With said HomeAssistant installation running, you'll also need to have the <a href="https://www.home-assistant.io/integrations/mqtt/">MQTT</a> integration running with an MQTT Broker. I use <a href="https://github.com/hassio-addons/addon-emqx">EMQX</a> becuase I like Erlang and graphs and everything else, but <a href="https://mosquitto.org">Mosquitto</a> works just fine. HomeAssistant claims RabbitMQs MQTT features don't work well for their use cases, and I never spent the time to prove them wrong.</p>
<h3><a href="#the-dongle" aria-hidden="true" class="anchor" id="the-dongle"></a>The dongle</h3>
<p>I purchased an <a href="https://amzn.to/3Q4y8KZ">RTLSDR Blog v4</a> from Amazon.com, as these have had good reviews over the years, and provide a nice <em>clean</em> data stream. People have had good luck with DVB-T shields for RPis, or USB DVB-T shields, but the price difference isn't that great, and so I went with a device I <em>knew</em> would work. I purchased the dongle <em>without</em> an antenna, as I wasn't quite sure where I was going to operate it. I have a few SMA connector antennas lying around, from other projects, but ultimately I went and bought a small antenna from a local RadioShack<sup class="footnote-ref"><a href="#fn-2" id="fnref-2" data-footnote-ref>2</a></sup> for this. There are many antennas that will work with this on Amazon, such as <a href="https://amzn.to/3Uh0BQ8">this one</a>, which looks very similar to the one I purchased from RadioShack. You just want to make sure the antenna you purchase works well in the 900MHz frequencies.</p>
<p>When you actually get the dongle, it's rather uneventful for setup. Plug it into your computer of choice, be it an RPi, server, or whatever else. You <em>don't have to run this on the same machine HomeAssistant is running on</em>. If you can't see the meter's readings where your server is, you can put the dongle/antenna on an RPi or other small computer in a place that can see the readings, as it will talk to HomeAssistant via MQTT.</p>
<h3><a href="#homeassistant-integration" aria-hidden="true" class="anchor" id="homeassistant-integration"></a>HomeAssistant Integration</h3>
<p>Searching around the internet lead me to <a href="https://github.com/ragingcomputer/amridm2mqtt">this repo by RagingComputer</a>. It looked like it might do <em>exactly</em> what I wanted; that is, package up RTLSDR, RTLAMR, and a bit of code to send messages across MQTT. However, in experimentation I was unable to get it running. There was an issue reported to the GitHub repo already, from two years ago, and the last commit was four years ago, so I figured the project was dead.</p>
<p>Feeling a bit lazy, I didn't really want to fork and fix it myself, so I set out to see if anyone else had solved it. Enter Allan Gomez GooD. He's got <a href="https://github.com/allangood/rtlamr2mqtt">an excellent repo</a> that does the same thing as the amridm2mqtt repo, but has gone the extra mile and built it out as a custom HomeAssistant addon. If you're running the dongle on the same system as your HomeAssistant installation, this is perfect. You can install the whole addon with a couple of clicks, and there are even <a href="https://github.com/allangood/rtlamr2mqtt#home-assistant-add-on">big friendly buttons</a> in the readme for doing just that.</p>
<p>If you run it on a separate computer, It's not much more complex, and the <a href="https://github.com/allangood/rtlamr2mqtt#docker-or-docker-compose">readme covers that as well</a></p>
<h3><a href="#configuring-rtlamr" aria-hidden="true" class="anchor" id="configuring-rtlamr"></a>Configuring RTLAMR</h3>
<p>Once you get it installed, either via HomeAssistant or as its own thing, you need to configure it. Configuration is rather straightforwards, and well documented.</p>
<p>For me, I went into EMQX and set up a custom user account for RTLAMR to send data to my MQTT system with. It can read and use the credentials HomeAssistant already has in its built in MQTT integration, but I like to keep things separated out; makes debugging and security easier.</p>
<p>Once I had that part configured, I had to figure out what meters were mine. I knew the number of my meter, it's printed on the front of the device, but that's where my knowledge stopped. I didn't know what format of messages it would send, and I didn't know how it would represent the data for import vs export (remember, I sell solar power <em>back</em> to the electric company). How would I figure this information out?</p>
<p>Fortunately, RTLAMR has you covered. There's a mode for listening to all meters, that can be dispatched as a one-off docker command:</p>
<pre class="athl"><code class="language-bash" translate="no" tabindex="0"><span class="line" data-line="1"><span class="function-call">docker</span> <span class="variable-parameter">run</span> <span class="variable-parameter">--rm</span> <span class="variable-parameter">-ti</span> <span class="variable-parameter">-e</span> <span class="variable-parameter">LISTEN_ONLY=yes</span> <span class="variable-parameter">-e</span> <span class="variable-parameter">RTL_MSGTYPE=</span><span class="string">&quot;all&quot;</span> <span class="variable-parameter">--device=/dev/bus/usb:/dev/bus/usb</span> <span class="variable-parameter">allangood/rtlamr2mqtt</span>
</span></code></pre>
<p>Running that on the machine with the dongle attached, you'll see the raw JSON messages as they are decoded. In my case, I saw big clusters of about 10 messages every 2 minutes, and then every 10 minutes or so I saw an even larger cluster of different messages.</p>
<p>Looking <em>into</em> the messages, I saw one message that had my meter number on it, along with a reading that was pretty close to what was on the front of the meter:</p>
<pre class="athl"><code class="language-json" translate="no" tabindex="0"><span class="line" data-line="1"><span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="2">  <span class="property">&quot;Message Type&quot;</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;SCM&quot;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="3">  <span class="property">&quot;ID&quot;</span><span class="punctuation-delimiter">:</span> #######<span class="number">4</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="4">  <span class="property">&quot;Type&quot;</span><span class="punctuation-delimiter">:</span> <span class="number">8</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="5">  <span class="property">&quot;TamperPhy&quot;</span><span class="punctuation-delimiter">:</span> <span class="number">0</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="6">  <span class="property">&quot;TamperEnc&quot;</span><span class="punctuation-delimiter">:</span> <span class="number">0</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="7">  <span class="property">&quot;Consumption&quot;</span><span class="punctuation-delimiter">:</span> <span class="number">1552536</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="8">  &quot;ChecksumVal&quot;<span class="punctuation-delimiter">:</span> #####
</span><span class="line" data-line="9"><span class="punctuation-bracket">&rbrace;</span>
</span></code></pre>
<p>But immediately following that, I saw two more messages, with their IDs just incremented off mine by 1 each time</p>
<pre class="athl"><code class="language-json" translate="no" tabindex="0"><span class="line" data-line="1"><span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="2">  <span class="property">&quot;Message Type&quot;</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;SCM&quot;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="3">  <span class="property">&quot;ID&quot;</span><span class="punctuation-delimiter">:</span> #######<span class="number">5</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="4">  <span class="property">&quot;Type&quot;</span><span class="punctuation-delimiter">:</span> <span class="number">8</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="5">  <span class="property">&quot;TamperPhy&quot;</span><span class="punctuation-delimiter">:</span> <span class="number">0</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="6">  <span class="property">&quot;TamperEnc&quot;</span><span class="punctuation-delimiter">:</span> <span class="number">0</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="7">  <span class="property">&quot;Consumption&quot;</span><span class="punctuation-delimiter">:</span> <span class="number">644391</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="8">  <span class="property">&quot;ChecksumVal&quot;</span><span class="punctuation-delimiter">:</span> #####
</span><span class="line" data-line="9"><span class="punctuation-bracket">&rbrace;</span>
</span><span class="line" data-line="10"><span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="11">  <span class="property">&quot;Message Type&quot;</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;SCM&quot;</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="12">  <span class="property">&quot;ID&quot;</span><span class="punctuation-delimiter">:</span> #######<span class="number">6</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="13">  <span class="property">&quot;Type&quot;</span><span class="punctuation-delimiter">:</span> <span class="number">8</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="14">  <span class="property">&quot;TamperPhy&quot;</span><span class="punctuation-delimiter">:</span> <span class="number">1</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="15">  <span class="property">&quot;TamperEnc&quot;</span><span class="punctuation-delimiter">:</span> <span class="number">1</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="16">  <span class="property">&quot;Consumption&quot;</span><span class="punctuation-delimiter">:</span> <span class="number">908145</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="17">  &quot;ChecksumVal&quot;<span class="punctuation-delimiter">:</span> #####
</span><span class="line" data-line="18"><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket"></span>
</span></code></pre>
<p>The message with the ID ending in 5 turned out to be my <em>export</em>, and the message ending with the 6 was the delta between the import and export. Why they send all three is a question for the power company. With the information I had, I could configure RTLAMR. My message type was SCM, and I had the two IDs for the meters I wanted. Entering those values in the config, I exited the listen all mode, and started up the HomeAssistant integration.</p>
<p>2 minutes later I had a reading, and my data was now ready to use.</p>
<p>I spent a bit more time faffing about trying to find an interval that didn't leave RTLAMR always listening, while keeping the data fairly fresh. By default, it sleeps for 5 minutes (300s) after a successful packet read, but through using the LISTEN ALL mode, I discovered my meter sends pretty accurately every 2 minutes. Since 5 does not divide cleanly into 2, I set my sleep interval to just shy of 120 seconds, so I'm getting every packet my radio sends, without wasting much effort.</p>
<p>You could probably do something similar just by watching the MQTT messages, using something like <a href="https://mqtt-explorer.com">MQTT Explorer</a>, but be aware of one caveat. If your meter doesn't report a change in the value, then no message will be sent to MQTT. I'm not sure if this is my server dropping duplicate messages or a feature built into the RTLAMR system I'm using, but it does reduce network congestion, so I'll take it.</p>
<p>If you leave the default configuration the addon came with, it will <em>automatically</em> create new sensors in your HomeAssistant install for all the meters you've added. If it doesn't see a meter you've told it to watch for, it won't add it <em>till it sees it</em>. This is the current situation of my Gas Meter, which I suspect only reports data when polled.</p>
<h3><a href="#configuring-homeassistant" aria-hidden="true" class="anchor" id="configuring-homeassistant"></a>Configuring HomeAssistant</h3>
<h4><a href="#without-tariff-data" aria-hidden="true" class="anchor" id="without-tariff-data"></a>Without Tariff Data</h4>
<p>If you don't care about how much your utility usage <em>costs</em>, or just have a single flat rate, I envy you. You get a much simpler configuration.</p>
<p>Head over to your <a href="https://my.home-assistant.io/redirect/energy/">Energy dashboard</a> in your HomeAssistant installation, and open up the config page. Under the relevant section, in my case Electricity Grid, add your relevant meters. For me, it <em>would</em> be the import meter under the consumption section, and the export meter under the Return to Grid section.</p>
<p>And that's it. You now have usage stats reported accurately. In a few hours, you'll see accurate usage information.</p>
<h4><a href="#with-tariff-data" aria-hidden="true" class="anchor" id="with-tariff-data"></a>With Tariff Data</h4>
<p>This is a somewhat more complicated flow, and involves the setup of multiple <a href="https://www.home-assistant.io/integrations/utility_meter/">Utility Meter helpers</a>, automations, and more. But if you want to get as accurate a picture of your costs, its worth it.</p>
<p>For me, my electric company breaks things down into a few tariff structures for purchasing:</p>
<ul>
<li><strong>Summertime</strong>
<ul>
<li><strong>First 400 kWh</strong>: $0.090279 / kWh</li>
<li><strong>All additional power</strong>: $0.11721 / kWh</li>
</ul>
</li>
<li><strong>Wintertime</strong>
<ul>
<li><strong>First 400 kWh</strong>: $0.079893 / kWh</li>
<li><strong>All additional power</strong>: $0.103725 / kWh</li>
</ul>
</li>
</ul>
<p>There's a similar, but simpler, pricing structure for export power:</p>
<ul>
<li><strong>Summertime</strong>: $0.05636 / kWh</li>
<li><strong>Wintertime</strong>: $0.04745 /kWh</li>
</ul>
<p>When I was just running the powerwall data, I made use of a <a href="https://github.com/paradox460/HomeAssistantConfig/blob/ff9980fb0bb3b32fbe089d215ed03949da99d6cc/templates/energy_rates.yaml">template</a> that just tracked time and cumulative usage from a single &quot;utility meter&quot; integration that reset with my billing period, and flip-flopped depending on the data, but this had the flaw that it wasn't accurately tracking the difference between the first 400 kWh and the remaining power usage. As soon as it flipped to the &quot;higher&quot; usage numbers, it could throw off older calculations.</p>
<p>The &quot;right&quot; way to do this is by creating utility meters for <em>each</em> bin of power usage, and using an automation to switch the currently active utility meter.</p>
<h5><a href="#utility-meters" aria-hidden="true" class="anchor" id="utility-meters"></a>Utility Meters</h5>
<p>For my use case, I set <a href="https://github.com/paradox460/HomeAssistantConfig/blob/bbaf7a4164ed67e2ee8d808e3695e875f763bc71/utility_meter.yaml">up 5 utility meters</a>, although you could make due with 3. I just like to have the extra day tracking for my own purposes.</p>
<p>I've set my utility meters to reset on the 27th, as that's the end of my billing cycle. I then create two meters, one that has tariffs applied, and one that does not. The one without tariffs is used to just track cumulative usage across the whole period. The meters with tariffs configured will actually show up as multiple different meters in HomeAssistant. Sure, you could sum up the values of the current seasonally active meters to get your 400 kWh threshold, but doing that logic in templates or automations is always a little more brittle than I like.</p>
<p>Once you've got the meters set up and have reloaded your HomeAssistant config, you should add <em>all</em> the tariff'd meters to your <a href="https://my.home-assistant.io/redirect/energy/">Energy Dashboard Config</a> along with their pricings as <em>fixed</em> values.</p>
<p>To switch between the bins, use an automation.</p>
<h5><a href="#active-meter-automation" aria-hidden="true" class="anchor" id="active-meter-automation"></a>Active Meter Automation</h5>
<p>You can do this pretty easily with native HomeAssistant Automations, but I didn't. I prefer to use NodeRed for my automations, and came up with something like this.</p>
<p><img src="/.netlify/images?url=/postimages/nodered-power-automation.png" alt="NodeRed graph showing the power meter automation" /></p>
<p>The &quot;Set Season Daily&quot; group starts with a timer that triggers every night at midnight, as well as on startup. This triggers a JavaScript function, which sets a NodeRed flow variable &quot;summer&quot; to a boolean true or false, depending on the current date. This JS function also splits output to two branches, depending on summmer. From these two branches, the current bin for the export power tariffs are set via a pair of select option service calls.</p>
<p>The Trigger state block in the Set Import Tariff group outputs depending on if the monthly meter without any tariff data reports greater than 400 kWh. Both branches, the greater than 400 kWh and the less-than, output to a switch statement, which reads the previously set flow variable for &quot;summer time&quot;, and then calls a select option service call that sets the active buckets for the two import utility meters.</p>
<details>
<summary>The NodeRed flow is available here</summary>
<pre class="athl"><code class="language-json" translate="no" tabindex="0"><span class="line" data-line="1"><span class="punctuation-bracket">[</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;tab&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;label&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;Electricity Tariffs&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;disabled&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;info&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;env&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;c2be4e7700b3d8e5&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;group&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;Set Season daily&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;style&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;label&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;nodes&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="string">&quot;163ac22ed1d95d60&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;51c6c4d86a4f87b8&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;1f1ac14da602b108&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">34</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">293</span><span class="punctuation-delimiter">,</span><span class="property">&quot;w&quot;</span><span class="punctuation-delimiter">:</span><span class="number">618</span><span class="punctuation-delimiter">,</span><span class="property">&quot;h&quot;</span><span class="punctuation-delimiter">:</span><span class="number">174</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;1d6ef5cec40e2629&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;group&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;Set Import Tariff&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;style&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;label&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;nodes&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="string">&quot;02ad35cf4db4cbd1&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;4bfdf3be32ae39ac&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;29c18e062e55fe14&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;ac28a1977f2b43f6&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;05cce05de54f7613&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;871107a9446c7041&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;35a21073487e74fc&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">34</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">19</span><span class="punctuation-delimiter">,</span><span class="property">&quot;w&quot;</span><span class="punctuation-delimiter">:</span><span class="number">972</span><span class="punctuation-delimiter">,</span><span class="property">&quot;h&quot;</span><span class="punctuation-delimiter">:</span><span class="number">242</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;1f1ac14da602b108&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;group&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;g&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;c2be4e7700b3d8e5&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;Set export tariff&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;style&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;label&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;nodes&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="string">&quot;0af23044806f9006&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;03d08d57cb753206&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">454</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">319</span><span class="punctuation-delimiter">,</span><span class="property">&quot;w&quot;</span><span class="punctuation-delimiter">:</span><span class="number">172</span><span class="punctuation-delimiter">,</span><span class="property">&quot;h&quot;</span><span class="punctuation-delimiter">:</span><span class="number">122</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;163ac22ed1d95d60&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;eztimer&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;g&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;c2be4e7700b3d8e5&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;debug&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;autoname&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;0:00:00&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;tag&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;eztimer&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;topic&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;suspended&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;sendEventsOnSuspend&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;latLongSource&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;haZone&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;latLongHaZone&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;zone.home&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;lat&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;41.1145565060444&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;lon&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;-111.91224648624485&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;timerType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;2&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;startupMessage&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span><span class="property">&quot;ontype&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;2&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;ontimesun&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;dawn&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;ontimetod&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;0:00:00&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;onpropertytype&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;msg&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;onproperty&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;payload&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;onvaluetype&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;num&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;onvalue&quot;</span><span class="punctuation-delimiter">:</span><span class="number">1</span><span class="punctuation-delimiter">,</span><span class="property">&quot;onoffset&quot;</span><span class="punctuation-delimiter">:</span><span class="number">0</span><span class="punctuation-delimiter">,</span><span class="property">&quot;onrandomoffset&quot;</span><span class="punctuation-delimiter">:</span><span class="number">0</span><span class="punctuation-delimiter">,</span><span class="property">&quot;onsuppressrepeats&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;offtype&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;1&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;offtimesun&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;dusk&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;offtimetod&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;dusk&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;offduration&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;00:01:00&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;offpropertytype&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;msg&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;offproperty&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;payload&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;offvaluetype&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;num&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;offvalue&quot;</span><span class="punctuation-delimiter">:</span><span class="number">0</span><span class="punctuation-delimiter">,</span><span class="property">&quot;offoffset&quot;</span><span class="punctuation-delimiter">:</span><span class="number">0</span><span class="punctuation-delimiter">,</span><span class="property">&quot;offrandomoffset&quot;</span><span class="punctuation-delimiter">:</span><span class="number">0</span><span class="punctuation-delimiter">,</span><span class="property">&quot;offsuppressrepeats&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;resend&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;resendInterval&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;0s&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mon&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span><span class="property">&quot;tue&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span><span class="property">&quot;wed&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span><span class="property">&quot;thu&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span><span class="property">&quot;fri&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span><span class="property">&quot;sat&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span><span class="property">&quot;sun&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">120</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">360</span><span class="punctuation-delimiter">,</span><span class="property">&quot;wires&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">[</span><span class="string">&quot;51c6c4d86a4f87b8&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;51c6c4d86a4f87b8&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;function&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;g&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;c2be4e7700b3d8e5&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;function 1&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;func&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;const month = new Date().getMonth();<span class="string-escape">\n</span><span class="string-escape">\n</span>if (month &gt;= 5 &amp;&amp; month &lt;= 9) &lbrace;<span class="string-escape">\n</span>    flow.set(&#39;summer&#39;, true);<span class="string-escape">\n</span>    return [&lbrace;payload: true&rbrace;, null];<span class="string-escape">\n</span>&rbrace; else &lbrace;<span class="string-escape">\n</span>    flow.set(&#39;summer&#39;, false);<span class="string-escape">\n</span>    return [null, &lbrace; payload: true &rbrace;];<span class="string-escape">\n</span>&rbrace;<span class="string-escape">\n</span><span class="string-escape">\n</span>&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputs&quot;</span><span class="punctuation-delimiter">:</span><span class="number">2</span><span class="punctuation-delimiter">,</span><span class="property">&quot;timeout&quot;</span><span class="punctuation-delimiter">:</span><span class="number">0</span><span class="punctuation-delimiter">,</span><span class="property">&quot;noerr&quot;</span><span class="punctuation-delimiter">:</span><span class="number">0</span><span class="punctuation-delimiter">,</span><span class="property">&quot;initialize&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;// Code added here will be run once<span class="string-escape">\n</span>// whenever the node is started.<span class="string-escape">\n</span>const month = new Date().getMonth();<span class="string-escape">\n</span><span class="string-escape">\n</span>if (month &gt;= 5 &amp;&amp; month &lt;= 9) &lbrace;<span class="string-escape">\n</span>    flow.set(&#39;summer&#39;, true)<span class="string-escape">\n</span>&rbrace; else &lbrace;<span class="string-escape">\n</span>    flow.set(&#39;summer&#39;, false)<span class="string-escape">\n</span>&rbrace;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;finalize&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;libs&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">280</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">360</span><span class="punctuation-delimiter">,</span><span class="property">&quot;wires&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">[</span><span class="string">&quot;0af23044806f9006&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">[</span><span class="string">&quot;03d08d57cb753206&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputLabels&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="string">&quot;summer&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;winter&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;02ad35cf4db4cbd1&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;trigger-state&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;g&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;1d6ef5cec40e2629&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;server&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;ffea7422.3895a8&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;version&quot;</span><span class="punctuation-delimiter">:</span><span class="number">4</span><span class="punctuation-delimiter">,</span><span class="property">&quot;inputs&quot;</span><span class="punctuation-delimiter">:</span><span class="number">0</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputs&quot;</span><span class="punctuation-delimiter">:</span><span class="number">2</span><span class="punctuation-delimiter">,</span><span class="property">&quot;exposeAsEntityConfig&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;entityId&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;sensor.electricity_import_month_total&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;entityIdType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;exact&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;debugEnabled&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;constraints&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;targetType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;this_entity&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;targetValue&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;propertyType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;current_state&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;propertyValue&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;new_state.state&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;comparatorType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&gt;=&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;comparatorValueDatatype&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;str&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;comparatorValue&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;400&quot;</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;customOutputs&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputInitially&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;stateType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;num&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;enableInput&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">250</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">120</span><span class="punctuation-delimiter">,</span><span class="property">&quot;wires&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">[</span><span class="string">&quot;4bfdf3be32ae39ac&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">[</span><span class="string">&quot;29c18e062e55fe14&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputLabels&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="string">&quot;gte 400&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;lt 400&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;4bfdf3be32ae39ac&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;switch&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;g&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;1d6ef5cec40e2629&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;is summer&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;property&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;summer&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;propertyType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;flow&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;rules&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;t&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;true&quot;</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;t&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;false&quot;</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;checkall&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;true&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;repair&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputs&quot;</span><span class="punctuation-delimiter">:</span><span class="number">2</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">710</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">80</span><span class="punctuation-delimiter">,</span><span class="property">&quot;wires&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">[</span><span class="string">&quot;ac28a1977f2b43f6&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">[</span><span class="string">&quot;05cce05de54f7613&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;29c18e062e55fe14&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;switch&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;g&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;1d6ef5cec40e2629&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;is summer&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;property&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;summer&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;propertyType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;flow&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;rules&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;t&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;true&quot;</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;t&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;false&quot;</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;checkall&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;true&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;repair&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputs&quot;</span><span class="punctuation-delimiter">:</span><span class="number">2</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">710</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">200</span><span class="punctuation-delimiter">,</span><span class="property">&quot;wires&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">[</span><span class="string">&quot;871107a9446c7041&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">[</span><span class="string">&quot;35a21073487e74fc&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;ac28a1977f2b43f6&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;api-call-service&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;g&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;1d6ef5cec40e2629&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;summer gt 400&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;server&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;ffea7422.3895a8&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;version&quot;</span><span class="punctuation-delimiter">:</span><span class="number">5</span><span class="punctuation-delimiter">,</span><span class="property">&quot;debugenabled&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;domain&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;select&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;service&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;select_option&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;areaId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;deviceId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;entityId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="string">&quot;select.energy_import_day&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;select.energy_import_month&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;data&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&lbrace;\&quot;option\&quot;:\&quot;summer_gt_400\&quot;&rbrace;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;dataType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;jsonata&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mergeContext&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mustacheAltTags&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputProperties&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;queue&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;none&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">900</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">60</span><span class="punctuation-delimiter">,</span><span class="property">&quot;wires&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;05cce05de54f7613&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;api-call-service&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;g&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;1d6ef5cec40e2629&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;winter gt 400&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;server&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;ffea7422.3895a8&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;version&quot;</span><span class="punctuation-delimiter">:</span><span class="number">5</span><span class="punctuation-delimiter">,</span><span class="property">&quot;debugenabled&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;domain&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;select&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;service&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;select_option&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;areaId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;deviceId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;entityId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="string">&quot;select.energy_import_day&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;select.energy_import_month&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;data&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&lbrace;\&quot;option\&quot;:\&quot;winter_gt_400\&quot;&rbrace;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;dataType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;jsonata&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mergeContext&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mustacheAltTags&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputProperties&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;queue&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;none&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">890</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">100</span><span class="punctuation-delimiter">,</span><span class="property">&quot;wires&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;871107a9446c7041&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;api-call-service&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;g&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;1d6ef5cec40e2629&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;summer lt 400&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;server&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;ffea7422.3895a8&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;version&quot;</span><span class="punctuation-delimiter">:</span><span class="number">5</span><span class="punctuation-delimiter">,</span><span class="property">&quot;debugenabled&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;domain&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;select&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;service&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;select_option&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;areaId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;deviceId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;entityId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="string">&quot;select.energy_import_day&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;select.energy_import_month&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;data&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&lbrace;\&quot;option\&quot;:\&quot;summer_lt_400\&quot;&rbrace;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;dataType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;jsonata&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mergeContext&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mustacheAltTags&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputProperties&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;queue&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;none&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">850</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">180</span><span class="punctuation-delimiter">,</span><span class="property">&quot;wires&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;35a21073487e74fc&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;api-call-service&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;g&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;1d6ef5cec40e2629&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;winter lt 400&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;server&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;ffea7422.3895a8&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;version&quot;</span><span class="punctuation-delimiter">:</span><span class="number">5</span><span class="punctuation-delimiter">,</span><span class="property">&quot;debugenabled&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;domain&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;select&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;service&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;select_option&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;areaId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;deviceId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;entityId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="string">&quot;select.energy_import_day&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;select.energy_import_month&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;data&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&lbrace;\&quot;option\&quot;:\&quot;winter_lt_400\&quot;&rbrace;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;dataType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;jsonata&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mergeContext&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mustacheAltTags&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputProperties&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;queue&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;none&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">840</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">220</span><span class="punctuation-delimiter">,</span><span class="property">&quot;wires&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;03d08d57cb753206&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;api-call-service&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;g&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;1f1ac14da602b108&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;winter&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;server&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;ffea7422.3895a8&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;version&quot;</span><span class="punctuation-delimiter">:</span><span class="number">5</span><span class="punctuation-delimiter">,</span><span class="property">&quot;debugenabled&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;domain&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;select&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;service&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;select_option&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;areaId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;deviceId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;entityId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="string">&quot;select.energy_export_day&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;select.energy_export_month&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;data&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&lbrace;\&quot;option\&quot;:\&quot;winter\&quot;&rbrace;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;dataType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;jsonata&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mergeContext&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mustacheAltTags&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputProperties&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;queue&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;none&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">530</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">400</span><span class="punctuation-delimiter">,</span><span class="property">&quot;wires&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;0af23044806f9006&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;api-call-service&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;z&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;7030ec85714e521d&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;g&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;1f1ac14da602b108&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;summer&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;server&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;ffea7422.3895a8&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;version&quot;</span><span class="punctuation-delimiter">:</span><span class="number">5</span><span class="punctuation-delimiter">,</span><span class="property">&quot;debugenabled&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;domain&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;select&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;service&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;select_option&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;areaId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;deviceId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;entityId&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="string">&quot;select.energy_export_day&quot;</span><span class="punctuation-delimiter">,</span><span class="string">&quot;select.energy_export_month&quot;</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;data&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&lbrace;\&quot;option\&quot;:\&quot;summer\&quot;&rbrace;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;dataType&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;jsonata&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mergeContext&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;mustacheAltTags&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;outputProperties&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span><span class="property">&quot;queue&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;none&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;x&quot;</span><span class="punctuation-delimiter">:</span><span class="number">540</span><span class="punctuation-delimiter">,</span><span class="property">&quot;y&quot;</span><span class="punctuation-delimiter">:</span><span class="number">360</span><span class="punctuation-delimiter">,</span><span class="property">&quot;wires&quot;</span><span class="punctuation-delimiter">:</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">[</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">]</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span><span class="punctuation-bracket">&lbrace;</span><span class="property">&quot;id&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;ffea7422.3895a8&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;type&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;server&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;name&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;Home Assistant&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;version&quot;</span><span class="punctuation-delimiter">:</span><span class="number">5</span><span class="punctuation-delimiter">,</span><span class="property">&quot;addon&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span><span class="property">&quot;rejectUnauthorizedCerts&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span><span class="property">&quot;ha_boolean&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;y|yes|true|on|home|open&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;connectionDelay&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span><span class="property">&quot;cacheJson&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span><span class="property">&quot;heartbeat&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">false</span><span class="punctuation-delimiter">,</span><span class="property">&quot;heartbeatInterval&quot;</span><span class="punctuation-delimiter">:</span><span class="number">30</span><span class="punctuation-delimiter">,</span><span class="property">&quot;areaSelector&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;friendlyName&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;deviceSelector&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;friendlyName&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;entitySelector&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;friendlyName&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;statusSeparator&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;at: &quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;statusYear&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;hidden&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;statusMonth&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;short&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;statusDay&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;numeric&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;statusHourCycle&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;h23&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;statusTimeFormat&quot;</span><span class="punctuation-delimiter">:</span><span class="string">&quot;h:m&quot;</span><span class="punctuation-delimiter">,</span><span class="property">&quot;enableGlobalContextStore&quot;</span><span class="punctuation-delimiter">:</span><span class="boolean">true</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">]</span>
</span></code></pre>
</details>
<h2><a href="#closing" aria-hidden="true" class="anchor" id="closing"></a>Closing</h2>
<p>I haven't yet got my water or gas meters integrated into this system. I plan on doing that eventually, but for now I'm happy with the electric results. The data won't be the most accurate for the remainder of this billing period, but it <em>should</em> reflect my next billing period fairly accurately, and I plan to check it.</p>
<h2><a href="#updates" aria-hidden="true" class="anchor" id="updates"></a>Updates</h2>
<h3><a href="#2024-03-23" aria-hidden="true" class="anchor" id="2024-03-23"></a>2024-03-23</h3>
<p>Since the article was published, my system picked up some messages from my gas meter, and now has a gas meter chart. I'd misconfigured the decimal place in rtlamr, and so had to dump my old readings and set it up again. I'll eventually set out to figure out a way to track gas tariffs and get estimates in there for now.</p>
<p>I spent some time tonight setting up the <a href="https://docs.digital-alchemy.app">digital-alchemy</a> add-on for HomeAssistant. It lets you write automations using typescript, with some nice tooling for VSCode or other typescript language server compatible editors. <a href="https://github.com/paradox460/HomeAssistantConfig/blob/739ae0618bb57620bfd7c33457e9a742d4b04e01/typescript/src/electricity-tariffs.ts">I ported my tariff switching logic out of NodeRED and into typescript</a>, and find the end result much simpler to reason about. I'll probably wind up doing the same style script for my gas tariff tracking.</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>Seriously, have you seen some of these installs? They look like horrific rats nests! <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
<li id="fn-2">
<p>I'm so ridiculously fortunate to have one of the few remaining RadioShacks nearby, so I try to get as much as I can justify from them, to help keep them afloat. <a href="#fnref-2" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="2" aria-label="Back to reference 2">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
    <item>
       <title>Fixing a broken smart cat feeder with ESP8266</title>
       <link>https://pdx.su/blog/2024-01-19-fixing-a-broken-smart-cat-feeder-with-esp32</link>
       <pubDate>Fri, 19 Jan 2024 18:28:48 -07</pubDate>
       <guid>https://pdx.su/blog/2024-01-19-fixing-a-broken-smart-cat-feeder-with-esp32</guid>
       <description><![CDATA[ <h1><a href="#fixing-a-broken-smart-cat-feeder-with-esp8266" aria-hidden="true" class="anchor" id="fixing-a-broken-smart-cat-feeder-with-esp8266"></a>Fixing a broken smart cat feeder with ESP8266</h1>
<p>Many years ago, I purchased a PetNet smart cat feeder. This one was well reviewed, and the app worked well enough not to be annoying. It let me set schedules, and dispense food in rather small increments, compared to its competition. Things worked fairly well for a few years, but in mid 2020, the company behind the product went out of business, and shut down their servers. The feeder would continue to work for a period, but you couldn't configure any settings. Eventually, it stopped working all-together.</p>
<p>Meaning to fix it &quot;sometime in the future,&quot; I put it in my garage, and forgot about it for a few years. Recently, I started mucking about with my HomeAssistant configuration, building up a new dashboard and getting my wife to use it, and started feeling the itch about the broken cat food feeder. Thinking it wouldn't be too hard, I went out and purchased an ESP8266, and popped open the feeder.</p>
<p><img src="/.netlify/images?url=/postimages/catfeeder-guts.jpg" alt="The guts of the cat feeder, opened for cleaning" /></p>
<p>The feeder was a mix of simple and complex. Simple, in that all that really <em>needs</em> to happen is the motor turns on at scheduled intervals, for a short period of time. Complex in that they built <em>so much more</em> functionality into the device than was really needed. This thing is <em>covered</em> in sensors; it's got two scales, ostensibly for measuring the weight of the hopper and the weight of food dispensed, a pair of infrared sensors to detect when the hopper is empty, sensors that monitor the motor's rotations and position, and probably others that I haven't figured out the purpose of.</p>
<p>Since the sensors are things I didn't really need to worry about, all I had to do here was trigger the motor for a burst of time, at a fixed interval of times. I also wanted to be able to trigger it remotely from my phone or a similar interface. This is pretty easy to do with ESPhome.</p>
<p>Wiring up the device wasn't too complex. The device came from the factory with a decent built-in power supply, running over USB 2 on a Micro-B port. The motor, sensors, and everything else plugged into the main board via little JST connectors. The ESP8266 devboard I used can be powered by either a 3.3 or a 5 VDC connection. To control the motor, I used a relay, wiring it directly to the incoming power supply and motor. Since the onboard power supply provides 5v, and the motor is 5v rated, I powered the devboard using 5v, using the 3.3v output to power the relay board, and triggering the board via a GPIO pin.</p>
<p>ESPHome makes the software side even easier. Getting the board flashed and talking to my HomeAssistant system was so trivial I was astonished. I just plugged the devboard into my computer, went to the ESPHome website, and, via the powers of WebSerial, flashed it with the ESPhome base firmware and got it set up on my wifi. From there, HomeAssistant &quot;saw&quot; the device and gave me the option to adopt it. This whole process took about 5 minutes. That's faster than the setup and adoption of some purpose-made &quot;smart home&quot; systems!</p>
<p>Changing the device to actually do what I needed wasn't much more complicated. Using ESPHome primitives, I set up a GPIO output pin, and a &quot;Button entity&quot; to trigger this pin for a second and a half. Finally, I set up a timer entity that triggers the button at a few fixed times throughout the day.</p>
<p>Once I'd put the whole device back together, I powered it up and added some catfood to the hopper. Triggering the button from HomeAssistant, I watched happily as catfood came pouring out of the dispenser. Pressing the button a second time resulted in no catfood, and a buzzing sound from the motor. After some trial and error, I eventually swapped to a smaller size of catfood pellet, and then ultimately to a different USB power supply. The original one that came with the feeder said 5v 1A on its nameplate, but after testing with a meter, I was only getting 3.3v and barely 100mA. Now the dispenser is triggering happily and consistently, and our cat no longer pesters us throughout the day for more food.</p>
<p><img src="/.netlify/images?url=/postimages/cat-eating.jpg" alt="A happy cat" /></p>
<p>If you are interested, you can see the configuration I wrote for the cat feeder <a href="https://github.com/paradox460/HomeAssistantConfig/blob/226583d02f2ba59565dc635673aa5e8a91ca5958/esphome/esphome-web-659621.yaml">here</a></p>
<h2><a href="#updates" aria-hidden="true" class="anchor" id="updates"></a>Updates</h2>
<p>HackerNews discussion prompted me to make the following clarifications: I used an ESP8266, not an ESP32.</p>
<p>Additionally, this is not the cat's primary food source; he gets two bowls of wet food a day, so the dry food is mostly for supplemental feeding and dental health.</p> ]]></description>
    </item>
    <item>
       <title>Migrating to Tableau SSG</title>
       <link>https://pdx.su/blog/2023-11-14-migrating-to-tableau-ssg</link>
       <pubDate>Tue, 14 Nov 2023 19:05:04 -07</pubDate>
       <guid>https://pdx.su/blog/2023-11-14-migrating-to-tableau-ssg</guid>
       <description><![CDATA[ <h1><a href="#migrating-to-tableau-ssg" aria-hidden="true" class="anchor" id="migrating-to-tableau-ssg"></a>Migrating to Tableau SSG</h1>
<p>Recently I moved this site off of the older Nuxt/Nuxt-content based static site generator to <a href="https://github.com/elixir-tools/tableau">Tableau</a>, an Elixir based SSG. Between not really ever being fully comfortable with how &quot;magical&quot; Nuxt content was, and some recent changes broke some aspects I was using. This migration has been fairly enjoyable, and I was able to contribute some of my changes back to Tableau.</p>
<h2><a href="#previously-on-pdxsu" aria-hidden="true" class="anchor" id="previously-on-pdxsu"></a>Previously, on pdx.su…</h2>
<p>Previously, I was using <a href="https://nuxt.com/">Nuxt</a> and <a href="https://content.nuxt.com">Nuxt-Content</a>. This was a fairly enjoyable stack, for the most part. I could write posts in Markdown, add my own custom components to be used within Markdown documents, and publishing was relatively straightforwards.</p>
<p>But it was often a black box. I could tweak some things about it, most of its layout and even the way some components in its output rendered, but things such as some head tags, and particular aspects of inline code, were always off-limits. Nuxt was very nice for quickly getting a site up and running, but I never really felt that I was in complete control over how it built itself. Its output was nice and fast, and the site rendered rather quickly, but given I was using no server-side rendering, and only using pre-generated HTML output, the server-side JS features ultimately proved more of a headache than a utility.</p>
<p>Getting the opengraph header meta tags to render properly for each post was a bit more difficult than it should have been, and I ultimately didn't get <em>all</em> the tags I should have.</p>
<p>But the biggest source of pain for me was code blocks. This is, nominally, a technical blog. I write about code a lot. And so having code blocks that look good is <em>vital</em>. Nuxt-Content uses a neat system where it processes Markdown code blocks, and lets you customize the output renderer by defining your own vue components. This is how I added things like a copy button, and some other nice decorations.</p>
<p>Unfortunately, those custom components don't extend to the actual syntax highlighting. When I initially made this site at the beginning of 2023, it used the Shiki syntax highlighting library. This library is pretty good, supports a decent amount of languages, and has a nice &quot;css-variables&quot; theme that lets you customize the output via CSS variables. I used this feature to enable use of the Base16 tomorrow syntax theme, and have light and dark mode versions.</p>
<p>But recently they moved over to a different fork of that highlighter, called Shikiji. This fork has some real improvements, such as actual light/dark support. However, it doesn't support the css-variables theme. It also doesn't ship with the tomorrow theme variants, and attempting to manually add them caused issues. I opened an issue against Nuxt-content regarding the problem, so hopefully it will be fixed in the future.</p>
<h2><a href="#contributing-to-tableau" aria-hidden="true" class="anchor" id="contributing-to-tableau"></a>Contributing to Tableau</h2>
<p>I've been loosely aware of an Elixir based static site generator, called Tableau. Developed by Mitch Hanberg and part of the elixir-tools project, it seemed like a pretty good solution. I know Elixir, it's the primary language I code in these days. Mitch is a pretty great guy as well, and his code is always clear and easy to read. So it seemed like a natural fit for what I was trying to do.</p>
<p>At the time I was looking, Tableau was somewhat &quot;incomplete,&quot; from my perspective. It was capable of doing all the nominal things it should do, and was being actively dogfooded, as it was what powered the elixir-tools site. But it was lacking a number of the niceities that other, older SSGs already have. It required a fair amount of &quot;stuff&quot; to be in the frontmatter of <em>every</em> post, that could be abstracted away to either defaults or somewhat &quot;smart&quot; generation at compile time.</p>
<p>The best way to get an open-source project to meet your needs is to directly contribute the code that enables it to do so yourself. And so that is what I did. I forked the repo, and added a variety of the features I wanted, over a few weeks, and opened pull requests for them. Mitch gave feedback where needed, tweaked the code to match the style he likes for Tablea, and ultimately merged in my changes. Not my first open-source contribution, not by a long shot, but it always feels good when that happens.</p>
<p>With the improvements to the post (and pages) systems in place, I contributed a sitemap feature, and got down to business, porting my site over.</p>
<h2><a href="#porting-my-site" aria-hidden="true" class="anchor" id="porting-my-site"></a>Porting my site</h2>
<p>With this current incarnation of my site (there are some older ones lost to time, and I don't care to revisit them), I generally kept things pretty simple. Most of my content would be in posts, and posts would largely exist &quot;alone.&quot; A reader who was interested could scan all that I'd written, but I wouldn't use annoying antipatterns to try and coerce them into it.</p>
<p>Most of the focus was on the prose, how easy it was to read. There were some clever amenities I wanted to keep; the table of contents on desktop, &quot;smart&quot; timestamps that show the user's local formatting and zone, and some enhanced markdown features MDC offered in Nuxt, but none of those were really blockers that would prevent me from doing what I needed.</p>
<p>Starting from the basics, setting up a new project to use Tableau was rather easy. Set up the Elixir project via <code>mix new</code>, add the dependencies, and write the configuration file. Writing the root template, and then descendent templates of post and page, was rather trivial. If you've ever written code for Rails or Phoenix, you would feel right at home. Compared to some &quot;magic&quot; other SSGs I've tried, it was refreshing to just have to do it yourself.</p>
<h3><a href="#temple-templates" aria-hidden="true" class="anchor" id="temple-templates"></a>Temple templates</h3>
<p>Tableau lets you use whatever templating language you want; I could have used Slime, an Elixir Slimlang port, that I've used in the past, but Mitch also has his own templating language, called <a href="https://github.com/mhanberg/temple">Temple</a>, which I wanted to experiment with. Temple characterizes itself by <em>being</em> nothing but &quot;real&quot; Elixir code. You write temple templates like this:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="function-call">temple</span> <span class="keyword">do</span>
</span><span class="line" data-line="2">  <span class="function-call">div</span> <span class="string-special-symbol">id: </span><span class="string">&quot;some_id&quot;</span><span class="punctuation-delimiter">,</span> <span class="string-special-symbol">class: </span><span class="string">&quot;foo&quot;</span> <span class="keyword">do</span>
</span><span class="line" data-line="3">    <span class="string">&quot;This is temple code&quot;</span>
</span><span class="line" data-line="4">  <span class="keyword">end</span>
</span><span class="line" data-line="5">  <span class="keyword">if</span> <span class="operator"><span class="constant">@<span class="constant">some_assigns</span></span></span> <span class="keyword">do</span>
</span><span class="line" data-line="6">    <span class="function-call">img</span> <span class="string-special-symbol">src: </span><span class="string">&quot;image.jpg&quot;</span><span class="punctuation-delimiter">,</span> <span class="string-special-symbol">alt: </span><span class="string">&quot;an image&quot;</span>
</span><span class="line" data-line="7">  <span class="keyword">end</span>
</span><span class="line" data-line="8"><span class="keyword">end</span>
</span></code></pre>
<p>That's it. There are no oddities around switching to or from control logic, no funky characters to control inline whitespace, and ultimately no new syntax needed.</p>
<p>And temple also supports components, similar to Phoenix LiveView, Surface, or your choice of JS framework (react, vue, lit, etc.). You just define a component as a function, and call it via the <code>c</code> macro, from within a <code>temple do…end</code> block.</p>
<p>This let me quickly throw together all the &quot;static&quot; parts of the site that I needed.</p>
<h3><a href="#table-of-contents" aria-hidden="true" class="anchor" id="table-of-contents"></a>Table of Contents</h3>
<p>For the ToC, I found the best way to generate it was to parse the generated markdown, extract the headings, and store them in an attribute. Then on each Post render, I could just pull the value, loop over it, and render it as needed.</p>
<p>I created a Tableau extension to handle this, that runs after the Markdown documents have been parsed and compiled, and outputs the appropriate data of the headers. Parsing was done using <a href="https://github.com/philss/floki">Floki</a>, and was quite easy to do.</p>
<p>The final bit of enhancements around the ToC were pretty easy to port over from my older Vue based site, in Javascript. I register a simple IntersectionObserver, which keeps track of which headers are visible within the viewport, and updates elements within the ToC accordingly.</p>
<p>A last bit of CSS was used to make the targeted header flash a few times when navigated to. Previously I used JS to hook into the Vue router and detect these changes, but doing it in pure CSS feels elegant.</p>
<h3><a href="#timestamps-and-notes" aria-hidden="true" class="anchor" id="timestamps-and-notes"></a>Timestamps and Notes</h3>
<p>For the timestamps and note component, I didn't want to toss Vue into the page again, but still wanted them to be somewhat interactive. The timestamp component can <em>only</em> be done with some client-side Javascript, as there is no server, and all pages are static. The markdown notes could have been parsed and rendered on the server side, by a Markdown pre-processor, but it was still a fun exercise to do them as client-side components.</p>
<p>Ultimately, I went with <a href="https://lit.dev">lit components</a>. Lit components provide a nice little library atop HTML custom elements, which have pretty excellent support across the board. Registering a component is simple enough, and using it is even simpler. The API lit provides is nicely similar to both react and vue, so anyone who is familiar with them should be able to pick it up rather quickly. And its heavy use of typescript decorators make it rather simple to write.</p>
<p>For the in-text notes, I didn't bother with adding a <code>noscript</code> equivalent. If you don't have JS enabled, sorry, they just won't render. You won't see anything broken, but they'll just be absent.</p>
<p>For timestamps, this was not an acceptable compromise. So I wrapped the timestamp calls in a Temple component, rendered server side, that outputs the custom element and a noscript wrapped <code>&lt;time&gt;</code> tag. If you have JS turned off, you'll see a date in US formatting. If you have JS on, the timestamps will be rendered according to what your browser says is appropriate for your current location and locale.</p>
<h3><a href="#assets" aria-hidden="true" class="anchor" id="assets"></a>Assets</h3>
<p>Tableau uses an approach similar to how Phoenix handles custom asset processors: it just lets you call out to them during the <code>development</code> phase, and that's it. It isn't aware of anything special regarding CSS, JS, TypeScript, and doesn't need to be.</p>
<p>Because of this, my asset processing pipeline is fairly boring and regular TypeScript and SCSS, built using esbuild. Tableau starts up the esbuild watcher, which compiles files on changes, emits them to the output directory, which then triggers Tableau's file watchers to send a refresh event to the browser.</p>
<p>For production, I just call the esbuild build script as part of my GitHub Action.</p>
<p>Keeping assets and code separate may feel a bit antiquated, but it's ultimately rather simple, and with modern CSS features such as scoping, nesting, and customizable cascade layers, all the pain points from the past are pretty much absent.</p>
<h2><a href="#its-all-open-source" aria-hidden="true" class="anchor" id="its-all-open-source"></a>It's all open source</h2>
<p>The previous &quot;build&quot; of this site was in a private GitHub repository, mostly because I wasn't the happiest with the quality of the code. But since there are comparably fewer Tableau sites to Nuxt sites, I felt that having this one be open-source would be a nice way for people interested in Tableau to see how some things work.</p>
<p>So you can check out my site, or view the source of a post, at <a href="https://github.com/paradox460/pdx.su">https://github.com/paradox460/pdx.su</a>.</p> ]]></description>
    </item>
    <item>
       <title>CalVer for Release Drafter</title>
       <link>https://pdx.su/blog/2023-11-06-calver-for-release-drafter</link>
       <pubDate>Mon, 06 Nov 2023 16:21:53 -07</pubDate>
       <guid>https://pdx.su/blog/2023-11-06-calver-for-release-drafter</guid>
       <description><![CDATA[ <h1><a href="#calver-for-release-drafter" aria-hidden="true" class="anchor" id="calver-for-release-drafter"></a>CalVer for Release Drafter</h1>
<p>Recently, I wanted to use <a href="https://calver.org/">CalVer</a> with <a href="https://github.com/release-drafter/release-drafter">Release Drafter</a>. This is for a project where the SemVer approach would result in a perpetually increasing patch version number, and a practically frozen major and minor version. Unfortunately, Release Drafter has no inbuilt support for CalVer, so we've gotta calculate version numbers ourselves.</p>
<pre class="athl"><code class="language-javascript" translate="no" tabindex="0"><span class="line" data-line="1"><span class="keyword-function">function</span> <span class="function">parseVersion</span><span class="punctuation-bracket">(</span><span class="variable-parameter">version</span><span class="punctuation-bracket">)</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="2">  <span class="keyword-conditional">if</span> <span class="punctuation-bracket">(</span><span class="operator">!</span><span class="variable">version</span><span class="punctuation-bracket">)</span> <span class="keyword-return">return</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="3">
</span><span class="line" data-line="4">  <span class="keyword">const</span> <span class="variable">regexp</span> <span class="operator">=</span> <span class="punctuation-bracket">/</span><span class="punctuation-delimiter"><span class="string-regexp">^</span>v<span class="operator">?</span><span class="punctuation-bracket">(?&lt;</span><span class="property">calVer</span><span class="punctuation-bracket">&gt;</span><span class="string-escape">\d</span><span class="operator">+</span><span class="string-regexp">\.</span><span class="string-escape">\d</span><span class="operator">+</span><span class="punctuation-bracket">)</span><span class="string-regexp">\.</span><span class="punctuation-bracket">(?&lt;</span><span class="property">incremental</span><span class="punctuation-bracket">&gt;</span><span class="string-escape">\d</span><span class="operator">+</span><span class="punctuation-bracket">)</span></span><span class="punctuation-bracket">/</span><span class="character-special">i</span>
</span><span class="line" data-line="5">  <span class="keyword">const</span> <span class="variable">matches</span> <span class="operator">=</span> <span class="variable">version</span><span class="punctuation-delimiter">.</span><span class="function-method-call">match</span><span class="punctuation-bracket">(</span><span class="variable">regexp</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="6">  <span class="keyword-conditional">if</span> <span class="punctuation-bracket">(</span><span class="operator">!</span><span class="variable">matches</span><span class="punctuation-bracket">)</span> <span class="keyword-return">return</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="7">
</span><span class="line" data-line="8">  <span class="keyword">const</span> <span class="punctuation-bracket">&lbrace;</span> <span class="variable">calVer</span><span class="punctuation-delimiter">,</span> <span class="variable">incremental</span> <span class="punctuation-bracket">&rbrace;</span> <span class="operator">=</span> <span class="variable">matches</span><span class="punctuation-delimiter">.</span><span class="variable-member">groups</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="9">
</span><span class="line" data-line="10">  <span class="keyword-return">return</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="11">    <span class="variable-member">calVer</span><span class="punctuation-delimiter">,</span>
</span><span class="line" data-line="12">    <span class="variable-member">incremental</span><span class="punctuation-delimiter">:</span> <span class="function-builtin">parseInt</span><span class="punctuation-bracket">(</span><span class="variable">incremental</span><span class="punctuation-bracket">)</span>
</span><span class="line" data-line="13">  <span class="punctuation-bracket">&rbrace;</span>
</span><span class="line" data-line="14"><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="15">
</span><span class="line" data-line="16"><span class="keyword-function">function</span> <span class="function">currentCalVer</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="17">  <span class="keyword">const</span> <span class="variable">date</span> <span class="operator">=</span> <span class="keyword-operator">new</span> <span class="constructor">Date</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="18">  <span class="keyword-return">return</span> <span class="string">`$&lbrace;<span class="variable">date</span><span class="punctuation-delimiter">.</span><span class="function-method-call">getUTCFullYear</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span><span class="punctuation-special">&rbrace;</span>.$&lbrace;<span class="variable">date</span><span class="punctuation-delimiter">.</span><span class="function-method-call">getUTCMonth</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span> <span class="operator">+</span> <span class="number">1</span><span class="punctuation-special">&rbrace;</span>`</span>
</span><span class="line" data-line="19"><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="20">
</span><span class="line" data-line="21"><span class="variable-builtin">module</span><span class="punctuation-delimiter">.</span><span class="function-method">exports</span> <span class="operator">=</span> <span class="keyword-coroutine">async</span> <span class="punctuation-bracket">(</span><span class="variable-parameter">github</span><span class="punctuation-delimiter">,</span> <span class="variable-parameter">context</span><span class="punctuation-bracket">)</span> <span class="operator">=&gt;</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="22">  <span class="keyword">const</span> <span class="variable">calVerDate</span> <span class="operator">=</span> <span class="function-call">currentCalVer</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="23">
</span><span class="line" data-line="24">  <span class="keyword">const</span> <span class="variable">latestRelease</span> <span class="operator">=</span> <span class="punctuation-bracket">(</span><span class="keyword-coroutine">await</span> <span class="variable">github</span><span class="punctuation-delimiter">.</span><span class="variable-member">rest</span><span class="punctuation-delimiter">.</span><span class="variable-member">repos</span><span class="punctuation-delimiter">.</span><span class="function-method-call">getLatestRelease</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">&lbrace;</span> <span class="variable-member">owner</span><span class="punctuation-delimiter">:</span> <span class="variable">context</span><span class="punctuation-delimiter">.</span><span class="variable-member">repo</span><span class="punctuation-delimiter">.</span><span class="variable-member">owner</span><span class="punctuation-delimiter">,</span> <span class="variable-member">repo</span><span class="punctuation-delimiter">:</span> <span class="variable">context</span><span class="punctuation-delimiter">.</span><span class="variable-member">repo</span><span class="punctuation-delimiter">.</span><span class="variable-member">repo</span> <span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">)</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">.</span><span class="variable-member">data</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="25">  <span class="keyword">const</span> <span class="variable">parsedVersion</span> <span class="operator">=</span> <span class="function-call">parseVersion</span><span class="punctuation-bracket">(</span><span class="variable">latestRelease</span><span class="punctuation-delimiter">.</span><span class="variable-member">tag_name</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="26">
</span><span class="line" data-line="27">  <span class="keyword-conditional">if</span> <span class="punctuation-bracket">(</span><span class="operator">!</span><span class="variable">parsedVersion</span><span class="punctuation-bracket">)</span> <span class="keyword-return">return</span> <span class="string">`$&lbrace;<span class="variable">calVerDate</span><span class="punctuation-special">&rbrace;</span>.0`</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="28">
</span><span class="line" data-line="29">  <span class="keyword-conditional">if</span> <span class="punctuation-bracket">(</span><span class="variable">parsedVersion</span><span class="punctuation-delimiter">.</span><span class="variable-member">calVer</span> <span class="operator">===</span> <span class="variable">calVerDate</span><span class="punctuation-bracket">)</span> <span class="keyword-return">return</span> <span class="string">`$&lbrace;<span class="variable">calVerDate</span><span class="punctuation-special">&rbrace;</span>.$&lbrace;<span class="variable">parsedVersion</span><span class="punctuation-delimiter">.</span><span class="variable-member">incremental</span> <span class="operator">+</span> <span class="number">1</span><span class="punctuation-special">&rbrace;</span>`</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="30"><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">;</span>
</span></code></pre>
<p>This script is meant to be used with Github's <a href="https://github.com/actions/github-script">actions/script</a> workflow, which allows you to use JavaScript inside an Actions workflow. You'd call it something like this:</p>
<pre class="athl"><code class="language-yaml" translate="no" tabindex="0"><span class="line" data-line="1"><span class="property">name</span><span class="punctuation-delimiter">:</span> <span class="string">Release Drafter</span>
</span><span class="line" data-line="2">
</span><span class="line" data-line="3"><span class="property">jobs</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="4">  <span class="property">update_release_draft</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="5">    <span class="property">permissions</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="6">      <span class="property">contents</span><span class="punctuation-delimiter">:</span> <span class="string">write</span>
</span><span class="line" data-line="7">      <span class="property">pull-requests</span><span class="punctuation-delimiter">:</span> <span class="string">read</span>
</span><span class="line" data-line="8">    <span class="property">runs-on</span><span class="punctuation-delimiter">:</span> <span class="string">ubuntu-latest</span>
</span><span class="line" data-line="9">    <span class="property">steps</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="10">      <span class="punctuation-delimiter">-</span> <span class="property">uses</span><span class="punctuation-delimiter">:</span> <span class="string">actions/checkout@v4</span>
</span><span class="line" data-line="11">      <span class="punctuation-delimiter">-</span> <span class="property">name</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;Generate CalVer&quot;</span>
</span><span class="line" data-line="12">        <span class="property">uses</span><span class="punctuation-delimiter">:</span> <span class="string">actions/github-script@v6</span>
</span><span class="line" data-line="13">        <span class="property">id</span><span class="punctuation-delimiter">:</span> <span class="string">calver</span>
</span><span class="line" data-line="14">        <span class="property">with</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="15">          <span class="property">result-encoding</span><span class="punctuation-delimiter">:</span> <span class="string">string</span>
</span><span class="line" data-line="16">          <span class="property">script</span><span class="punctuation-delimiter">:</span> <span class="string"><span class="punctuation-delimiter">|</span></span>
</span><span class="line" data-line="17"><span class="string">            <span class="function-call">const</span> <span class="variable-parameter">genCalVer</span> <span class="variable-parameter">=</span> <span class="variable-parameter">require</span><span class="punctuation-delimiter"></span><span class="punctuation-bracket">(</span><span class="string">&#39;./.github/workflow-scripts/calver.js&#39;</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span></span>
</span><span class="line" data-line="18"><span class="string">            <span class="function-call">const</span> <span class="variable-parameter">version</span> <span class="variable-parameter">=</span> <span class="variable-parameter">await</span> <span class="variable-parameter">genCalVer</span><span class="punctuation-delimiter"></span><span class="punctuation-bracket">(</span><span class="function-call">github,</span> <span class="variable-parameter">context</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span></span>
</span><span class="line" data-line="19"><span class="string">            <span class="function-builtin">return</span> <span class="variable-parameter">version</span><span class="punctuation-delimiter">;</span></span>
</span><span class="line" data-line="20">      <span class="punctuation-delimiter">-</span> <span class="property">uses</span><span class="punctuation-delimiter">:</span> <span class="string">release-drafter/release-drafter@v5</span>
</span><span class="line" data-line="21">        <span class="property">env</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="22">          <span class="property">GITHUB_TOKEN</span><span class="punctuation-delimiter">:</span> <span class="string">$&lbrace;&lbrace; secrets.PAT_TOKEN &rbrace;&rbrace;</span>
</span><span class="line" data-line="23">        <span class="property">with</span><span class="punctuation-delimiter">:</span>
</span><span class="line" data-line="24">          <span class="property">prerelease</span><span class="punctuation-delimiter">:</span> <span class="string">$&lbrace;&lbrace; github.event_name != &#39;pull_request&#39; &rbrace;&rbrace;</span>
</span><span class="line" data-line="25">          <span class="property">publish</span><span class="punctuation-delimiter">:</span> <span class="boolean">false</span>
</span><span class="line" data-line="26">          <span class="property">version</span><span class="punctuation-delimiter">:</span> <span class="string">&quot;$&lbrace;&lbrace; steps.calver.outputs.result &rbrace;&rbrace;&quot;</span>
</span></code></pre>
<p>Note that the <code>version</code> input is provided to the release-drafter script. This overrides release drafter's internal version calculations, setting it to the output result of our script.</p> ]]></description>
    </item>
    <item>
       <title>CSS is fun again</title>
       <link>https://pdx.su/blog/2023-10-25-css-is-fun-again</link>
       <pubDate>Wed, 25 Oct 2023 18:57:41 -06</pubDate>
       <guid>https://pdx.su/blog/2023-10-25-css-is-fun-again</guid>
       <description><![CDATA[ <h1><a href="#css-is-fun-again" aria-hidden="true" class="anchor" id="css-is-fun-again"></a>CSS is fun again</h1>
<p>CSS has been undergoing a quiet renaissance lately. Lots of big features which previously required an external tool to use, are now native parts of the language, and its growing more and more all the time. If you haven't used CSS in a long time, for whatever reason, now is the time to take a look again.</p>
<h2><a href="#brief-history-and-how-css-became-not-fun" aria-hidden="true" class="anchor" id="brief-history-and-how-css-became-not-fun"></a>Brief history, and how CSS became &quot;not-fun&quot;</h2>
<p>Back in the late 90s, we styled our websites using inline attributes. <code>bgcolor</code>, <code>font</code>, and friends ruled the roost. And this was okay. If you wanted a more complex style, you used tables and sliced up images. Or you just used Flash, but thats a whole different beast. When CSS came out, it was pretty cool, but far too simple to really do what we needed. You could set colors and sizes and some positional properties, but that was kind of the limit. You still had to use tables to do complex layouts. You still had to do image slicing for round corners and shadows and other silly things, but it was &quot;better.&quot; CSS 2 rolled around and gave us some primitives for actually positioning our stuff. We could move things around the page, float them off to the side, and adjust a lot of how they looked. CSS was finally viable for doing the whole page. We ditched tables. And we quickly ran into the limits of what we could do. Rounded corners still needed image slicing. Complex positioning, i.e. grids (so complex!) required JavaScript. And for many people, this is where CSS stagnated.
CSS didn't really stagnate. It slowly marched forwards, growing things like shadows, border-radius, and then later flexbox and grids. But, despite this slow progress, it had gathered a reputation of being brittle and difficult to work with. Tricks like clearfixes arose; bits of knowledge you just had to find and use, and then apply like cheap plaster in a house you intend to flip. Reset styles arrived to make things somewhat consistent. External tools like Sass and PostCSS streamlined the process of authoring CSS, making it so you didn't have to remember all the browser prefixes or how to write all the border radiuses. And eventually, some developers just started throwing away parts of CSS, favoring simpler approaches, while not being sure exactly of what they were giving up, but certain they didn't need it.</p>
<h2><a href="#the-quiet-renaissance" aria-hidden="true" class="anchor" id="the-quiet-renaissance"></a>The quiet renaissance</h2>
<p>As time passed, CSS slowly grew new features. Most of them were novel, but constrained in their utility. Selectors like <code>:is</code> and <code>:where</code>, while useful, only slightly moved the needle in terms of just how much code you had to write. Flexbox and CSS Grids arrived, made things easier for those who used them, but received less fanfare than they deserved; many sites and layout frameworks still use older methods, the ones the developers learned through fire trials.</p>
<p>Variables, or custom properties, made a big splash, particularly because they can do things that couldn't be done otherwise. Preprocessor variables are not context aware, they can't be reset using things like media styles or user preferences, and so they're basically just super-fancy find-and-replace placeholders.</p>
<p>But recently, the pace of new features has accelerated so much, that I envy those who are just learning CSS today, and don't have to learn all the baggage and old ways of doing things. They'll just see all the new tools at their workbenches, and get down to work.</p>
<p>CSS Nesting is the biggest &quot;preprocessor&quot; feature, with every preprocessor worth its salt implementing it, and now it's done in native CSS. <del>There's a bit of a caveat around syntax as some last minute changes landed, but it's not quite like the prefixes of yesteryear; you just have to use an <code>&amp;</code> in more selectors than is ideal, and as browsers mature, this limitation will likely disappear.</del> But you can now easily scope styles to a specific element or selector, and write support for the various psuedos that are common, in a neat, clean, non-repeating way. And you can finally write your media queries <em>inline</em>, where they belong, with properties inside them, rather than the previous redundancy we saw before. Not really exciting if you've already been using a preprocessor, but now you don't need that preprocessor for <em>this</em>.</p>
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>CSS Nesting with relaxed nesting is supported in the stable release of ALL major browsers in December 2023. So you can drop those extra <code>&amp;</code> at the start of selectors!</p>
</div>
<p><code>color-mix</code> takes another bite out of the preprocessor pie. Instead of having to use sass functions like <code>lighten</code> or <code>darken</code> or <code>transparentize</code>, you can now just write them with real css. And since they're done in real css, they're color space aware, and can make use of custom properties too. Wanna make a highlight out of a primary color, which is picked and set externally? Piece of cake</p>
<pre class="athl"><code class="language-css" translate="no" tabindex="0"><span class="line" data-line="1"><span class="punctuation-delimiter">.</span><span class="type">selector</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="2">  <span class="property">background</span><span class="punctuation-delimiter">:</span> <span class="function">color-mix</span><span class="punctuation-bracket">(</span>in srgb<span class="punctuation-delimiter">,</span> <span class="function">var</span><span class="punctuation-bracket">(</span><span class="variable">--primary-color</span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">,</span> white <span class="number">50<span class="string">%</span></span><span class="punctuation-bracket">)</span>
</span><span class="line" data-line="3"><span class="punctuation-bracket">&rbrace;</span>
</span></code></pre>
<p>This mixes your primary color with 50% white, in the sRGB space. You can use other, newer, better color spaces too, such as OKLCH, which is my favorite.</p>
<p>Containment and Style queries landed, which let you make whole sections of your stylesheet that style based on <em>their size</em>, not the window's size. For component based designs, this is incredible. Now you can make components that render one way when they are &quot;small&quot; and another way when they're bigger.</p>
<p>And it's not all big new features either. Lots of things got quiet little improvements. Transform properties were broken out into separate properties, so now you can do <code>translate: 50%</code> instead of <code>transform: translate(50%)</code>. <code>display</code> was revisited, allowing now to mix and match display types, so you can more adequately specify both how something is positioned relative to its content (inline vs block) and how content inside it is positioned (flex, grid, table, etc). <code>display</code> also became animatable, so now you no longer have to figure out how to move an element between hidden, shown but not visible, and shown but visible. New trigonometric functions landed, allowing for accurate angular math.</p>
<p>All of these features are available <em>today</em>, as of this reading, with every major browser supporting them in the current version</p>
<h2><a href="#the-renaissance-isnt-over" aria-hidden="true" class="anchor" id="the-renaissance-isnt-over"></a>The renaissance isn't over</h2>
<p>And more features are on the horizon, or already landing.</p>
<p>Colors are gaining even more superpowers, via the Relative Colors feature of the CSS Colors level 5 spec. When it lands, you don't even have to use the &quot;new&quot; <code>color-mix</code> feature to do common things, like hue-shift or lighten a color. You can now easily manipulate any value of a color, in any space you like, same as you would any other value.</p>
<p>Want to make a transparent version of a color? Piece of cake:</p>
<pre class="athl"><code class="language-css" translate="no" tabindex="0"><span class="line" data-line="1"><span class="punctuation-delimiter">:</span><span class="attribute">root</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="2">  <span class="variable">--primary</span><span class="punctuation-delimiter">:</span> blue<span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="3">  <span class="variable">--transparent-blue</span><span class="punctuation-delimiter">:</span> <span class="function">hsl</span><span class="punctuation-bracket">(</span>from <span class="function">var</span><span class="punctuation-bracket">(</span><span class="variable">--primary</span><span class="punctuation-bracket">)</span> h s l <span class="operator">/</span> <span class="number">50<span class="string">%</span></span><span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="4"><span class="punctuation-bracket">&rbrace;</span>
</span></code></pre>
<p>How about making a lighter version of a color?</p>
<pre class="athl"><code class="language-css" translate="no" tabindex="0"><span class="line" data-line="1"><span class="punctuation-delimiter">:</span><span class="attribute">root</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="2">  <span class="variable">--light-blue</span><span class="punctuation-delimiter">:</span> <span class="function">oklch</span><span class="punctuation-bracket">(</span>from blue<span class="punctuation-delimiter">,</span> <span class="function">calc</span><span class="punctuation-bracket">(</span>l <span class="operator">+</span> <span class="number">25</span><span class="punctuation-bracket">)</span> c h<span class="punctuation-bracket">)</span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="3"><span class="punctuation-bracket">&rbrace;</span>
</span></code></pre>
<p>Instead of having to use cumbersome functions, you can just use <code>calc</code> and the same color functions as you would to define a single color.</p>
<p>Native CSS scoping is landing too. Using it you can now target styles to a specific scoping root, or specifically carve out holes in your styles, without having to get too funky with things like <code>:not</code>. This is a rather complex feature, so I'll <a href="https://developer.chrome.com/blog/new-in-chrome-118/#css-scope">link to the Chrome blogpost about it</a></p>
<h2><a href="#closing" aria-hidden="true" class="anchor" id="closing"></a>Closing</h2>
<p>I've been using nesting and the color-mix features pretty heavily since I found out about them earlier this year, and they've been great. There's a bit of a learning curve, as with any new tool, but its rather short and gentle. I still keep Sass around, as well as PostCSS, for things that CSS just wont ever be able to do (nor should it), but they're fading into the background, rather than being at the forefront of my mind when writing styles.
And for small, simple projects, I don't use them at all. Just pure CSS. I haven't found it lacking.</p> ]]></description>
    </item>
    <item>
       <title>Why I (generally) don&#39;t use indented syntax templates anymore</title>
       <link>https://pdx.su/blog/2023-08-22-i-dont-use-indented-anymore</link>
       <pubDate>Tue, 22 Aug 2023 16:54:47 -06</pubDate>
       <guid>https://pdx.su/blog/2023-08-22-i-dont-use-indented-anymore</guid>
       <description><![CDATA[ <h1><a href="#why-i-generally-dont-use-indented-syntax-templates-anymore" aria-hidden="true" class="anchor" id="why-i-generally-dont-use-indented-syntax-templates-anymore"></a>Why I (generally) don't use indented syntax templates anymore</h1>
<p>Sass, Pug, Haml, Slim, Stylus, and their friends all aim to make writing various bits of your frontend easier. And they mostly deliver on this primary promise. But they are all victims to the vagaries of open software development, and seem to have mostly fallen by the wayside. I loved using these through my career, so its with a bit of sadness that I realized I don't want to use them for new projects.</p>
<h2><a href="#what-they-set-out-to-do-and-usually-achieve" aria-hidden="true" class="anchor" id="what-they-set-out-to-do-and-usually-achieve"></a>What they set out to do (and usually achieve)</h2>
<p>All these indented syntaxes, <a href="https://haml.info">haml</a>, <a href="https://slim-template.github.io">slim</a>, and <a href="https://pugjs.org/api/getting-started.html">pug</a> mainly, aim to reduce the amount of boilerplate needed when writing HTML, and <a href="https://sass-lang.com">Sass</a> and <a href="https://stylus-lang.com">Stylus</a> aim to do this with CSS. They are typically poised as an alternative to a built-in or already popular template language in their respective web development ecosystems, most of which typically are just HTML or CSS but with a special syntax for escaping to &quot;real&quot; code. With CSS the improvement is negligible, but with HTML its significant, as HTML likes to make you repeat yourself a lot.</p>
<p>Take the following simple HTML document:</p>
<pre class="athl"><code class="language-html" translate="no" tabindex="0"><span class="line" data-line="1"><span class="tag-delimiter">&lt;</span><span class="tag">html</span> <span class="tag-attribute">lang</span><span class="operator">=</span><span class="string">&quot;en&quot;</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="2">
</span><span class="line" data-line="3"><span class="tag-delimiter">&lt;</span><span class="tag">head</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="4">  <span class="tag-delimiter">&lt;</span><span class="tag">meta</span> <span class="tag-attribute">charset</span><span class="operator">=</span><span class="string">&quot;UTF-8&quot;</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="5">  <span class="tag-delimiter">&lt;</span><span class="tag">meta</span> <span class="tag-attribute">name</span><span class="operator">=</span><span class="string">&quot;viewport&quot;</span> <span class="tag-attribute">content</span><span class="operator">=</span><span class="string">&quot;width=device-width, initial-scale=1.0&quot;</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="6">  <span class="tag-delimiter">&lt;</span>title<span class="tag-delimiter">&gt;</span><span class="markup-heading">Document</span><span class="tag-delimiter">&lt;/</span><span class="tag">title</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="7"><span class="tag-delimiter">&lt;/</span><span class="tag">head</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="8">
</span><span class="line" data-line="9"><span class="tag-delimiter">&lt;</span><span class="tag">body</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="10">  <span class="tag-delimiter">&lt;</span><span class="tag">header</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="11">    <span class="tag-delimiter">&lt;</span>h1<span class="tag-delimiter">&gt;</span><span class="markup-heading-1">Header</span><span class="tag-delimiter">&lt;/</span><span class="tag">h1</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="12">  <span class="tag-delimiter">&lt;/</span><span class="tag">header</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="13">  <span class="tag-delimiter">&lt;</span><span class="tag">nav</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="14">    <span class="tag-delimiter">&lt;</span><span class="tag">ul</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="15">      <span class="tag-delimiter">&lt;</span><span class="tag">li</span><span class="tag-delimiter">&gt;</span>Link<span class="tag-delimiter">&lt;/</span><span class="tag">li</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="16">      <span class="tag-delimiter">&lt;</span><span class="tag">li</span><span class="tag-delimiter">&gt;</span>Link<span class="tag-delimiter">&lt;/</span><span class="tag">li</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="17">      <span class="tag-delimiter">&lt;</span><span class="tag">li</span><span class="tag-delimiter">&gt;</span>Link<span class="tag-delimiter">&lt;/</span><span class="tag">li</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="18">      <span class="tag-delimiter">&lt;</span><span class="tag">li</span><span class="tag-delimiter">&gt;</span>Link<span class="tag-delimiter">&lt;/</span><span class="tag">li</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="19">      <span class="tag-delimiter">&lt;</span><span class="tag">li</span><span class="tag-delimiter">&gt;</span>Link<span class="tag-delimiter">&lt;/</span><span class="tag">li</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="20">    <span class="tag-delimiter">&lt;/</span><span class="tag">ul</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="21">  <span class="tag-delimiter">&lt;/</span><span class="tag">nav</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="22">  <span class="tag-delimiter">&lt;</span><span class="tag">article</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="23">    <span class="tag-delimiter">&lt;</span>h2<span class="tag-delimiter">&gt;</span><span class="markup-heading-2">Article</span><span class="tag-delimiter">&lt;/</span><span class="tag">h2</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="24">    <span class="tag-delimiter">&lt;</span><span class="tag">p</span><span class="tag-delimiter">&gt;</span>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quae blanditiis voluptas amet eligendi nemo libero corrupti accusamus minima laboriosam iure, quia modi nulla. Accusamus, excepturi! Voluptate dignissimos repudiandae minima facere.<span class="tag-delimiter">&lt;/</span><span class="tag">p</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="25">  <span class="tag-delimiter">&lt;/</span><span class="tag">article</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="26"><span class="tag-delimiter">&lt;/</span><span class="tag">body</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="27">
</span><span class="line" data-line="28"><span class="tag-delimiter">&lt;/</span><span class="tag">html</span><span class="tag-delimiter">&gt;</span>
</span></code></pre>
<p>Kind of long? You can write it in <a href="https://pugjs.org/api/getting-started.html">pug</a> a lot faster:</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1">html(lang=&quot;en&quot;)
</span><span class="line" data-line="2">  head
</span><span class="line" data-line="3">    meta(charset=&quot;UTF-8&quot;)
</span><span class="line" data-line="4">    meta(name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;)
</span><span class="line" data-line="5">    title Document
</span><span class="line" data-line="6">  body
</span><span class="line" data-line="7">    header
</span><span class="line" data-line="8">      h1 Header
</span><span class="line" data-line="9">    nav
</span><span class="line" data-line="10">      ul
</span><span class="line" data-line="11">        li Link
</span><span class="line" data-line="12">        li Link
</span><span class="line" data-line="13">        li Link
</span><span class="line" data-line="14">        li Link
</span><span class="line" data-line="15">        li Link
</span><span class="line" data-line="16">    article
</span><span class="line" data-line="17">      h2 Article
</span><span class="line" data-line="18">      p Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quae blanditiis voluptas amet eligendi nemo libero corrupti accusamus minima laboriosam iure, quia modi nulla. Accusamus, excepturi! Voluptate dignissimos repudiandae minima facere.
</span></code></pre>
<p>18% fewer characters, and it's generally pretty easy to see how everything works. Pug and other indent based languages have small differences, but will largely look the same.</p>
<p>With Sass (specifically, the <code>.sass</code> indented system) and Stylus, the difference is smaller, and mostly comes down to a bit of developer ergonomics. You don't need to remember to write all those messy <code>&lbrace;&rbrace;;</code> characters (stylus lets you even omit the <code>:</code>). They both also provide nesting, mixins, functions, and other utilities, but they're kind of irrelevant to the normal, day to day ergonomics.</p>
<h2><a href="#where-they-start-to-fall-short" aria-hidden="true" class="anchor" id="where-they-start-to-fall-short"></a>Where they start to fall short</h2>
<h3><a href="#technical-issues" aria-hidden="true" class="anchor" id="technical-issues"></a>Technical issues</h3>
<p>Indent based syntaxes look very good on the surface, and so many developers dive right into them. Only after using them for a while do you begin to encounter the little flaws that make them less fun to use. And generally, these flaws <em>are</em> fixable, or at least work-aroundable.</p>
<p>Take a recent &quot;flaw&quot; I encountered when writing a style using Stylus. I was trying to make use of the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl">newer syntax for writing hsl colors with an alpha channel</a>. Problem is, Stylus internally has a function called <code>hsl</code>, that exists for legacy reasons (converting HSL to hex). It has a very specific signature expected, one that matches the &quot;old style&quot; HSL syntax.</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1">.foo
</span><span class="line" data-line="2">  background-color hsl(210 100% 52% / 25%)
</span></code></pre>
<p>This started throwing compiler errors, and my stylesheet stopped working. There are a couple fixes. Obviously, I could rewrite it in the old style and use <code>hsla</code> instead. But stylus also provides an escape hatch to pure CSS, which is what I wound up using instead:</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1">.foo
</span><span class="line" data-line="2">  background-color @css &lbrace; hsl(210 100% 52% / 25%) &rbrace;
</span></code></pre>
<p>Not really great, but I was able to get on with the project.</p>
<p>Another such example is that the indented syntax for Sass, <code>.sass</code>, <em>doesn't support multi-line constructs</em>. At the time sass was first created, this wasn't really much of a problem, but since then it has become one. <code>grid-template-areas</code> rely on newlines to indicate different rows of the grid, and a built-in sass feature, keyword lists, is hobbled by the inability to have multiple lines. There was an issue opened about this in 2011, and it is <em>still</em> open, with no resolution. Dart-sass does support the indent based syntax yes, but it seems relatively limited; bugfixes only appear if they first surface in the <code>.scss</code> format, and also happen to manifest themselves in the indent based format. Stylus, for what its worth, supports multi-line constructs via a <code>\</code> at the end of lines, indicating a continuation.</p>
<p>Stylus has drifted in and out of maintenance status for the last few years, with the future of the project being uncertain. It works fine now, but as the web continues to move forwards, I worry that more places will need to use the <code>@css</code> escape hatch, and at some point you're writing more CSS than Stylus, and might as well just switch over</p>
<h3><a href="#the-small-learning-curve" aria-hidden="true" class="anchor" id="the-small-learning-curve"></a>The small learning curve</h3>
<p>No matter how close these things are to their &quot;native&quot; counterparts, they <em>are not the same</em>, and so there is always a technical learning curve. In many organizations, this curve is too much, however gentle, and so they aren't used at all. I think this limits their exposure, and so they're always sort of a side project, with minimal support. <code>.scss</code> is <em>far</em> more common to find &quot;out there&quot; than <code>.sass</code>, despite the latter existing before the former. Pug is extremely rare in the JS ecosystem, most people either use something like mustache or just JSX. In Elixir land, EEx and variants are more common than Slime, and in ruby the same goes for ERB vs slim/haml.</p>
<p>And that's assuming you even need them. CSS has nesting now. CSS has variables. CSS has all sorts of combinator selectors that can let you write code that's even more terse than you could with preprocessors. JavaScript is gaining newer and better ways to template out HTML, and at the end of the day you can always fall back on template literals.</p>
<h3><a href="#the-inherent-flaws-in-indent-based-syntaxes" aria-hidden="true" class="anchor" id="the-inherent-flaws-in-indent-based-syntaxes"></a>The inherent flaws in indent-based syntaxes</h3>
<p>Indent based syntaxes are very easy to write. But finding out where blocks start and end can be frustrating. Most editors have systems that help you with this, be it indentation indicators, visible whitespace characters, etc. But you still have to rely on them to know where you are. With a delineated block syntax, you just look for the <code>&rbrace;</code> or the closing tag, and you know that's where it ends.</p>
<h2><a href="#i-dont-want-to-use-them-on-future-projects-and-that-makes-me-sad" aria-hidden="true" class="anchor" id="i-dont-want-to-use-them-on-future-projects-and-that-makes-me-sad"></a>I don't want to use them on future projects, and that makes me sad</h2>
<p>I love the indent based syntaxes. Throughout my career, I've reached for them time and time again. They've saved countless keystrokes, and when written well, look excellent. But I've moved away from them.</p>
<p>The little annoyances start to mount. Having to use escape hatches all over the place just gets tedious. And trying to figure out if it's a problem with the preprocessor or the output code is often an annoying bit of yak shaving that I could do without.</p>
<p>And god help you maintain consistency if you're working on a team. Stylus is amazing for small passion projects. But it does hamper maintainability. Write a codebase as a solo engineer long enough and you'll start to see inconsistencies in how properties are applied. Some lines will have semicolons separating the property from the values, others won't. Stylus doesn't care, and eventually you won't care either. And if you toss other engineers into the mix, then it becomes even more messy.</p>
<p>Getting templates working isn't hard. Typically, its just install a dependency, add a few lines to a config, and start using it. In Vue.js you have to tag your template with a lang tag: <code>&lt;template lang=&quot;pug&quot;&gt;</code>{lang=html}, but that isn't too bad. But out of the box you don't even have to do that. You can just start writing HTML. And since you're using components, all the messiness of HTML is somewhat soothed and combed down.</p>
<p>I worked on upgrading some stylings on this blog over the past weekend. I moved from hardcoded base font size to respecting the browsers font size, and using those sizes for the few breakpoints I have. Initially I did all my changes in the stylus files that I created with this blog. But I had to use a few escape hatches here and there, mostly around color and the &quot;new&quot; <code>color-mix</code>{lang=css} function in CSS, and it left a sour taste in my mouth. I started moving the files over to <code>.sass</code>, to get away from the need for escape hatches, thinking &quot;Since Sass is more actively maintained, it will have kept up with these standards&quot;. No more issues with <code>color-mix</code>{lang=css}, but now I ran smack-dab into the multi-line problem. From there, I just swallowed my opposition, and moved to <code>.scss</code> files and syntax across the whole codebase. Vim-surround, textobj-indent, and a few other tricks made this migration rather easy, and now the codebase is fairly clean.</p>
<p>I have no opposition to indent based syntaxes, and would love to continue using them. But the cost-value proposition is currently out of alignment, and unless things change dramatically, will likely stay out of alignment.</p> ]]></description>
    </item>
    <item>
       <title>Use CSS attributes not classes</title>
       <link>https://pdx.su/blog/2023-07-27-use-css-attributes</link>
       <pubDate>Thu, 27 Jul 2023 10:33:42 -06</pubDate>
       <guid>https://pdx.su/blog/2023-07-27-use-css-attributes</guid>
       <description><![CDATA[ <h1><a href="#use-css-attributes-not-classes" aria-hidden="true" class="anchor" id="use-css-attributes-not-classes"></a>Use CSS attributes not classes</h1>
<p>A common pattern in CSS, particularly when using frameworks, is to use a bunch of classes to affect how something looks. Things like <code>btn btn-primary btn-blue</code> are far too common. There is a better way, with support built into CSS too</p>
<h2><a href="#whats-the-purpose" aria-hidden="true" class="anchor" id="whats-the-purpose"></a>What's the purpose</h2>
<p>Let's dissect the classes being used on our aforementioned button. We've got <code>btn</code>, which is pretty straight forwards, it indicates that it's a button. Then we've got <code>btn-primary</code>, which probably indicates that the button is somehow an important or primary button. Then we've got <code>btn-blue</code>, which means our button has a blue-ish color.</p>
<p>Only one of those is actually meaningful. The rest are presentational aspects. And there's no way to tell which ones can mix and which can't. We can intuit that <code>btn-blue</code> and <code>btn-red</code> don't mix, but <em>maybe they do</em>, and we'd get a purple button.</p>
<p>What we're trying to do is affect how the button is rendered, by changing a few other properties of said button. Lucky for us, HTML already gives us the ability to set attributes, without having to over-rely on the <code>class</code> attribute</p>
<h2><a href="#use-data-attributes" aria-hidden="true" class="anchor" id="use-data-attributes"></a>Use Data attributes</h2>
<p>If we define our button like this</p>
<pre class="athl"><code class="language-html" translate="no" tabindex="0"><span class="line" data-line="1"><span class="tag-delimiter">&lt;</span><span class="tag">div</span> <span class="tag-attribute">class</span><span class="operator">=</span><span class="string">&quot;btn&quot;</span> <span class="tag-attribute">data-primary</span> <span class="tag-attribute">data-color</span><span class="operator">=</span><span class="string">&quot;blue&quot;</span><span class="tag-delimiter">&gt;</span>Button<span class="tag-delimiter">&lt;/</span><span class="tag">div</span><span class="tag-delimiter">&gt;</span>
</span></code></pre>
<p>We can then style it like this:</p>
<pre class="athl"><code class="language-css" translate="no" tabindex="0"><span class="line" data-line="1"><span class="punctuation-delimiter">.</span><span class="type">btn</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="2">  <span class="property">border</span><span class="punctuation-delimiter">:</span> <span class="number">1<span class="string">px</span></span> solid<span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="3">  <span class="property">padding</span><span class="punctuation-delimiter">:</span> <span class="number">5<span class="string">px</span></span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="4">
</span><span class="line" data-line="5">  <span class="character-special">&amp;</span><span class="punctuation-bracket">[</span><span class="tag-attribute">data-color</span><span class="operator">=</span><span class="string">&quot;blue&quot;</span><span class="punctuation-bracket">]</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="6">    <span class="property">background</span><span class="punctuation-delimiter">:</span> blue<span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="7">    <span class="property">color</span><span class="punctuation-delimiter">:</span> white<span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="8">  <span class="punctuation-bracket">&rbrace;</span>
</span><span class="line" data-line="9">  <span class="character-special">&amp;</span><span class="punctuation-bracket">[</span><span class="tag-attribute">data-color</span><span class="operator">=</span><span class="string">&quot;red&quot;</span><span class="punctuation-bracket">]</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="10">    <span class="property">background</span><span class="punctuation-delimiter">:</span> red<span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="11">    <span class="property">color</span><span class="punctuation-delimiter">:</span> white<span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="12">  <span class="punctuation-bracket">&rbrace;</span>
</span><span class="line" data-line="13">  <span class="character-special">&amp;</span><span class="punctuation-bracket">[</span><span class="tag-attribute">data-primary</span><span class="punctuation-bracket">]</span> <span class="punctuation-bracket">&lbrace;</span>
</span><span class="line" data-line="14">    <span class="property">font-weight</span><span class="punctuation-delimiter">:</span> bold<span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="15">  <span class="punctuation-bracket">&rbrace;</span>
</span><span class="line" data-line="16"><span class="punctuation-bracket">&rbrace;</span>
</span></code></pre>
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>I use nested CSS here, which has <a href="https://caniuse.com/css-nesting">very good support across browsers</a>. If you're targeting older browsers, you'll have to use a preprocessor to flatten out the CSS.</p>
</div>
<p>Now there's a very clear separation of concerns. The class indicates <em>what</em> this thing is supposed to be, and the data attributes affect parts of how it looks. The primary appearance is styled by classes still, but secondary aspects are given to us from data attributes.</p>
<h2><a href="#read-more" aria-hidden="true" class="anchor" id="read-more"></a>Read more</h2>
<ul>
<li><a href="http://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors">Attribute Selectors</a></li>
<li><a href="http://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*">Data attributes</a></li>
</ul> ]]></description>
    </item>
    <item>
       <title>Tailwind, and the death of web craftsmanship</title>
       <link>https://pdx.su/blog/2023-07-26-tailwind-and-the-death-of-craftsmanship</link>
       <pubDate>Wed, 26 Jul 2023 12:40:47 -06</pubDate>
       <guid>https://pdx.su/blog/2023-07-26-tailwind-and-the-death-of-craftsmanship</guid>
       <description><![CDATA[ <html><head></head><body><section id="Tailwind-and-the-death-of-web-craftsmanship">
<h1>Tailwind, and the death of web craftsmanship</h1>
<p>There&#39;s a worrying trend in modern web development, where developers
are throwing away decades of carefully wrought systems for a bit of
perceived convenience. Tools such as Tailwind CSS seem to be spreading
like wildfire, with very few people ever willing to acknowledge the
regression they bring to our field. And I&#39;m getting tired of it</p>
<section id="History">
<h2>History</h2>
<p>Back in the 90s and early 00s, if you built a website, you probably
didn&#39;t style it very much. If you did, you used tables, some attributes
on the various HTML tags you had, and a lot of images. Changing anything
required you to update every single occurrence of that <em>thing</em> within
the HTML. There were attempts to work around these defects; people would
build templating systems in Perl and other languages, but fundamentally,
you were putting colors, dimensions, and such <em>into</em> your HTML. This was
awful, and when CSS came out, even as limited as CSS 1.0 was, people
eagerly adopted it. CSS+tables, some image slicing in Fireworks or
Photoshop, and you could build a pretty good looking website. CSS 2 came
out a few years later and dramatically improved things, allowing far
better layouts to be achieved. In the middle of the 00s, Firefox and
then Apple began pushing standards forwards, introducing many new
features. Border-radiuses, gradients, box shadows, enhanced fonts,
flexbox, grids, and many other layout tools. It was a veritable CSS
renaissance. Pre-processor languages, such as Sass and Less, sprouted
up, bringing useful bits of programming to CSS, and frameworks like
Compass, Bourbon, and Neat grew out of them. Tools like Autoprefixer
later cropped up, reducing the overhead of cross-browser support to
almost nothing. Things matured, and we entered a period where building
web apps was more fun than it had ever been.</p>
<section id="The-Cracks-start-showing">
<h3>The Cracks start showing</h3>
<p>As we entered the twilight of this CSS renaissance, some issues CSS has
begun to show up more and more frequently. Wrangling large CSS files
became more and more tedious, deeply scoped selectors began causing
issues, the fragility of a single global namespace pushed people towards
componentization, towards building &quot;website legos&quot; to build with.
There were attempts to tame the CSS beast. BEM, OOCSS, SMACSS, and
friends pitched themselves as the &quot;one true&quot; solution. They all
basically have something in common: they tell you to get rid of various
CSS features to &quot;simplify&quot; things. Out of this rose Tailwind. Instead
of writing your CSS, you just used a bunch of different utility classes
to style things.</p>
</section>
<section id="AtomicUtility-CSS">
<h3>Atomic/Utility CSS</h3>
<p>Often, when maintaining a large CSS codebase, we would write one-off
utility classes. <code>rounded</code> to make something have border-radius, without
having to rewrite it <em>everywhere</em>. Various small classes that adjust the
padding. We always did this, so we could prevent changes from seeping
out of the little place we needed them to everywhere else. We didn&#39;t
use inline styles, because you weren&#39;t supposed to (and they make
maintenance an unholy burden). Gradually, libraries of these utility
classes began to grow. There were some that were passed around as gists,
some that actually grew into things you could install via npm or a
similar package manager (remember bower?), and some that were generated
by or included in preexisting toolchains (Bootstrap sprouted some
utility classes fairly early on).</p>
</section>
<section id="The-rise-of-tailwind">
<h3>The rise of tailwind</h3>
<p>Tailwind started out as a particularly good set of Utility CSS classes.
It was notable for being heavily configurable from day one. Its class
names were reasonable, and it established certain useful conventions
regarding sizing, color systems (very similar to that of Material
Design), and lots of other common base settings. Some of these were
&quot;borrowed&quot; from old libraries like Bootstrap, others were just created
out of the need to buy-into utility CSS wholesale. Early versions of
tailwind were <em>horrifically heavy and slow</em>. You&#39;d have to ship
megabytes of CSS, for a page that might have a half dozen styled
&quot;things&quot; on it. And it was rightly lambasted for this. Utility classes
were supposed to make things easier, faster, more convenient, and
shipping a JPG worth of <em>unused</em> CSS was not in line with that. Tailwind
eventually fixed this, with a generator approach, which would scan your
codebase, pull out tailwind classes, and only put them in the generated
CSS output. This also let tailwind grow the ability to have arbitrary
values, without having to update a configuration file. Now you could do
<code>bg-[#ffccff]</code> for a pinkish background, without having to add it to
your color scheme. Useful, but dangerous too. Tailwind even sprouted
component libraries, built atop tailwind. The tailwind devs have one,
called TailwindUI, and there&#39;s an open-source one called Daisy.</p>
</section>
</section>
<section id="The-problem">
<h2>The problem</h2>
<p>The problem I have with tailwind is that it reduces your HTML to a
gigantic pile of near-gibberish CSS classes. What would be a few, maybe
even half dozen, lines of CSS in a separate file, are now shoved into
the generated code, and <em>repeated everywhere</em>. Any structure to the
styling of an item is completely gone; you have hover styles and focus
styles and dark styles and whatnot all mixed together in a single big
space-separated string. Finding CSS one-offs is not even feasible
anymore, <em>everything is a CSS one-off</em>. Tooling is utterly broken by
tailwind, despite the claims that there is good tooling on tailwind&#39;s
own UI. Look at this screenshot of a web inspector on the Netlify admin
dashboard, using tailwind:</p>
<p id="bigmess"><img alt="&quot;an element with a few hundred CSS tailwind
classes&quot;" src="/.netlify/images?url=/postimages/tailwind-garbage.jpg"/></p>
<p>Yeah, you might have auto-completion in your editor, but the browser
inspector is utterly neutered. You can&#39;t use the applied styles
checkbox, because you&#39;ll wind up disabling a style for <em>every usage of
that tailwind class</em>. <del>You have to manually edit the <code>class</code> attribute
to remove classes to try and push your element to look how you
want.</del><a id="fnref1" href="#fn1" role="doc-noteref"><sup>1</sup></a> Due to the tailwind compiler, which was a solution to the
&quot;shipping massive amounts of CSS&quot; problem, you don&#39;t have a guarantee
that the class you&#39;re trying to poke into the inspector view will
actually exist.</p>
<p>You can&#39;t chain selectors. If you want your hover, focus, and active
classes to be the same, you <em>have to write them all</em>. You can&#39;t do
this:</p>
<pre class="athl"><code class="language-css" translate="no" tabindex="0"><span class="line" data-line="1"><span class="punctuation-delimiter">.</span><span class="type">foo</span><span class="punctuation-delimiter">:</span><span class="attribute">is</span><span class="punctuation-bracket">(</span><span class="punctuation-delimiter">:</span><span class="attribute">hover</span><span class="punctuation-delimiter">,</span> <span class="punctuation-delimiter">:</span><span class="attribute">focus</span><span class="punctuation-delimiter">,</span> <span class="punctuation-delimiter">:</span><span class="attribute">active</span><span class="punctuation-bracket">)</span> <span class="punctuation-bracket">{</span>
</span><span class="line" data-line="2">  <span class="property">background</span><span class="punctuation-delimiter">:</span> purple<span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="3">  <span class="property">border</span><span class="punctuation-delimiter">:</span> <span class="number">1<span class="string">px</span></span> solid blue<span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="4"><span class="punctuation-bracket">}</span>
</span></code></pre>
<p>You have to do this:</p>
<pre class="athl"><code class="language-html" translate="no" tabindex="0"><span class="line" data-line="1"><span class="tag-delimiter">&lt;</span><span class="tag">div</span> <span class="tag-attribute">class</span><span class="operator">=</span><span class="string">&quot;hover:bg-purple active:bg-purple focus:bg-purple hover:border active:border focus:border hover:border-blue focus:border-blue active:border-blue&quot;</span><span class="tag-delimiter"></span>
</span></code></pre>
<p>It gets even worse with dark mode and other variants.</p>
<section id="Its-all-meaningless">
<h3>It&#39;s all meaningless</h3>
<p>In tailwind, it&#39;s common to write things like <code>m-3</code> for a margin. But
<em>what</em> margin? How big is an <code>m-3</code>? Trick question. It depends
<em>entirely</em> on your configuration. An <code>m-3</code> on my site and an <code>m-3</code> on
someone else&#39;s could be entirely different. Same goes for basically
every other number in tailwind. They have absolutely no meaning, and
there&#39;s no consistency. When writing it at work, I frequently either
find myself opening up our tailwind config and the default tailwind
config, just to find which size I want, realizing it&#39;s not there, and
ultimately going with a <code>m-[8px]</code> or something similar, creating another
one-off. And its not just numbers. Class names themselves are
inconsistent. Generally a tailwind class will loosely reflect the
underlying CSS. But then you run into the mess of justify and align.
There are 4 tailwind classes that all impact justification and
alignment. <code>justify-*</code>, <code>align-*</code>, <code>items-*</code>, and <code>content-*</code>. There&#39;s
a bit of false consistency, both <code>justify-*</code> and <code>align-*</code> map to
<code>justify-content</code> and <code>align-content</code> properties. But what about
<code>content-*</code> or <code>items-*</code>? What the hell do they mean? It makes sense if
you use it, but it doesn&#39;t if you try to explain it.</p>
<p>With &quot;real&quot; CSS, properties might not have the most explicit meaning.
But that meaning is part of a standard, and you can look it up online.
Tailwind has <em>no</em> requirements to not change the meaning out from under
you. You have no guarantee that they&#39;re not going to &quot;fix&quot; the
weirdness around the content/justify/align properties tomorrow, and now
you&#39;ve gotta update your entire codebase.</p>
</section>
<section id="It-throws-out-a-ton-of-good-stuff">
<h3>It throws out a ton of good stuff</h3>
<p>Tailwind, and other utility frameworks, throw out the best and most
often misunderstood aspects of CSS. CSS was explicitly designed with
certain features, that, while complicated, are immensely useful once you
can master them. The cascade, selector chaining (see above), and
specificity are all very powerful tools. If you misunderstand them, yeah
you can cut yourself. Just like if you don&#39;t use a saw properly you can
cut yourself.</p>
<p>The cascade is probably the most powerful part of CSS, that gets
<em>completely tossed</em> by tailwind. In short, the cascade allows you to
have multiple stylesheets, multiple styles, that apply to an item, and
at render time they are reduced down to a single set of applied styles.
This lets you do things like have a general purpose stylesheet, and then
theme or page specific ones that change parts of this stylesheet. You
might protest that you&#39;ve never used that, and I&#39;d call you a liar.
The browsers provide a set of base styles, and then we, as developers,
add our own on top of them. We might use a reset style, which normalizes
browsers base styles (less common these days, as browsers have done a
good job of normalizing their styles to each other). You can also use
the cascade to easily apply dark mode, light mode, high contrast mode,
or other color schemes. You just have to write your original CSS in a
judicious manner.</p>
<p>Selector chaining, particularly with the advent of new CSS selectors
like <a href="http://developer.mozilla.org/en-US/docs/Web/CSS/:is"><code>is</code></a> and
<a href="http://developer.mozilla.org/en-US/docs/Web/CSS/:where"><code>where</code></a>,
allows very clean reuse of common bits of CSS. You get to separate the
properties that affect how something looks from what is being affected.
It&#39;s obvious, but in tailwind, <em>you don&#39;t get that</em>.</p>
<p>Finally, specificity is useful for how to resolve conflicts, what to do
where two selector sets both match the same elements. Its complex and
has bitten many developers hands over the years, but it can be a
powerful tool when you need it. And 0-specificity selectors like <code>where</code>
have arrived to help clean things up. And it&#39;s a lie to say you don&#39;t
have to worry about specificity in tailwind; tailwind has its own
specificity. Classes are read left-to-right by the browser, and so if
you do <code>b-1 bg-blue b-2</code>, guess what? Your element gets both <code>b-1</code> and
<code>b-2</code>, and the browser will <em>probably</em><a id="fnref2" href="#fn2" role="doc-noteref"><sup>2</sup></a> choose <code>b-2</code> as your border,
and throw away <code>b-1</code></p>
</section>
<section id="Its-an-obtuse-abstraction">
<h3>It&#39;s an obtuse abstraction</h3>
<p>For all the simple stuff, tailwind is decent. Borders, colors, font
sizes, all fairly trivial, and map well to utility classes. But then you
start to get into &quot;advanced&quot; CSS stuff. Grids. Psuedo-elements.
Gradients. All have very limited, if any, support in Tailwind. The
built-in grid support is mostly just simple repeating grids. If you want
different/better grids, you either have to use one-offs, or define them
ahead-of-time in your tailwind configuration. Psuedo-elements have very
limited support, and suffer massive amounts of repetition. Gradients
give you simple linear gradients between a few color stops, but doing
more complex gradients is just simply impossible, there&#39;s not even a
reasonable one-off escape hatch. Tailwind <em>does</em> expose certain gradient
values as CSS custom properties (CSS variables), so you can write your
own CSS for gradients, but then you might as well ask &quot;why not just
write my own CSS in general&quot;</p>
</section>
<section id="apply-sucks">
<h3>@apply sucks</h3>
<p>One of the bits of advice Tailwind gives is to use <code>@apply</code> and extract
common blobs of CSS into common classes<a id="fnref3" href="#fn3" role="doc-noteref"><sup>3</sup></a>. You&#39;d use this like so:</p>
<pre class="athl"><code class="language-css" translate="no" tabindex="0"><span class="line" data-line="1"><span class="punctuation-delimiter">.</span><span class="type">btn</span> <span class="punctuation-bracket">{</span>
</span><span class="line" data-line="2">  <span class="keyword-directive">@apply</span> m-2 p-2 bg-blue text-white<span class="punctuation-delimiter"></span>
</span><span class="line" data-line="3"><span class="punctuation-bracket">}</span>
</span></code></pre>
<p><em>Why?</em></p>
<p>Why bother to even use @apply? Just write the damn CSS. Extract color
values and stuff to custom properties if you so desire, it makes
maintainability easier, but don&#39;t do this. <code>@apply</code> is a gigantic
code-smell, it goes against everything tailwind supposedly stands for,
and doesn&#39;t make writing your CSS any easier.</p>
</section>
<section id="It-makes-maintenanace-a-nightmare">
<h3>It makes maintenanace a nightmare</h3>
<p>Tailwind is rather far to the &quot;write-only&quot; side of the software
maintenance spectrum. Once written, untangling which class in a big ball
of classes causes an effect can be painful. This is never more apparent
than trying to undo a decision made once in the past.</p>
<p>I recently had to update a site I was working on to support both light
and dark color schemes. Tailwind has a nice <code>dark:</code> modifier built in,
that converts whatever comes after it to a <code>prefers-color-scheme: dark</code>
class. Problem is, the site I was working on was written, from the
get-go, with a dark color scheme. In effect, I had to add light mode.
And due to how browsers handle color schemes<a id="fnref4" href="#fn4" role="doc-noteref"><sup>4</sup></a>, tailwind offers no
<code>light</code> modifier (nor should they). My process of adding the light mode
was thus reduced to a painful series of find all instances of a certain
color, evaluate them, and if they needed, add the light color as the
default and a <code>dark:</code> modifier to their expression. It wasn&#39;t as simple
as a global find-and-replace, as there were some places we always wanted
the dark colors to shine through.</p>
<p>Were this done using a more traditional CSS approach, I could just
globally make the site use light mode, and then add a dark stylesheet
that only affected the elements in question, or I could have updated
color tables. Both are far easier than having to skulk through dozens of
HTML templates, trying to find all the selectors that needed
manipulation.</p>
</section>
</section>
<section id="Whats-the-alternative">
<h2>What&#39;s the alternative?</h2>
<p>Scoped CSS and component based design. These are basically a magic
bullet for any and all valid complaints about CSS.</p>
<p>Scoped CSS lets you write CSS how you want, without having to worry
about its impact outside the local environment, and components help you
define said local environment.</p>
<p>Component driven design is wonderful; you just write the base
components, and then build things using those components, using other
components, and everything just slots together. React, Vue, Svelte,
Surface-UI, and many other tools all espouse this paradigm, and if you
haven&#39;t tried them, you really should.</p>
<p>For example, here&#39;s a simple button in vue:</p>
<pre class="athl"><code class="language-vue" translate="no" tabindex="0"><span class="line" data-line="1"><span class="tag-delimiter">&lt;</span><span class="tag">template</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="2">  <span class="tag-delimiter">&lt;</span><span class="tag">div</span> <span class="tag-attribute">class</span><span class="operator">=</span><span class="string">&quot;button&quot;</span><span class="tag-delimiter">&gt;</span>Some Button<span class="tag-delimiter">&lt;/</span><span class="tag">div</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="3"><span class="tag-delimiter">&lt;/</span><span class="tag">template</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="4">
</span><span class="line" data-line="5"><span class="tag-delimiter">&lt;</span><span class="tag">style</span> <span class="tag-attribute">scoped</span><span class="tag-delimiter">&gt;</span>
</span><span class="line" data-line="6">  <span class="punctuation-delimiter">.</span><span class="type">button</span> <span class="punctuation-bracket">{</span>
</span><span class="line" data-line="7">    <span class="property">margin</span><span class="punctuation-delimiter">:</span> <span class="number">5<span class="string">px</span></span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="8">    <span class="property">padding</span><span class="punctuation-delimiter">:</span> <span class="number">5<span class="string">px</span></span><span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="9">    <span class="property">color</span><span class="punctuation-delimiter">:</span> white<span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="10">    <span class="property">background</span><span class="punctuation-delimiter">:</span> blue<span class="punctuation-delimiter">;</span>
</span><span class="line" data-line="11">  <span class="punctuation-bracket">}</span>
</span><span class="line" data-line="12"><span class="tag-delimiter">&lt;/</span><span class="tag">style</span><span class="tag-delimiter">&gt;</span>
</span></code></pre>
<p>I don&#39;t have to worry if some other component uses a <code>.button</code> class,
as my styles are scoped <em>only</em> to the button in this component. And
using that button elsewhere in my codebase is trivial, I just write
<code>&lt;Button /&gt;</code>. No need to copy around a ton of tailwind styles, no need
to extract things into global <code>.button</code> classes, none of that. I just
have a button, and it works. The end.</p>
<p>Overrides are easy enough, you just add props to your component, that
let you tweak predefined values about said property. You can add a
<code>class</code> prop, and pass in arbitrary classes, and then parent components
can affect child components <em>via their own locally scoped styles</em>.</p>
</section>
<section id="Common-defenses-of-tailwind-that-Ive-heard">
<h2>Common defenses of tailwind that I&#39;ve heard</h2>
<section id="Tailwind-is-faster-to-write">
<h3>Tailwind is faster to write</h3>
<p>Not in my experience, and ultimately, who cares? I&#39;ve never found the
bottleneck of developing code to be having to write <code>margin</code> instead of
<code>m</code>, and I have emmet anyway, so I just do <code>m10</code>, hit tab, and get
<code>margin: 10px</code>. You might have a wee bit of credibility in that I don&#39;t
have to come up with semantic names for <em>everything</em>, but that&#39;s solved
by using scoped styles and components.</p>
</section>
<section id="Tailwind-isnt-actually-bloated-it-compiles-the-classes-you-need-only-for-prod">
<h3>Tailwind isn&#39;t actually bloated, it compiles the classes you need
only for prod</h3>
<p>The CSS tailwind generates might not be bloated, but repeating the
gigantic strings of classes all over your codebase certainly adds to the
size of the final HTML output. And yes, things like gzip will reduce the
network transfer size of said files, there&#39;s still the parsing
complexity. Browsers are good at parsing HTML, but it still takes time
and CPU cycles.</p>
</section>
<section id="Tailwind-helps-you-stay-consistent">
<h3>Tailwind helps you stay consistent</h3>
<p>Unless you use one-offs. Or use different &quot;numbers&quot; when writing your
margins and paddings.</p>
</section>
<section id="Tailwind-is-better-than-inline-styles">
<h3>Tailwind is better than inline styles</h3>
<p>Yeah, it is, but barely. That&#39;s like saying &quot;this apple is slightly
less rotten than that one.&quot; Both are rotten</p>
</section>
<section id="The-proper-way-to-use-tailwind-is-to-make-your-own-utility-classes-via-apply">
<h3>The proper way to use tailwind is to make your own utility classes
via @apply</h3>
<p>When you&#39;re reaching for apply, why not just reach a little further and
write real CSS?</p>
</section>
<section id="Tailwinds-configuration-lets-you-define-values-ahead-of-time-and-then-reuse-them-everywhere">
<h3>Tailwind&#39;s configuration lets you define values ahead of time and
then reuse them everywhere!</h3>
<p>I&#39;ve had value tables in Sass since 2008. Juniors still come along and
just do <code>margin: 13px</code>. In tailwind, they do <code>m-[13px]</code>. No difference.
At least with CSS its centralized.</p>
</section>
</section>
<section id="The-death-of-craftsmanship">
<h2>The death of craftsmanship</h2>
<p>Tailwind is a symptom of what I feel to be a larger problem in
development. There&#39;s been a rapid deterioration in
pride-of-craftsmanship in development. It&#39;s naive to believe that
&quot;back in the old day&quot; everyone wrote everything with a perfect eye
towards beautiful craftsmanship, and now we just push code out as fast
as we can. I remember having to use ugly CSS hacks to get IE to render
things properly, or float clears to get containers not to shrink under
an inline image. But there was definitely more interest in &quot;doing it
right&quot; rather than dismissing it as a problem that wasn&#39;t worth
solving.</p>
<p>I don&#39;t want to dismiss tailwind as &quot;for juniors&quot; or as &quot;for backend
engineers forced to do frontend,&quot; but there is a kernel of truth to
those statements. The people I&#39;ve seen who are most excited over
tailwind are generally those that would view frontend as something they
<em>have</em> to do, not something they <em>want</em> to do. Juniors are still
learning, and so they&#39;re attracted to the marketing spiel of tailwind,
and see &quot;look how easy it is I have to type less!&quot; They don&#39;t have
the experience to tell pyrite apart from gold. And backend engineers
have an unfortunate tendency to view frontend as &quot;not real
engineering,&quot; as something beneath them. They used to reach for
Bootstrap, now they reach for Tailwind. They want to get something
passable done, and go back to &quot;real&quot; engineering on the backend.</p>
<p>That&#39;s not to say there aren&#39;t great minds that love tailwind. Many
great developers that I have a ton of respect for use and espouse
tailwind. I don&#39;t understand why, but they seem to have blinders for
all of its problems, but maybe they see something I don&#39;t.</p>
<p>Maybe Tailwind is a symptom, an inflammatory reaction to the sorry state
of CSS at the time it was conceived. Browser makers have kicked the can
on native scoped styles again and again, and issues arising from
specificity have persisted until very recently (and only are &quot;gone&quot; if
you know how to use <code>:where</code>). I&#39;ve seen other engineers, of all
levels, stuck in a mire of bad CSS, and so to them maybe Tailwind seems
like a lifesaver. But CSS <em>is better now</em>. It&#39;s not perfect, but it&#39;s
better than its ever been, and it&#39;s better than tailwind. Give it
another try. Don&#39;t reach for big globs of libraries to paper over the
issues you think it has. Start off with something small and light. Use
sass and autoprefixer, but not much else. Keep things simple and small.
You might find you&#39;re even having fun writing CSS.</p>
<p>And it&#39;s not just frontend stuff that has seen this death. I&#39;ve met
senior engineers who don&#39;t understand git, and don&#39;t want to
understand git. You point them at git-from-the-bottom-up, and they
recoil as if it was a venomous snake. I&#39;ve seen people, lead and
principal engineers, who refuse to learn modern JS, insisting that since
it was bad in 2006 its bad today. Worse still is some of these people
have used their leadership positions to prevent the use of modern JS, of
component frameworks like react/vue, from being used across an
organization.</p>
</section>
<section id="Addendum">
<h2>Addendum</h2>
<p>Everything I wrote about here is mostly a problem I noticed regarding
Tailwind. But other people have noticed them too. Here&#39;s some other
things, some dated, some not, that expand on some issues of Tailwind:</p>
<ul>
<li>
<p><a href="https://www.aleksandrhovhannisyan.com/blog/why-i-dont-like-tailwind-CSS/">Why I Don&#39;t Like Tailwind
CSS</a></p>
</li>
<li>
<p><a href="https://johanronsse.be/2020/07/08/why-youll-probably-regret-using-tailwind/">Why you’ll probably regret using
Tailwind</a></p>
</li>
<li>
<p><a href="https://twitter.com/chriscoyier/status/1331302651179495425?s=20">Chris Coyer, of CSS-Tricks, notes the tailwind
&quot;smell&quot;</a></p>
</li>
<li>
<p><a href="https://medium.com/codex/tailwindui-and-heres-the-real-failwind-scam-b74357371ca5">TailwindUI, And Here’s The Real Failwind
Scam</a></p>
</li>
<li>
<p><a href="https://thoughtbot.com/blog/tailwind-and-the-femininity-of-css">Tailwind and the femininity of
CSS</a> -
An interesting perspective on this.</p>
</li>
<li>
<p><a href="https://open-props.style">OpenProps</a>: CSS custom properties
(variables) that give you tailwind-like building blocks, but not the
class mess. Never used it, but it looks promising, if you need this
sort of design system</p>
</li>
<li>
<p>A counterpoint article: <a href="https://www.swyx.io/why-tailwind">Why
Tailwind</a></p>
</li>
</ul>
</section>
<section id="Updates">
<h2>Updates</h2>
<ul>
<li>
Fix a typo, add Counterpoint article and Tailwind and the femininity
of CSS article links
</li>
<li>
Correct section on difficulty of editing the <code>class</code> attribute to
reflect the chrome web inspector&#39;s <code>.cls</code> button
</li>
</ul>
</section>
</section>
<section role="doc-endnotes">
<hr/>
<ol>
<li id="fn1">
<p>The Chrome inspector has apparently grown a nice little <code>.cls</code>
button in the Styles tab, which lets you toggle on and off the
classes applied to the selected element via checklists. Thanks to
<a href="https://news.ycombinator.com/item?id=36977864">swanson on
HackerNews</a> for
pointing that out!<a href="#fnref1" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn2">
<p>I say probably because it&#39;s very loosely defined behavior. What
happens if you document is written in an rtl language?<a href="#fnref2" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn3">
<p><a href="https://twitter.com/adamwathan/status/1559250403547652097">Except the creator of tailwind himself regrets adding
<code>@apply</code></a><a href="#fnref3" role="doc-backlink">↩︎</a></p>
</li>
<li id="fn4">
<p>Light mode is the default, as far as browsers are concerned. If no
dark style is provided, they will always fall back to light mode<a href="#fnref4" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</section>
</body></html> ]]></description>
    </item>
    <item>
       <title>Some Elixir Testing Tricks</title>
       <link>https://pdx.su/blog/2023-06-14-some-elixir-test-tricks</link>
       <pubDate>Wed, 14 Jun 2023 00:00:00 -06</pubDate>
       <guid>https://pdx.su/blog/2023-06-14-some-elixir-test-tricks</guid>
       <description><![CDATA[ <h1><a href="#some-elixir-testing-tricks" aria-hidden="true" class="anchor" id="some-elixir-testing-tricks"></a>Some Elixir Testing Tricks</h1>
<p>Testing in Elixir is pretty great. ExUnit, combined with the functional nature of Elixir, makes it very easy to test almost everything in your codebase. However, it is very easy for boilerplate to creep into your tests. Common setup patterns, similar assertions, and more can quickly make your test suite feel cumbersome. But ExUnit files are <em>just</em> Elixir files. So you can write bits of code that will help you out tremendously.</p>
<h2><a href="#common-setup" aria-hidden="true" class="anchor" id="common-setup"></a>Common Setup</h2>
<p>You're testing something in your codebase, and you have to set up a user, maybe an account or organization, and maybe even some content to &quot;work&quot; with. You've got some fixtures or factories ready to go, so you just write the setup code inline in the test. But now you need to write another test. So you extract the setup to a <a href="https://hexdocs.pm/ex_unit/ExUnit.Callbacks.html#setup/1"><code>setup/1</code></a> block. Pretty good. Now all the other tests in that file or describe group will have those steps run before they do, and any results added to the context.</p>
<p>But now you have another test file, and it's got similar, but slightly different, setup steps. You copy the setup block, change it slightly, and proceed with things. Repeat that over the next several other test files, and now you're in an unfortunate situation. You've got a bunch of <em>similar</em>, but not identical setup steps. Some tests need users of different permission levels, some need different content, others need their own special things.</p>
<h3><a href="#extracting-universal-steps-to-a-macro" aria-hidden="true" class="anchor" id="extracting-universal-steps-to-a-macro"></a>Extracting Universal steps to a macro</h3>
<p>A common approach is to put the most universal setup steps into a custom <code>__using__</code> macro, and then using it via <code>use</code>. If you're using Phoenix, you already have a macro generated for you, in your <code>test/support/conn_case.ex</code> file, and similar ones for database access and channels. You can just add a <code>setup</code> block to these macros, and it will effectively be included in every test that needs it.</p>
<p>This is great for setup steps that <em>need</em> to occur universally. But that's a surprisingly small number of steps. You might assume that your user setup and auth steps need to be universal, but they don't. How are you going to test if unauthenticated users are prohibited from using parts of your application?</p>
<p>In the past I've seen people split out a separate case, or add options to the <code>__using__</code> macro, that are passed in from runtime. Things like:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="keyword-function">defmodule</span> <span class="module">MyAppWeb.SomeBigTest</span> <span class="keyword">do</span>
</span><span class="line" data-line="2">  <span class="keyword">use</span> <span class="module">MyAppWeb.ConnCase</span><span class="punctuation-delimiter">,</span> <span class="string-special-symbol">noauth: </span><span class="boolean">true</span>
</span><span class="line" data-line="3"><span class="keyword"></span>
</span></code></pre>
<p>This <em>works</em>, but it quickly becomes opaque. You have to document all the various <code>opts</code> you can pass to the <code>__using__</code> macro, and discoverability is minimal. And as your test suite grows, the number of <code>opts</code> you have to support, plus the ways they can be combinant, can get overwhelming.</p>
<h3><a href="#setup-functions" aria-hidden="true" class="anchor" id="setup-functions"></a>Setup functions</h3>
<p>An alternative that <em>supplements</em> universal setup steps is <em>setup functions</em>. While <a href="https://hexdocs.pm/ex_unit/ExUnit.Callbacks.html#setup/1"><code>setup/1</code></a> nominally accepts a block, it also accepts a <em>single arity function</em>, which is called with the current context. This lets you define a collection of discrete setup functions, that can be mixed together to handle your test case setup. If you edit your aforementioned using macro to import a module containing these setup functions, you can use them anywhere the macro is used, however you like.</p>
<p>Take for example the following helper module:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="keyword-function">defmodule</span> <span class="module">MyApp.Support.Helpers</span> <span class="keyword">do</span>
</span><span class="line" data-line="2">  <span class="keyword-function">def</span> <span class="function">insert_user</span><span class="punctuation-bracket">(</span><span class="variable">context</span><span class="punctuation-bracket">)</span> <span class="keyword">do</span>
</span><span class="line" data-line="3">    <span class="comment"># Some code that sets up a user and adds it to the context</span>
</span><span class="line" data-line="4">  <span class="keyword">end</span>
</span><span class="line" data-line="5">
</span><span class="line" data-line="6">  <span class="keyword-function">def</span> <span class="function">authorize</span><span class="punctuation-bracket">(</span><span class="variable">context</span><span class="punctuation-bracket">)</span> <span class="keyword">do</span>
</span><span class="line" data-line="7">    <span class="comment"># Some code that takes a user off the context and &quot;authenticates&quot; them</span>
</span><span class="line" data-line="8">  <span class="keyword">end</span>
</span><span class="line" data-line="9">
</span><span class="line" data-line="10">  <span class="keyword-function">def</span> <span class="function">insert_content</span><span class="punctuation-bracket">(</span><span class="variable">context</span><span class="punctuation-bracket">)</span> <span class="keyword">do</span>
</span><span class="line" data-line="11">    <span class="comment"># Generate some content</span>
</span><span class="line" data-line="12">  <span class="keyword">end</span>
</span><span class="line" data-line="13">
</span><span class="line" data-line="14">  <span class="comment"># More functions...</span>
</span><span class="line" data-line="15"><span class="keyword">end</span>
</span></code></pre>
<p>As you can see, those functions are pretty small. They implement single, simple concerns, and are pretty straightforwards, from the name alone.</p>
<p>If you added them to your <code>__using__</code> macros, you can then simply call <code>setup :functionname</code> anywhere you'd put a setup block, and it will be called, and passed in the current context. You can have each setup function take and return things to the context, and build pretty powerful setup pipelines.</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="keyword-function">defmodule</span> <span class="module">MyAppWeb.SomeBigTest</span> <span class="keyword">do</span>
</span><span class="line" data-line="2">  <span class="keyword">use</span> <span class="module">MyAppWeb.ConnCase</span>
</span><span class="line" data-line="3">
</span><span class="line" data-line="4">  <span class="function-call">setup</span> <span class="string-special-symbol">:insert_user</span>
</span><span class="line" data-line="5">  <span class="function-call">setup</span> <span class="string-special-symbol">:authorize</span>
</span><span class="line" data-line="6">
</span><span class="line" data-line="7">  <span class="function-call">test</span> <span class="string">&quot;test_something&quot;</span><span class="punctuation-delimiter">,</span> <span class="punctuation-special">%</span><span class="punctuation-bracket">&lbrace;</span><span class="string-special-symbol">current_user: </span><span class="variable">user</span><span class="punctuation-bracket">&rbrace;</span> <span class="keyword">do</span>
</span><span class="line" data-line="8">    <span class="comment"># ...</span>
</span><span class="line" data-line="9">  <span class="keyword">end</span>
</span><span class="line" data-line="10"><span class="keyword">end</span>
</span></code></pre>
<p>Very clean, and for test suites that don't need to have a user or an authed user, you can simply omit them.</p>
<h3><a href="#configuration" aria-hidden="true" class="anchor" id="configuration"></a>Configuration</h3>
<p>You've now got a pretty clean setup system. You can pick and choose whole chunks of setup, and everything more or less works. But you're still in a position where you have to write a <em>bunch</em> of different functions for various permutations of how you'd set up your test cases. Maybe you need both a regular user and an admin. Whatever the case, having to have an <code>insert_user/1</code> and an <code>insert_admin/1</code> that share much of the same code. Maybe you extracted them to a private function inside your Helpers module. That works, but there's a better way.</p>
<p>You might also want to have slightly different configuration for <em>each</em> test in a suite. They all call the same functions, but you might want each to be subtly different.</p>
<p>There's a nice solution for this, <em>built into ExUnit</em>. The <code>@tag</code>.</p>
<p>Out of the box, without any configuration, you can change any value in the <code>context</code> of your tests using the <code>@tag</code> attribute (and the <code>@describetag</code> for your <code>describe</code> blocks). Since <code>setup</code> is run <em>once per test</em>, you can simply call <code>@tag someattr: somevalue</code> before each test to override the values generated by your <code>setup</code> functions. Have a file-wide setup for inserting and authorizing a user, but want to test what happens when <code>current_user</code> is <code>nil</code>? Trivial, <code>@tag current_user: nil</code>.</p>
<p>But that's just overriding values from the setup with your own values, on a per test basis. Better than nothing, but you can do better still. Remember that the custom functions we wrote for <code>setup</code> <em>receive the context as their parameter</em>. You can use this to accept configuration values from the context and make your setup functions do different things.</p>
<p>Take this example of changing <code>insert_user/1</code> to handle things like the user being an administrator:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="keyword-function">def</span> <span class="function">insert_user</span><span class="punctuation-bracket">(</span><span class="punctuation-special">%</span><span class="punctuation-bracket">&lbrace;</span><span class="string-special-symbol">admin: </span><span class="boolean">true</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">)</span> <span class="keyword">do</span>
</span><span class="line" data-line="2">  <span class="comment"># some code that makes a user and sets them up as an administrator</span>
</span><span class="line" data-line="3"><span class="keyword">end</span>
</span><span class="line" data-line="4"><span class="keyword-function">def</span> <span class="function">insert_user</span><span class="punctuation-bracket">(</span><span class="variable">context</span><span class="punctuation-bracket">)</span> <span class="keyword">do</span>
</span><span class="line" data-line="5">  <span class="comment"># code that just creates a normal user</span>
</span><span class="line" data-line="6"><span class="keyword">end</span>
</span></code></pre>
<p>Now, without changing the <code>setup</code> calls at the top of the test suite (or <code>describe</code> block), we can make tags have an admin user on the fly:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="operator"><span class="constant">@<span class="function-call"><span class="constant">tag <span class="string-special-symbol">admin: </span><span class="boolean">true</span></span></span></span></span>
</span><span class="line" data-line="2"><span class="function-call">test</span> <span class="string">&quot;an admin can delete a post&quot;</span> <span class="keyword">do</span>
</span><span class="line" data-line="3">  <span class="comment"># something that tests this</span>
</span><span class="line" data-line="4"><span class="keyword">end</span>
</span><span class="line" data-line="5">
</span><span class="line" data-line="6"><span class="function-call">test</span> <span class="string">&quot;a regular user cannot delete a post&quot;</span> <span class="keyword">do</span>
</span><span class="line" data-line="7">  <span class="comment"># similar code, but it would refute that the post was deleted</span>
</span><span class="line" data-line="8"><span class="keyword">end</span>
</span></code></pre>
<p>That's it! Very simple, very flexible, very useful.</p>
<p>I've started using the above patterns all over my testing, and now can't imagine working without them.</p>
<h2><a href="#custom-assertions" aria-hidden="true" class="anchor" id="custom-assertions"></a>Custom Assertions</h2>
<p>Another common smell I'll see all over test suites is a lot of boilerplate around assertions. You'll see a lot around things like testing HTML matches certain values, or certain elements are present, or in an evented system that an event was received. People coming from other testing frameworks seem to think there's something magic about the native <code>assert</code>, and may ask if there are other libraries, such as how Rspec has the matchers libraries in Ruby.</p>
<p>Assert, and its sibling refute, are just plain Elixir code, same as most everything else in Elixir and ExUnit. You can write your own asserting functions and macros trivially easy.</p>
<h3><a href="#checking-for-presence-of-an-html-element" aria-hidden="true" class="anchor" id="checking-for-presence-of-an-html-element"></a>Checking for presence of an HTML element</h3>
<p>A common pattern when testing web apps is to see if the generated output contains a particular element. When working on LiveView apps, this is even more common. Generally, a lot of test suites will start off by just doing simple string matching, such as <code>html =~ &quot;&lt;div class=\&quot;bar\&quot;&quot;</code>. This proves to be quite fragile and prone to dumb breakage. So eventually most developers will implement something like this, using the <a href="https://github.com/philss/floki">Floki</a> library:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="function-call">assert</span> <span class="variable">html</span> <span class="operator">|&gt;</span> <span class="module">Floki</span><span class="operator">.</span><span class="function-call">parse_fragment!</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span> <span class="operator">|&gt;</span> <span class="module">Floki</span><span class="operator">.</span><span class="function-call">find</span><span class="punctuation-bracket">(</span><span class="string">&quot;.bar&quot;</span><span class="punctuation-bracket">)</span>
</span></code></pre>
<p>This does the job, but its rather verbose, and having to write it <em>every</em> time you want to check if an element exists is tedious.</p>
<p>Instead, you can turn it into a simple macro:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="keyword-function">defmacro</span> <span class="function">assert_html</span><span class="punctuation-bracket">(</span><span class="variable">html</span><span class="punctuation-delimiter">,</span> <span class="variable">selector</span><span class="punctuation-bracket">)</span> <span class="keyword">do</span>
</span><span class="line" data-line="2">  <span class="keyword">quote</span> <span class="keyword">do</span>
</span><span class="line" data-line="3">   <span class="function-call">assert</span> <span class="keyword">unquote</span><span class="punctuation-bracket">(</span><span class="variable">html</span><span class="punctuation-bracket">)</span> <span class="operator">|&gt;</span> <span class="module">Floki</span><span class="operator">.</span><span class="function-call">parse_fragment!</span><span class="punctuation-bracket">(</span><span class="punctuation-bracket">)</span> <span class="operator">|&gt;</span> <span class="module">Floki</span><span class="operator">.</span><span class="function-call">find</span><span class="punctuation-bracket">(</span><span class="keyword">unquote</span><span class="punctuation-bracket">(</span><span class="variable">selector</span><span class="punctuation-bracket">)</span><span class="punctuation-bracket">)</span>
</span><span class="line" data-line="4">  <span class="keyword">end</span>
</span><span class="line" data-line="5"><span class="keyword">end</span>
</span></code></pre>
<p>And use it like this:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="function-call">assert_html</span> <span class="variable">html</span><span class="punctuation-delimiter">,</span> <span class="string">&quot;.bar&quot;</span>
</span></code></pre>
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>You don't have to use macros for these, they can be quite easily written as functions. However, if you write them as functions, you <em>must</em> import the appropriate &quot;things&quot; into the module they are defined in.</p>
<p>By using macros, you can step around this, because the macros generate code that lives at the call-site, which already has access to the appropriate &quot;things&quot;</p>
</div>
<h3><a href="#handling-event-boilerplate" aria-hidden="true" class="anchor" id="handling-event-boilerplate"></a>Handling Event Boilerplate</h3>
<p>In evented Elixir, we have the useful assertion <code>assert_receive</code>. This lets you state that a process should get an event within a timeout, and even specify the message to error with in the case no event is received.</p>
<p>But if your events have a particular pattern they follow, i.e. <code>&lbrace;:event_fired, %&lbrace;action: FooEvent&rbrace;&rbrace;</code>, and you want to implement custom timeouts other than the default 100ms, or custom error messages, it can get pretty verbose pretty quickly.</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="function-call">assert_receive</span> <span class="punctuation-bracket">&lbrace;</span><span class="string-special-symbol">:event_fired</span><span class="punctuation-delimiter">,</span> <span class="punctuation-special">%</span><span class="punctuation-bracket">&lbrace;</span><span class="string-special-symbol">action: </span><span class="module">FooEvent</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span> <span class="number">1000</span><span class="punctuation-delimiter">,</span> <span class="string">&quot;Did not receive event&quot;</span>
</span></code></pre>
<p>Not terrible, but now you have to do that all over your tests, and if you want to change the failure message or timeout, you have to update <em>all</em> the implementation sites.</p>
<p>Macros can simplify this:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="keyword-function">defmacro</span> <span class="function">assert_event</span><span class="punctuation-bracket">(</span><span class="variable">event</span><span class="punctuation-bracket">)</span> <span class="keyword">do</span>
</span><span class="line" data-line="2">  <span class="keyword">quote</span> <span class="keyword">do</span>
</span><span class="line" data-line="3">    <span class="variable">event_name</span> <span class="operator">=</span> <span class="keyword">unquote</span><span class="punctuation-bracket">(</span><span class="variable">event</span><span class="punctuation-bracket">)</span><span class="operator">.</span><span class="variable">name</span>
</span><span class="line" data-line="4">
</span><span class="line" data-line="5">    <span class="function-call">assert_receive</span> <span class="punctuation-bracket">&lbrace;</span><span class="string-special-symbol">:event_fired</span><span class="punctuation-delimiter">,</span> <span class="variable">e</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span> <span class="number">1000</span><span class="punctuation-delimiter">,</span> <span class="string">&quot;Did not receive event&quot;</span>
</span><span class="line" data-line="6">
</span><span class="line" data-line="7">    <span class="function-call">assert</span> <span class="variable">event_name</span> <span class="operator">==</span> <span class="variable">e</span><span class="operator">.</span><span class="variable">name</span>
</span><span class="line" data-line="8">
</span><span class="line" data-line="9">    <span class="variable">e</span>
</span><span class="line" data-line="10">  <span class="keyword">end</span>
</span><span class="line" data-line="11"><span class="keyword">end</span>
</span></code></pre>
<p>You can then call this like so</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="variable">event</span> <span class="operator">=</span> <span class="function-call">assert_event</span> <span class="module">FooEvent</span>
</span></code></pre>
<p>Since the macro returns the matched event object, you can use it further in your test suite:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="variable">event</span> <span class="operator">=</span> <span class="function-call">assert_event</span> <span class="module">FooEvent</span>
</span><span class="line" data-line="2"><span class="function-call">assert</span> <span class="string">&quot;some event data&quot;</span> <span class="operator">==</span> <span class="variable">event</span><span class="operator">.</span><span class="variable">data</span>
</span></code></pre>
<p>If you want a further optimization, you can add a version of <code>assert_event</code> that accepts and calls a function, giving said function the event. This lets you treat the function as a lambda, and keep any assertions on said event scoped to only that event. In busy test suites, where you're asserting event after event, this can dramatically improve readability.</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="keyword-function">defmacro</span> <span class="function">assert_event</span><span class="punctuation-bracket">(</span><span class="variable">event</span><span class="punctuation-bracket">)</span> <span class="keyword">do</span>
</span><span class="line" data-line="2">  <span class="keyword">quote</span> <span class="keyword">do</span>
</span><span class="line" data-line="3">    <span class="variable">event_name</span> <span class="operator">=</span> <span class="keyword">unquote</span><span class="punctuation-bracket">(</span><span class="variable">event</span><span class="punctuation-bracket">)</span><span class="operator">.</span><span class="variable">name</span>
</span><span class="line" data-line="4">
</span><span class="line" data-line="5">    <span class="function-call">assert_receive</span> <span class="punctuation-bracket">&lbrace;</span><span class="string-special-symbol">:event_fired</span><span class="punctuation-delimiter">,</span> <span class="variable">e</span><span class="punctuation-bracket">&rbrace;</span><span class="punctuation-delimiter">,</span> <span class="number">1000</span><span class="punctuation-delimiter">,</span> <span class="string">&quot;Did not receive event&quot;</span>
</span><span class="line" data-line="6">
</span><span class="line" data-line="7">    <span class="function-call">assert</span> <span class="variable">event_name</span> <span class="operator">==</span> <span class="variable">e</span><span class="operator">.</span><span class="variable">name</span>
</span><span class="line" data-line="8">
</span><span class="line" data-line="9">    <span class="variable">e</span>
</span><span class="line" data-line="10">  <span class="keyword">end</span>
</span><span class="line" data-line="11"><span class="keyword">end</span>
</span><span class="line" data-line="12">
</span><span class="line" data-line="13"><span class="keyword-function">defmacro</span> <span class="function">assert_event</span><span class="punctuation-bracket">(</span><span class="variable">event</span><span class="punctuation-delimiter">,</span> <span class="variable">func</span><span class="punctuation-bracket">)</span> <span class="keyword">do</span>
</span><span class="line" data-line="14">  <span class="keyword">quote</span> <span class="keyword">do</span>
</span><span class="line" data-line="15">    <span class="variable">event</span> <span class="operator">=</span> <span class="function-call">assert_event</span><span class="punctuation-bracket">(</span><span class="keyword">unquote</span><span class="punctuation-bracket">(</span><span class="variable">event</span><span class="punctuation-bracket">)</span><span class="punctuation-bracket">)</span>
</span><span class="line" data-line="16">
</span><span class="line" data-line="17">    <span class="keyword">unquote</span><span class="punctuation-bracket">(</span><span class="variable">func</span><span class="punctuation-bracket">)</span><span class="operator">.</span><span class="punctuation-bracket">(</span><span class="variable">event</span><span class="punctuation-bracket">)</span>
</span><span class="line" data-line="18">  <span class="keyword">end</span>
</span><span class="line" data-line="19"><span class="keyword">end</span>
</span></code></pre>
<p>And you can use it like so:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="variable">assert_event</span><span class="punctuation-bracket">(</span><span class="module">FooEvent</span><span class="punctuation-delimiter">,</span> <span class="keyword">fn</span> <span class="variable">e</span> -&gt;
</span><span class="line" data-line="2">  <span class="variable">assert</span> &quot;some event data == e.data
</span><span class="line" data-line="3">end)
</span></code></pre>
<p>Very clean!</p>
<h2><a href="#generating-test-suites-from-a-matrix" aria-hidden="true" class="anchor" id="generating-test-suites-from-a-matrix"></a>Generating test suites from a matrix</h2>
<p>Often you'll find cases where you need to test that a variety of cases are valid, and a variety of cases are invalid. These test suites might be largely identical, apart from the variable factor. Since tests are just elixir, you can do this:</p>
<pre class="athl"><code class="language-elixir" translate="no" tabindex="0"><span class="line" data-line="1"><span class="function-call">describe</span> <span class="string">&quot;Post Removal&quot;</span> <span class="keyword">do</span>
</span><span class="line" data-line="2">  <span class="function-call">setup</span> <span class="string-special-symbol">:create_post</span>
</span><span class="line" data-line="3">
</span><span class="line" data-line="4">  <span class="keyword">for</span> <span class="variable">role</span> <span class="operator">&lt;-</span> <span class="punctuation-bracket">[</span><span class="string-special-symbol">:moderator</span><span class="punctuation-delimiter">,</span> <span class="string-special-symbol">:admin</span><span class="punctuation-bracket">]</span><span class="punctuation-delimiter">,</span> <span class="variable">own</span> <span class="operator">&lt;-</span> <span class="punctuation-bracket">[</span><span class="boolean">true</span><span class="punctuation-delimiter">,</span> <span class="boolean">false</span><span class="punctuation-bracket">]</span> <span class="keyword">do</span>
</span><span class="line" data-line="5">    <span class="operator"><span class="constant">@<span class="function-call"><span class="constant">tag <span class="string-special-symbol">own_post: </span><span class="variable">own</span></span></span></span></span>
</span><span class="line" data-line="6">    <span class="function-call">test</span> <span class="string">&quot;<span class="string-special">#&lbrace;</span><span class="variable">role</span><span class="string-special">&rbrace;</span> can remove <span class="string-special">#&lbrace;</span><span class="keyword">if</span> <span class="variable">own</span><span class="punctuation-delimiter">,</span> <span class="string-special-symbol">do: </span><span class="string">&quot;their own&quot;</span><span class="punctuation-delimiter">,</span> <span class="string-special-symbol">else: </span><span class="string">&quot;someone else&#39;s&quot;</span><span class="string-special">&rbrace;</span> post&quot;</span> <span class="keyword">do</span>
</span><span class="line" data-line="7">      <span class="comment"># some code that removes the post and asserts its removal</span>
</span><span class="line" data-line="8">    <span class="keyword">end</span>
</span><span class="line" data-line="9">  <span class="keyword">end</span>
</span><span class="line" data-line="10">
</span><span class="line" data-line="11">  <span class="operator"><span class="constant">@<span class="function-call"><span class="constant">tag <span class="string-special-symbol">own_post: </span><span class="boolean">true</span></span></span></span></span>
</span><span class="line" data-line="12">  <span class="function-call">test</span> <span class="string">&quot;users can remove their own post&quot;</span> <span class="keyword">do</span>
</span><span class="line" data-line="13">    <span class="comment"># some code that removes the post and asserts its removal</span>
</span><span class="line" data-line="14">  <span class="keyword">end</span>
</span><span class="line" data-line="15">
</span><span class="line" data-line="16">  <span class="operator"><span class="constant">@<span class="function-call"><span class="constant">tag <span class="string-special-symbol">own_post: </span><span class="boolean">false</span></span></span></span></span>
</span><span class="line" data-line="17">  <span class="function-call">test</span> <span class="string">&quot;users can&#39;t remove other people&#39;s posts&quot;</span> <span class="keyword">do</span>
</span><span class="line" data-line="18">    <span class="comment"># some code that attempts to remove the post and refutes if the removal was successful</span>
</span><span class="line" data-line="19">  <span class="keyword">end</span>
</span><span class="line" data-line="20"><span class="keyword">end</span>
</span></code></pre>
<p>While this is a contrived example, you can see how we were able to test 4 test cases for the moderator and admin roles with a single test, and then test the more specific user test cases separately.</p>
<p>Remember, all that ExUnit provides is some useful tools around testing. Under the hood, its <em>just</em> Elixir.</p>
<hr />
<h2><a href="#updates" aria-hidden="true" class="anchor" id="updates"></a>Updates</h2>
<ul>
<li>Post was updated to add a note about why macros were used instead of plain functions</li>
</ul> ]]></description>
    </item>
    <item>
       <title>Rest in Peace, Reddit Compact</title>
       <link>https://pdx.su/blog/2023-04-06-rip-reddit-compact</link>
       <pubDate>Thu, 06 Apr 2023 10:00:00 -06</pubDate>
       <guid>https://pdx.su/blog/2023-04-06-rip-reddit-compact</guid>
       <description><![CDATA[ <h1><a href="#rest-in-peace-reddit-compact" aria-hidden="true" class="anchor" id="rest-in-peace-reddit-compact"></a>Rest in Peace, Reddit Compact</h1>
<p>Reddit has recently disabled/removed access to the compact interface, which was useful on mobile and low power devices. As my first professional project <em>ever</em>, I'd like to reflect on it, 13 years the wiser.</p>
<h2><a href="#origins" aria-hidden="true" class="anchor" id="origins"></a>Origins</h2>
<p>Back in 2009, I had a Blackberry Storm. It wasn't a great device, but it was neat for the time. The screen was &quot;big,&quot; it looked pretty, and it could browse the web, albeit poorly. The built-in browser was mostly optimized for WAP style sites, a few steps below the then amazing browsers on the iPhone and Android devices. There was also the Opera browser, which was much better at rendering pages, but used server-side rendering, meaning <em>no javascript support at all</em>. Because of these limitations, browsing reddit on a Blackberry, even a big touchscreen one like the Storm, was almost exclusively a read-only experience. You could choose between the then-WAP styled, read-only reddit mobile site, which looked a lot like HackerNews does now, amusingly, or the &quot;full&quot; desktop experience, which kind of worked without JS, but not well. I was mostly okay with this, as I could blame it on the lack of decent browsers on the Blackberry.
Christmas 2009, I received a Motorola Droid, known as a Milestone outside the US. This was a substantial upgrade over the Blackberry. It had a physical keyboard, a better touch screen, ran Android, and, importantly for this post, a Webkit based browser. Suddenly the read-only reddit seemed like a much bigger hindrance than it did before. You could post and browse the full desktop site, but it was suboptimal (seriously, try browsing <a href="https://old.reddit.com">https://old.reddit.com</a> on your phone). I wasn't impressed by what I saw in terms of mobile interfaces; I can't remember if Reddit is Fun (now RiF for Reddit) was one of the apps I tried, but if it was, 18-year-old me wasn't impressed. And so I set out to make my own mobile interface.</p>
<h2><a href="#the-early-steps-of-the-mobile-interface" aria-hidden="true" class="anchor" id="the-early-steps-of-the-mobile-interface"></a>The early steps of the mobile interface</h2>
<p>This was right smack-dab in the sekuomorphic interface era, and there was plenty of iPhone style interfaces for &quot;studying&quot; across the internet, mostly in screenshots. Reddit had its own in-house iPhone app, called iReddit, but nothing for Android. Being a web developer primarily, I started out making a simple interface on top of reddit. My javascript skills at the time were quite lacking, so I was handling most of the API stuff with glued together bits of Perl and PHP, rendering it on the server. This wasn't terribly unusual for the time, the era of jQuery, Ext.JS, and Google Web Toolkit. The interface was lightly styled, mostly using webkit based CSS features, lots of background gradients, and so forth, but it looked a lot like the final compact interface looked.</p>
<h2><a href="#becoming-an-official-reddit-interface" aria-hidden="true" class="anchor" id="becoming-an-official-reddit-interface"></a>Becoming an official reddit interface</h2>
<p>January 2010, I was in the #reddit IRC channel on freenode, and some reddit admins were active one night. I showed keysersosa some screenshots of the reddit interface, and he seemed fairly interested in it. Extended conversations turned into an invitation to make it an official reddit project. I was given some access to a staging instance of reddit, ssh access to said instance, and some instructions on how reddit's internals worked. Chris, aka keysersosa, and the other reddit admins, were tremendously helpful, and put up with a lot of my naïve and immature questions and approaches. Over the next several months, the mobile interface took shape. Towards June, it had become fairly feature-complete, with voting, submitting, commenting, sign-ups, and most other core reddit features. Some interfaces never really got built explicitly for mobile, but they still worked reasonably well, since the mobile layout was fairly flexible to render content. Several people helped with the QA, catching bugs I had no hope to ever figure out.</p>
<p><img src="/.netlify/images?url=/postimages/reddit-mobile-tc.jpg" alt="v1 of reddit mobile, as seen on a TechCrunch article" /></p>
<p>Come June 9th, <a href="https://web.archive.org/web/20100612133310/http://blog.reddit.com/2010/06/better-mobile-reddit-for-all.html">we launched it</a>, and got a <a href="https://techcrunch.com/2010/06/09/reddit-mobile">nice article in TechCrunch about it</a>.</p>
<p><a href="https://web.archive.org/web/20100614000623/http://paradoxdgn.com/post/the-design-process-for-reddit-mobile">I wrote an old blog post</a>; for whatever reason Archive.org didn't preserve my sites styles, but you can read and see some great images, regardless of CSS. You can see the android 2.0 style pop-overs for post and comment options in that article.</p>
<p><a href="https://www.reddit.com/r/announcements/comments/cd9ju/weve_revamped_reddits_mobile_site_let_us_know/">You can view the old reddit comment thread</a> here.</p>
<h2><a href="#becoming-a-contractoremployee-for-reddit" aria-hidden="true" class="anchor" id="becoming-a-contractoremployee-for-reddit"></a>Becoming a contractor/employee for reddit</h2>
<p>With the success of reddit, and my need for cash as I went off to University, I asked if I could become gainfully employed by reddit. Surprisingly, they said yes, and I was able to join reddit's team and become an admin. This was incredible for 18-19 year old me, as I was able to get a good jump-start on my career.</p>
<p>Over the next year, I worked on a variety of reddit &quot;things,&quot; from a revamp of the advertisers dashboard, several side-banner ads for certain campaigns, a large resolution banner we used for the Rally to Restore Sanity reddit booth, some UX tweaks and changes for the fresh reddit gold, and more.</p>
<h2><a href="#improving-reddit-mobile" aria-hidden="true" class="anchor" id="improving-reddit-mobile"></a>Improving Reddit Mobile</h2>
<p>One thing that some people noticed about that first version is that it could be quite resource intensive to render on first loads, and had weird performance issues. Additionally, certain systems were completely inaccessible from mobile, bouncing you out to the desktop interface.</p>
<p>Most of the slowness was traced to the massive amounts of CSS gradients and effects used. Every post, button, and more, used gradients, shadows, and more. They would often be repainted for <em>each</em> button, which was costly on phones of the time (the Nexus One was considered speedy, with its 1GHz CPU).</p>
<p>This was fixed by rendering a small image of the button styles, and then slicing the image up, 9-patch style, with CSS border images. This gave us buttons that could be any dimensions, with nice looking backgrounds. Other gradients were also flattened into background images, such as those of the header toolbars.</p>
<p>The post options pop-overs were kind of a sticking point as well, with some people complaining about their usability. We replaced them with inline expandos, that spanned the width of the browser, and had much bigger touch targets.</p>
<p>Finally, we added a mail and modmail link to the upper right, replacing the old double-chevron menu, and removed the <code>Home</code> button in the upper left, instead rendering the subreddit logo image and subreddit title.</p>
<p>These features launched, and had a <a href="https://web.archive.org/web/20110724041754/http://blog.reddit.com/2011/07/next-generation-of-reddit-mobile.html">small blog post celebrating them.</a>, with a <a href="https://www.reddit.com/r/blog/comments/iw1kz/the_next_generation_of_reddit_mobile/">corresponding reddit comment section</a></p>
<h2><a href="#leaving-reddit" aria-hidden="true" class="anchor" id="leaving-reddit"></a>Leaving reddit</h2>
<p>During 2011, reddit was undergoing a lot of significant changes. They split off from Condé Nast, becoming their own company, and underwent some organizational restructuring. Almost the entire staff, who worked there when I joined, had moved onto other things, and there was an effort to consolidate power and access to the San Francisco office. Finally, my school-work was starting to take more of my time up, and so I was unable to seriously focus on reddit and school at the same time. So it was time to move on. Some features that I had intended to add to the compact version, that never materialized, were mobile moderation, better sharing (this would take nearly a decade for browser-induced share intents to arrive), web-worker/service worker backed notifications, and many more things. Many of them materialized in the &quot;new&quot; reddit interface</p>
<h2><a href="#the-end-of-compact" aria-hidden="true" class="anchor" id="the-end-of-compact"></a>The end of compact</h2>
<p>For the last couple of months, there have been comments on the <a href="https://www.reddit.com/r/compact">/r/compact</a> subreddit about issues rising up with the interface. Commenting, posting, voting and other features would sporadically stop working. A few weeks ago, adding <code>i.</code> as a prefix, or <code>.compact</code>, to reddit URLs, stopped rendering the interface. A workaround was to use <code>https://old.reddit.com/whatever.compact</code> instead, but that was patched last week.</p>
<p>There's been an outpouring of disappointed comments in the compact subreddit, which is touching to see, and several people have started working on userscripts, alternative interfaces, and so forth, to try and resurrect compact.</p>
<p>When people are asked why they like compact, typically they will say something along the lines of how performant it is. Which is not surprising, because it was built targeting a device with a 533MHz CPU and 512Mb of ram. JS was minimal, mostly jQuery style, and the CSS optimizations we made in 2011 <em>still</em> hold up.</p>
<h2><a href="#looking-back-13-years-later" aria-hidden="true" class="anchor" id="looking-back-13-years-later"></a>Looking back, 13 years later</h2>
<p>13 years wiser, with nearly 10 years of full-time software engineering experience, I can't but help look back at where I got started from. I am extremely grateful to the people that helped me along the path, Chris, Mike, David, Eric, Jeremy, and many others. Without them, I surely wouldn't be the software engineer I am today. Without the opportunity to &quot;jump right in,&quot; I might not have even become a software engineer. I really appreciated the professionality I was afforded, as someone so green, was awesome. And getting to go to DC for the Rally to Restore Sanity is still one of the high points of my freshman year of college.</p>
<p>The comments I've gotten over the years, and the recent outpouring of support for the interface, a decade later, is incredible. Knowing that people love and continue to use the interface I built, and are going out of their way to resurrect it when it was End-of-Life'd by reddit, is awesome.</p>
<p>So, Thank you old reddit admins, users, and everyone else who helped me along the path.</p>
<p><a href="https://www.reddit.com/r/programming/comments/12dpmq6/rest_in_peace_reddit_compact/">comments on reddit</a> <a href="https://news.ycombinator.com/item?id=35470777">comments on hackernews</a></p> ]]></description>
    </item>
    <item>
       <title>The little things matter</title>
       <link>https://pdx.su/blog/2023-03-14-the-little-things</link>
       <pubDate>Tue, 14 Mar 2023 19:07:12 -06</pubDate>
       <guid>https://pdx.su/blog/2023-03-14-the-little-things</guid>
       <description><![CDATA[ <h1><a href="#the-little-things-matter" aria-hidden="true" class="anchor" id="the-little-things-matter"></a>The little things matter</h1>
<p>When working on software, I've noticed a tendency in myself and others to overly focus on the MVP. We aim to get the big stuff working as soon as possible, with some vague promise that we'll apply polish later. While this is efficient from a corprorate perspective, I still feel it does a disservice to building quality systems in the long run.</p>
<p>Think about the various systems that you've used. Think about what you like about some of them, and what you don't like about others. Chances are you'll like something rather trivial, when you view it at a distance, and dislike something for, again, a rather trivial reason. These are the little things I'm talking about. Tech debt, warts under the skin, or a messy back office are all problems, but they aren't the problems that often make or break something.</p>
<h2><a href="#picking-code-editors" aria-hidden="true" class="anchor" id="picking-code-editors"></a>Picking code editors</h2>
<p>As a programmer, I'm rather passionate about my text editor. It is the canvas upon which I work, after all. Over the years I've moved between editors a few times. Starting out, I used vim (technically Elvis, as the early 00s didn't see particularly well implemented vim GUIs on Windows). I'd use Notepad++ a few times, but always came back to vim. When I got my first Mac, I used Textmate 2, followed by SublimeText 2, followed by Atom, and now I use VSCode.</p>
<p>Vim is a perfect example of little things mattering. Vim is <em>atrocious</em> when it comes to discoverability. There are no contextual hints, no helpful pop-ups, no type-aheads that guide you. You're tossed in to the deep end, and have to learn piecemeal. But every time you figure something out from the manual, a tutorial, or a coworker, you get a little jolt of endorphins. Vim is excellent at focusing on little things. Textobjects are a perfect example. I'd wager a very large plurality, if not flat majority, of vim users don't use nor care about textobjects. But when you're working, being able to replace the whole word/paragraph/line/region/quote content <em>while your cursor is inside it</em> saves you time, and makes you feel just a little more efficient.</p>
<p>TextMate 2 can be looked at as the beginning of the &quot;editor revolution.&quot; Yes, there are very good Mac editors that predate it, such as BBEdit and TextWrangler, but TM2 did so many <em>little</em> things right, that its legacy is felt today. Most modern syntax themes use a format devised by TextMate. The idea of a heavily pluggable editor, while not new (Vim and Emacs have had plugins forever), was popularized by TextMate 2. And TextMate 2 had something else going for it: it was <em>pretty</em>. Working with something that looks nice is a lot better than working with something that's functional, but ugly.</p>
<p>When TextMate 2 got a bit long in the tooth, and some work paradigms shifted (i.e. using a tree view baked into the editor came into vogue), I moved to SublimeText 2. ST2 did a lot of things well, and a lot of those little things were carried over from TextMate 2. But ST2 <em>also</em> introduced its own little things. The keyboard-driven quick command menu is fundamentally no different from Vim's command mode or various Emacs chords, but in practice, the little UI that comes up when you trigger it, the typeahead nature of it, and the implicit discoverability, all tie together to make it a <em>better</em> user interface. Atom and VSCode have also copied this pattern, and it's now emergent in pretty much every technical application out there. Even &quot;creative&quot; tools like Photoshop have implemented similar features.</p>
<div class="markdown-alert markdown-alert-tip">
<p class="markdown-alert-title">Tip</p>
<p>If you're on macOS, I suggest editing your system-wide keyboard shortcuts to add a shortcut to &quot;Open Help Menu&quot;. MacOS helpfully has a menu search system that can be used as a command palette in most applications, and hitting the keyboard shortcut puts the cursor in a search box within that help menu.</p>
</div>
<p>Atom improved dramatically on how much power third party plugins can have, both over the text, and over various presentational aspects. Things like rich markdown preview, regex railroad graphs, embedded diagrams, and whatnot were the little things atom added that dramatically improved it over sublime text, enough that the performance hit was worth it. VSCode further improved on this by adding performance back while only limiting plugins in certain ways, and by introducing standardized ways to build support for IDE like features, without needing a big heavy IDE.</p>
<p>I also tried Emacs for a bit. I like a lot of the &quot;big things&quot; about emacs: it's modal, it uses lisp for <em>everything</em>, it has features out the wazoo, org mode is hard to beat, etc. I used the Spacemacs distribution, which adds vim like keybindings, Atom/VSCode like command palettes with typeahead discoverability, and some helpful conventions for how everything works. I spent a decent amount of time fiddling with my configs, writing a ton of (very fun) lisp, and just seeing where I ran into limits.</p>
<p>And one of these limits was what made me stop using Emacs. It was an incredibly trivial thing, and most people would say that it's a stupid reason not to use it. But it was a nucleation point for dissatisfaction. That small issue? Smooth scrolling. Emacs is, like vim, generally a <em>terminal oriented</em> program, and as such, scrolling tends to &quot;jump&quot; from line to line. You never have a partial line visible. And that's what I initially noticed. If I was writing a document, there would be a wee bit extra padding at the bottom, where there wasn't enough space to display the next line, but more space than the previous line needed. I tried to ignore it, but it nagged at me. Looking for solutions proved fruitless, the few recommendations some people had didn't work for me. And suddenly, I couldn't <em>not</em> see the issue. Things that were fun before, like editing the configs, took on an air of tedium. I couldn't get it exactly the way I wanted, and so previously where things were just challenges to work around, gleefully, they became chores, annoying issues I needed to fix.</p>
<h2><a href="#my-snowblower" aria-hidden="true" class="anchor" id="my-snowblower"></a>My snowblower</h2>
<p>My snow blower isn't tremendously remarkable. It's a recent model Toro two-stage blower, with a 27&quot; scoop. It does its job, well enough, that it's not too annoying to have to blow the driveway after a heavy winter snow. It's reliable, and, provided the snow is dry enough, has never failed me. But it's <em>better than every other kind</em>. It doesn't move snow any better or worse than other brand. Its tires aren't better or worse than any other brand. Its motor isn't easier or harder to start than any other brand. What makes it better is one little thing: the chute controls.</p>
<p>If you've never used a snowblower, you might not know what I'm talking about. A snowblower is a machine, typically gas powered, although there are electric models, that uses a large auger in a scoop to pull snow from in front of it to the middle and back, into a chute. Two stage blowers use a fan (of sorts) to push the snow up and out the chute, with enough force that you can clear a snowbank and blow it into your yard. The chute typically needs to be repositioned a few times during use, so you can aim the jet of snow around obstacles, or as you reorient while clearing a space.</p>
<p>I tend to clear my driveway using a spiral pattern. I aim the chute to the right, and start traveling down my driveway. At the bottom, I turn 180º left, and repeat the process. This creates an ever widening space of cleared snow, pushing the removed snow out and off my driveway. By doing this I never have to reposition the chute, in theory. But in practice, as I get to the outer part of my driveway, I want to aim the chute so as not to spray snow onto my neighbors house. And when I get to a certain stage, it doesn't make sense to continue spiraling, because the other side of the driveway has been completely cleared, so I switch to a more simple back-and-forth method, swapping which side the chute aims at with each reversal. When clearing the area in front of the mailbox, I want to direct the snow <em>around</em> the box, so it doesn't fill up and soak any letters inside when the snow melts. And when doing walks, I typically make one pass down the walk, turn 180º, turn the chute 180º, and walk back. Not too much chute movement, but its consistent when it pops up.</p>
<p>Every blower I've used before has varying degrees of terrible chute controls. Some have fixed angle, which is set via a screw, and can't be changed. That's not really too much of a problem, you just aim the chute down and go about your business. But the direction is the issue. Many have a crank and worm-gear based approach, so every time you want to move the chute 180º, you have to spin a crank far too many times. Do that at the end of a few runs, and you'll start to hate the thing.</p>
<p>Which is where the Toro's approach becomes the distinguishing <em>little thing</em>. Toro uses a control interface that works like a joystick. You grab the stick, and can move it around a hemisphere. Every movement directs the chute in some way, either adjusting the throw angle or the direction. Reversing the chute to throw to the opposite side? Just move the joystick, so it points to the opposite side. Want to throw snow high up in the air to distribute it across your lawn? Pull the stick back. It's so intuitive you don't even think about it, you just do it. It saves maybe 5 minutes a job, nothing significant, but when you're out in the cold and wet, those 5 minutes sure can drag on.</p>
<h2><a href="#lutron-home-automation" aria-hidden="true" class="anchor" id="lutron-home-automation"></a>Lutron home automation</h2>
<p>The first home automation device I ever purchased was an X10 module and IR remote. I bought it at a neighbors garage sale, for about $5. I was 10 years old. It was extremely cool to ten year old me. I never managed to get any apparatus to connect it to my computer, and after the shine wore off, it went into a drawer with other toys.</p>
<p>I'd followed home automation on and off for years after that. Eventually, in late 2015, while renting, I built up enough of an interest to buy some Z-Wave switches, a USB Z-Wave stick, and got started. Our rental home eventually sprouted automation in all the places that mattered; the bedrooms, hallways, and living room. I never got around to the kitchen or bathrooms.</p>
<p>Z-Wave was good enough, but it had a few problems. First, every software package out there had its flaws. HomeAssistant, at the time, was extremely immature and not very easy to use. OpenHAB had issues with Z-Wave locks. HomeSeer was paid, and felt dated, but &quot;worked.&quot; Everything was a compromise, including hardware. Most switches at the time were somewhat limited in what they supported. Most didn't have comprehensive central scene support, many didn't even have proper reporting, and very few had more than a single association. Software at the time basically assumed associations didn't exist, so if you wanted to network a single smart switch to control two loads, you had to create an &quot;automation&quot; in your software of choice, and then hope the server it was on was reliable enough to stay up so you didn't have to think about it.</p>
<p>Furthermore, there was a small variety of Z-Wave devices beyond the standard switches, plug modules, and motion sensors. Want a decent looking remote? There's maybe one, and its got a somewhat modern experience. Its not something that a guest could look at and immediately intuit. Other, closed ecosystems, in this case Lutron, have very stylish remotes available. When my primary load control for my home automation was Z-Wave based, I invested in a Lutron Caseta bridge, a few plug modules, and a hand-full of Pico remotes, because they looked, and worked, better than anything in the Z-Wave system. Z-Wave was still what I put in the wall, the Caseta wall switches at the time were rather &quot;techie&quot; looking, and the better looking Maestro switches weren't an option unless you upgraded to RadioRA2. RA2 select, the Sunnata dimmers, and other modern additions to the Lutron product line, didn't exist at the time.</p>
<p>Aside from looking better, Lutron's plug modules and remotes had a few other things going for them, that weren't possible (at least from my research in 2016) with Z-Wave. You could <em>hold down</em> a button on a Pico remote, and the corresponding loads would change <em>while you were holding it</em>. Release the load, and the lights would stop at the level you released at. Z-Wave required you to work in &quot;steps&quot; and fixed percentages; you'd say <code>set bedroom lights to 15%</code>, and it would undergo a smooth transition to that 15%, but if you weren't sure if you wanted 15% or 20%, you couldn't hold the button down until it &quot;looked right&quot;.</p>
<p>When we finally bought a house, I decided that our HA was going to be much more Lutron based than before. Z-Wave had served me quite well, but the <em>little things</em> about how adjusting loads worked, about how the switches were rather ugly (unless you sacrificed features), and how software support was patchwork, had soured me on it. Fundamentally, Z-Wave did everything I wanted, but so did Lutron, and Lutron did it better. I got myself an RA2 inclusive certification, and have slowly been adding lights to each room in the house.</p>
<p>Lutron isn't without its shortcomings. Advanced logic is locked out of the RA2/RA3 product line; you have to buy into a HomeWorks system, which has basically no way to DIY it. The programming software is <em>windows only</em>, and has its own set of odd terms and features. Integrations with third parties, in this case HomeAssistant, work well enough, and let you fake certain logic features that would otherwise be unavailable. But, even in spite of all those things, Lutron scratches the itch of &quot;little things&quot; that Z-Wave, ZigBee, X10, and whatever else never did.</p>
<h2><a href="#ok-but-who-really-cares" aria-hidden="true" class="anchor" id="ok-but-who-really-cares"></a>Ok but who really cares?</h2>
<p>When I'm talking about the above points, or really anything with similar sentiment, I'll have people express some variant of &quot;Ok, that's cool, but who really cares?&quot; And that is a valid point! If you endlessly focus on the smallest thing, you can be prone to bike-shedding. But if you ignore them completely, you may ship sub-par products. You don't need to dive into a mire of minutiae on every subject. I find periodically stopping, taking a look at what you're doing, and seeing if there's a better way to do it is often worth the small-time detour it takes to implement.</p>
<p>If you have a command line task you have to execute fairly regularly, and are relying on your shell's history to pull it out, why not just copy it into a script, and then just add a few niceties around it? Modern shells have nice tools for argument parsing, so instead of having to edit the command line each time you have to do something slightly different, just encapsulate those in arguments. Future you will thank you for your foresight. Doing this doesn't take much, doesn't add much, but it's a <em>little thing</em>.</p>
<p>If you use a tool every day, you might as well look at the pain points, and try to optimize them away. Someone else may have had the same problem you did, and may have written an extension or tool that solves it. And if not, and you come up with it, someone else may stumble across your solution.</p>
<p>Everyone pays attention to the big things. The features listed on a marketing page, the topics mentioned during keynote speeches. No one ever really considers the tiny little thing, most don't even notice it. But when they're gone, you miss them.</p> ]]></description>
    </item>
    <item>
       <title>Updating my Fish Shell prompt and Theme</title>
       <link>https://pdx.su/blog/2023-02-13-updating-my-fish-theme</link>
       <pubDate>Mon, 13 Feb 2023 22:18:18 -07</pubDate>
       <guid>https://pdx.su/blog/2023-02-13-updating-my-fish-theme</guid>
       <description><![CDATA[ <h1><a href="#updating-my-fish-shell-prompt-and-theme" aria-hidden="true" class="anchor" id="updating-my-fish-shell-prompt-and-theme"></a>Updating my Fish Shell prompt and Theme</h1>
<p>I've been using Fish Shell for nearly a decade now, and over that time I've gradually developed a theme and prompt that I enjoyed. At the start of this year, I rewrote it from the ground up, eliminating many years of accumulated cruft.</p>
<p><img src="/.netlify/images?url=/postimages/fish-theme.png" alt="screenshot of the theme" /></p>
<div class="markdown-alert markdown-alert-important">
<p class="markdown-alert-title">Important</p>
<p>My theme is available at <a href="https://github.com/paradox460/paradox-theme">https://github.com/paradox460/paradox-theme</a></p>
</div>
<h2><a href="#the-beginning" aria-hidden="true" class="anchor" id="the-beginning"></a>The beginning</h2>
<p>Around the new year, the Fish team released a new update, 3.6, that brought with it many new features, and some breaking changes. Historically, when a new version of the shell has been released, I've updated my prompt/theme to accommodate the new changes, but rarely made significant changes to how the underlying prompt was built. This time, I had a few days off, so I decided that it was a good time to do some cleaning, removing old features, rewriting new ones to use fish builtins, and adding support to features that Fish has grown since the theme was first created. I still wanted the theme to be roughly the same as before, but faster, leaner, and more predictably stable.</p>
<h2><a href="#goals" aria-hidden="true" class="anchor" id="goals"></a>Goals</h2>
<p>Looking at my existing theme, there were some things I wanted to keep:</p>
<ul>
<li>Command separators</li>
<li>Right-prompt timestamps</li>
<li>Git/VCS lines</li>
<li>A display of the current git hash in the prompt</li>
<li>Ability to support variable color themes</li>
</ul>
<p>There were also some things that just didn't ever work right, worked but weren't ever of any use, or just were irrelevant with the ongoing changes to fish:</p>
<ul>
<li>Current command execution time</li>
<li>Current ruby version</li>
</ul>
<h2><a href="#initial-rewrite" aria-hidden="true" class="anchor" id="initial-rewrite"></a>Initial rewrite</h2>
<p>Years ago, when I wrote the first version of my theme, it was mostly a &quot;port&quot; of a theme I'd previously used on Zsh. The old zsh theme is lost to time, but when I initially ported it to fish, certain features the current fish shell has did not exist. The command separators were created using the unix <code>jot</code> command, with some math to calculate the width needed to print the separator. The first version of the separator didn't have a command status, and was a fixed color, but upon seeing a shell theme from a former coworker, he had short (5 hyphens) separators between commands, colored depending on the exit status of the previous command. I liked that <em>a lot</em>, and so for my theme (at the time, zsh based), I copied it. Later, I added a section, using box drawing characters, to display non-zero exit codes.</p>
<p>In the time since that theme was written and ported to fish, fish has gained the <a href="https://fishshell.com/docs/current/cmds/string.html#repeat-subcommand">string repeat</a> command. This command takes a string and number, and repeats the string the number of times. Simple. And exactly what I wanted. Fish also gained a new feature, called pipestatus. Pipestatus is similar to the plain ol' status variable, except it's a fish list (an array) of exit codes, one for each pipe. Useful. Implementing the printing of these in a style that fit my theme was fairly easy. Other sub-commands of the <code>string</code> command were used, and gave me a satisfactory output.</p>
<p>The rest of the prompt was pretty easy to make. Fish gives you a lot of useful little primitives for printing a prompt, such as a truncated <code>pwd</code>, utilities for displaying user and host name (I don't use either, I find them useless noise), and a very good VCS prompt. The VCS prompt has the ability to show you the current branch, ref, or sha, but getting it to display both a ref and a sha is still impossible. This is trivial enough to wrap in a small function, and so I did.</p>
<p>The right hand side of the prompt is even more trivial. Fish gives you a function to print rprompt, so I just overrode this function and made it print the date.</p>
<p>Finally, I hard-coded some color support into the theme. In my older version of the theme, I was using one of the <a href="https://github.com/tinted-theming/base16-shell">base16-shell</a> color scripts, but found they have a non-trivial runtime impact, and don't play nicely with older curses apps. Fish supports expressing colors in hex, and, provided your terminal emulator supports it, will directly print components of its &quot;ui&quot; in the colors you specify. You can set these via variables, most prefixed <code>fish_color</code>. This is all fairly well documented, and if you didn't want to do it in a theme file, you can do it via Fish's web configuration interface.</p>
<h2><a href="#evolving-the-color-scheme-support" aria-hidden="true" class="anchor" id="evolving-the-color-scheme-support"></a>Evolving the color scheme support</h2>
<p>Many modern terminal emulators support use of codes to set colors in the terminal. Not colors over certain areas, but rather, the <em>terminal colors themselves</em>. Things like the background, what the older color values are painted as, and so forth. I primarily use iTerm2, which has its own set of escape codes to set colors, and I wanted my theme to ensure that it was rendering the way I intended, so I added support for these color codes.</p>
<p>While doing this, I hacked in some primitive support for <a href="https://github.com/tinted-theming/base16-shell">base16-shell</a> themes. This was partially because I have a friend, who has expressed interest in my theme, who is unable to use dark mode due to his astigmatism. By adding a &quot;hook&quot;<sup class="footnote-ref"><a href="#fn-hook" id="fnref-hook" data-footnote-ref>1</a></sup>, I added support for grabbing the colors out of a theme file, and using them appropriately in the fish UI.</p>
<p>This wasn't the best approach, because both the <a href="https://github.com/tinted-theming/base16-shell">base16-shell</a> and my theme would attempt to set terminal colors (at least in iTerm2). So as a hack I tossed in a check for either a theme-specific variable, or the presence of a base16-shell set envar, and if found, disable the theme shell color setting.</p>
<h3><a href="#lightdark-mode" aria-hidden="true" class="anchor" id="lightdark-mode"></a>Light/Dark mode</h3>
<p>This was mostly satisfactory, until I got the bright idea to add automatic day-night theme support. Apple exposes the current color scheme to any application that asks for it via a <code>defaults</code> property, so by running <code>defaults read -g AppleInterfaceStyle</code> you can determine if the user is using a light or dark mode. Thinking that supporting two different color schemes would be trivial, I added a set of light colors, and a check to toggle between the two. This turned out to be a rabbit hole that I spent more time on than the writing of everything up to this point.
First, reading the value doesn't always return a value. If the user is on light mode, then you get <em>nothing</em> back. Normalizing this value wasn't hard, but it was another thing I just had to figure out, as documentation was sparse.
Second, I felt that, for non-mac users, there should be a way for them to set their color scheme preferences.
Finally, for users of a specific color scheme from base16-shell, I added a check that just bypasses all the light/dark logic.</p>
<p>Gradually, other features grew into this functionality. None of it is terribly exciting, mostly just trial and error, so I've summarized it:</p>
<ul>
<li>Checking the system color scheme at prompt init time gives you a colored prompt, <em>and never checks again</em>. This means that if you change your scheme, you'll get a shell that is dark or light, and sticks out. I added a configuration to check the system color changes with <em>every</em> prompt (defaulted to off)</li>
<li>Users may want to manually trigger a color refresh, such as if they've manually loaded colors or changed variables. Exposing internal functions as external ones allows for this, in a convenient way</li>
<li>Some shells will still fudge color rendering, for things like bold colors. This is true, even if you set the extended color table values (more on that in a moment). There's nothing you can do about it, other than instruct your users.</li>
<li>Programs using older versions of ncurses will break your 256 and true-color themes. Short of the program fixing its dependency, all you can do is reset the colors on program exit. There's no universal way to do this, so for the few apps I use that do it (tig), I just aliased their commands to a function that calls the program, and then calls the color scheme refresh command on exit.</li>
</ul>
<h3><a href="#extended-colors-and-other-terminals" aria-hidden="true" class="anchor" id="extended-colors-and-other-terminals"></a>Extended colors and other terminals</h3>
<p>Some programs have support for &quot;extended colors.&quot; This lets you specify additional colors over the older 16 colors, which can enhance TUIs. The <a href="https://github.com/tinted-theming/base16-shell">base16-shell</a> project has long set these colors, but the code I'd added to my prompt <em>didn't</em>. I thought I didn't care about this feature too much, but seeing the colors absent from certain programs was jarring. Fixing it was trivial enough, one just has to find the extra colors that can be set, and set them. I was only targeting iTerm2, and so this was a piece of cake.</p>
<p>But what about other terminals? One of my friends uses Linux, and the theme consistently didn't render as well as I'd have hoped on their terminal. And, from time to time, I'll open a terminal in VSCode, which doesn't support the iTerm2 color codes either, and so my prompt would render funky there as well. Fixing this wasn't the most trivial thing, but it wasn't exciting either. Mostly trial and error, again. Along the way I had a few false starts, with mistakes about how to encode the colors (hex pairs, 0-255 pairs, etc.), how to properly send the escape codes, and so forth. Documentation on this is <em>atrocious</em>. I had to consult several older websites, wikipedia, and then engage in a lot of testing in <code>kitty.app</code> to get the output I wanted.</p>
<p>During this iteration, I also &quot;matured&quot; some features. I genericized some terms (they previously referenced only iTerm2), optimized some code paths, and fixed up the readme (which I wrote using <a href="/blog/2023-02-05-asciidoc-and-markdown">asciidoc</a>). The theme is now &quot;stable&quot;, and I've not had to make any changes since. If you do choose to use it, and find something wrong with it, please open an issue, and we can figure it out.</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-hook">
<p>Mostly just a check to see if the <code>$BASE16_SHELL_ENABLE_VARS</code> variable was set, and then reading the color values into my internal color variables. <a href="#fnref-hook" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
    <item>
       <title>I Wish AsciiDoc was more popular</title>
       <link>https://pdx.su/blog/2023-02-05-asciidoc-and-markdown</link>
       <pubDate>Sun, 05 Feb 2023 17:39:53 -07</pubDate>
       <guid>https://pdx.su/blog/2023-02-05-asciidoc-and-markdown</guid>
       <description><![CDATA[ <h1><a href="#i-wish-asciidoc-was-more-popular" aria-hidden="true" class="anchor" id="i-wish-asciidoc-was-more-popular"></a>I wish Asciidoc was more popular</h1>
<p>I've been using Markdown for a long time, and have grown accustomed to it. It has various quirks, features, and oddities, but what doesn't. But recently I decided to take a look at Asciidoc, a Markdown &quot;competetor&quot;. I found it a great little document toolchain, but it won't replace Markdown.</p>
<p>Both Asciidoc and Markdown allow you to write text based content using a simplified markup syntax. Instead of heavier syntaxes, such as LaTeX, HTML, and friends, or rich-text formats that require WYSIWYG, both allow you to focus on the writing, and add formatting with simple, plain-text friendly characters. And, for most use cases, you won't really see the difference between Markdown and Asciidoc. 90% of the time, it won't make a difference in how you write, save maybe using a different character to format something some way. But the remaining 10% is where empires are built, and in this area Asciidoc is far superior.</p>
<h2><a href="#markdown" aria-hidden="true" class="anchor" id="markdown"></a>Markdown++</h2>
<p>Asciidoc will happily accept Markdown, verbatim. You can take a Markdown snippet, paste it into your Asciidoc file, and it will (generally) work. But ad has so much more to offer, and so many little better ways of doing something, that you'll soon wish Markdown worked similarly.</p>
<p>For example, Markdown has a syntax for inserting a break, inside a paragraph. You put two space characters at the <em>end</em> of a line, and the parser will inject a line break. Unfortunately, spaces are a &quot;weird&quot; character. They're not visible, unless you turn on the &quot;show invisibles&quot; equivalent in your editor, and even with that enabled, it can be difficult to see them while scanning a document. Also, many editors will remove trailing spaces, causing your break to disappear. You can typically configure them to <em>not</em> do this to a Markdown document, but its another thing you have to remember, hope the developer of the editor remembered, or set up a plugin to do for you.</p>
<p>You don't want to make newlines significant, like GitHub does on its issues and some other sites have done, because then you lose a great feature of Markdown (and most other markup languages): the ability to format and fit content in your editor, loosely independent of how it would be presented to the user. If you've got your editor set up to hard-wrap at 80 cols, for example, you want those lines to be joined together in the output, as the width of output content is a stylistic concern. Copying and pasting a hard-wrapped snippet of text into GitHub usually requires you to join the lines together, using an editor feature (like <code>J</code> in vim) if you're lucky, and by hand if you're not.</p>
<p>Asciidoc fixes this elegantly. Instead of making trailing spaces indicative of a significant break, they make a trailing <code>+</code> character indicate the break. This solves basically every single problem the Markdown implementation has. You can visually scan for the <code>+</code> at the end of a line, you can have syntax formatting that makes it significant, and, going the other way, you <em>don't</em> need special editor features to show you its presence.</p>
<p>And this style of minor improvements persists almost everywhere else throughout Asciidoc. The formats it chooses for its primitives are <em>better</em> than Markdown, in almost all cases. I'm not a huge fan of how it approaches links (you put the URL <em>before</em> the link's text), but it's not any stranger than HTML.</p>
<p>You get a lot more formatting tools out of the box, including super and subscripts admonitions, easy video and audio embeds, automatic references, tables, and more. And where features are common across both Asciidoc and Markdown, the Asciidoc implementation is typically better, such as how they handle nested and ordered lists. Markdown list depth is usually a game of wrangling with indents and newlines, to get your particular parser to pick up and agree to how they work. Asciidoc uses a repeated list delimiter approach, where you just repeat the delimiter to indicate depth:</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1">* item 1
</span><span class="line" data-line="2">** sublist
</span><span class="line" data-line="3">*** sub-sublist
</span><span class="line" data-line="4">* item 2
</span></code></pre>
<p>Ordered lists are better too. Instead of having to either manually number them yourself, or just use <code>1.</code> as the indicator, you just use a <code>.</code> character:</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1">. item 1
</span><span class="line" data-line="2">. item 2
</span><span class="line" data-line="3">. item 3
</span></code></pre>
<p>You can also adjust where the list starts, and any skips, via attributes.</p>
<p>Other list types exist too, such as definition lists, questions and answers, and checklists.</p>
<p>Asciidoc also has first class support for &quot;admonitions&quot;. Admonitions are basically a specifically formatted block of text, designed to draw the readers eye, and call out something that might be relevant to the surrounding text, but is ultimately not part of it.</p>
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>This is an admonition!</p>
</div>
<p>If you've ever read the O'Reilly programming books, you should be familiar with these little things, they're used liberally.</p>
<h2><a href="#attributes-and-blocks-give-it-superpowers" aria-hidden="true" class="anchor" id="attributes-and-blocks-give-it-superpowers"></a>Attributes and blocks give it superpowers</h2>
<p>One of the most powerful features of Asciidoc is the attributes and blocks system. Asciidoc documents are structured in blocks, which are arbitrary length collections of lines. Lines can be text, attributes, directives, or formatting instructions.</p>
<p>You can arbitrarily create a block using some delimiters, and then use directives and attributes to change things about that block. Admonitions, which I mentioned earlier, are just one example of a special block type. There are also attributes that affect lists, code blocks, blockquotes, tables, images, and even just simple ones for adding a CSS class to a paragraph.</p>
<p>There are inline attributes that let you have access to more powerful text formatting, such as attributes for underlining text, inserting a <code>kbd</code> formatting style to indicate keystrokes, and more.</p>
<h2><a href="#includes-macros-and-references-save-you-time" aria-hidden="true" class="anchor" id="includes-macros-and-references-save-you-time"></a>Includes, macros, and references save you time</h2>
<p>Includes, macros, and references are all things that can simplify document creation, particularly in the case of technical writing and documentation, where you have lots of repeated content, content that could be better written in its own source file, and cross-links.</p>
<p>Includes are what they sound like, the ability to have all or a portion of another document injected into the current one. If you've ever used latex, and have a &quot;master&quot; document that includes all the other documents in your project, you'll understand how nice it is. You can split up thoughts into chapters or logical sections, with file-system level distinctions, and then arrange them however you want without having to cut and paste large amounts of text.</p>
<p>Macros are great for repeated urls, acronyms, and disclaimers. You can define them once, and just use a shortcut syntax to reference them anywhere.</p>
<p>Finally, the references syntax Asciidoc uses is far superior to the de-facto reference syntax Markdown has adopted. Markdown cross-references are a function of the output, typically being HTML, and require your output tool to generate memorable, but unique, IDs on your headers, so you can link to them like <code>[some reference](#some-reference)</code>. There's generally no easy way to know what the reference will be ahead of time, and no way to customize or override it.</p>
<p>In Asciidoc? References are first class, and have special features. All Asciidoc implementations use the same rules for a reference, so its easy enough to predict what it is. References are inserted using a special syntax, compared to links, which makes them visually distinct while editing. And references, by default, insert the contents of the <em>header that defined them</em> at the reference site. You can override it, both at the reference site, <em>or the default at the header</em>. In one document, I had a very long header title, that was referenced frequently. I set its reference tag to a short 3 letter abbreviation, and the injected text to be a slightly longer abbreviation.</p>
<h2><a href="#extensibility" aria-hidden="true" class="anchor" id="extensibility"></a>Extensibility</h2>
<p>Asciidoc is inherently extensible. Since the document structure is very well-defined and described, writing extensions that hook into any part of the processing isn't difficult. You can add your own custom blocks, admonitions, or anything else. You can implement your own handlers for things such as video tags, so you can reference a YouTube video as simply as writing <code>video::4QdWRgNdir4[youtube]</code></p>
<h2><a href="#the-downsides" aria-hidden="true" class="anchor" id="the-downsides"></a>The downsides</h2>
<p>After reading all that, you might be wondering if there's anything bad about Asciidoc? I've waxed positive about it for nearly 8000 characters, but in fairness we should discuss some of the things that <em>aren't so good about it</em>.</p>
<p>First off, it's a <em>single</em> implementation.<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup> This is both a blessing and a curse. The blessing is that you only ever have to worry about how your document will be parsed <em>once</em>. The curse is that you have to be happy with whatever the main Asciidoc developers decide, or write your own extensions. If your extensions get too far out of sync with the main standard, you kind of run into the problem Markdown faces, where you're basically a language that looks and kind of reads the same, but is ultimately incompatible.</p>
<p>Single implementation also limits its utility for other systems. If you're maintaining a service, like StackOverflow, Reddit, or GitHub, and you want to parse Asciidoc content for your users, doing so can be more complicated than it would be with Markdown. I wouldn't be surprised if there's a Markdown parser in every programming language ever written, but I would be surprised if you can find more than a handful of Asciidoc processors. The Asciidoc website lists 3 official ones, a ruby one, a javascript one (transpiled from ruby), and a java one. There's no C implementation, no rust one, none of that. So if you want to get Asciidoc support in Elixir, you have to either write your own, hope someone else wrote one, or come up with some way to shim one of the official ones into your application (NIF, port, dedicated service).</p>
<p>With Markdown, if you wanted to parse user content, and you were worried about the parsing being a bottleneck, you had a wide variety of options to choose from. You could just send the raw MD over the wire, and let a client-side piece of JS do the formatting (to be fair, you can do this with Asciidoc too). Or you could reach for some speed-optimized Markdown processing toolchain, implement it on your server, and go about your day. Over a decade ago, reddit moved between Markdown parsers a few times, from a python one, to discount, to a variant of <a href="https://github.com/reddit/snudown">sundown</a>. I suspect other sites that parse a large amount of Markdown content, such as StackOverflow or GitHub, have done similar things.</p>
<p>GitHub <em>does</em> support Asciidoc, and their support is very good, but it runs in Asciidoc <em>safe mode</em>, which disables some of the more interesting features such as includes. Additionally, you have to do some special trickery to get admonition icons working right.</p>
<h2><a href="#markdown-is-still-king" aria-hidden="true" class="anchor" id="markdown-is-still-king"></a>Markdown is still king</h2>
<p>Markdown has inertia, and that's one hell of a thing. Its almost ubiquitous at this point. There are editor plugins, there are services for it, there are universal conversion tools that convert to and from Markdown. Many languages even have <em>multiple</em>, competing Markdown implementations.</p>
<p>Markdown is (loosely) universal. You can take something written using primitive Markdown (not any specific implementation's features, but the core described by Gruber or Commonmark) and use it on a huge variety of sites and services.</p>
<p>Markdown is fairly extensible, within reason. While the true extensibility of Markdown depends on your processing toolchain, how hard you want to work, and what you're willing to do, it is ultimately still extensible. Asciidoc extensions are more standardized and easier to reason about, but there are simply <em>more</em> Markdown extensions and implementations out there.</p>
<p>Want GitHub-style Markdown? Go for it, GitHub has even <a href="https://github.github.com/gfm/">published a standard for GFM</a>. Want your own? No problem, Slack and Telegram have both done it.</p>
<p>If you want to build documentation sites, static sites, dynamic sites, use a CMS, use a form, whatever, chances are there's extensive Markdown support for what you want to do.</p>
<p>I looked into using Asciidoc for my blog. I got really excited to do so, but then ran out of steam almost immediately. There's just no real extensive support for it, so anything I was going to do, I'd be blazing my own trail. While those kinds of projects are often really enjoyable and educational, I just wanted to get the blog online, so I deferred.</p>
<p>I'll still likely keep using Asciidoc for certain types of documentation sites, although with limited support in Elixir's HexDoc, I'm not sure how often I'll be able to.</p>
<h2><a href="#addendum" aria-hidden="true" class="anchor" id="addendum"></a>Addendum</h2>
<p>In the <a href="https://news.ycombinator.com/item?id=34680558#34683736">Hacker News</a> discussion on this post, <em>zh3</em> pointed out that, on many systems, an installation of Asciidoctor is frequently rather heavy. This, as pointed out by <em>yrro</em>, is because it tries to ship with support for db-LaTeX, which allows for the generation of LaTeX (and therefore PDF) files via Docbook. On systems that use Apt for package management, one can <em>just</em> get Asciidoctor, and none of the latex support, by running</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1">apt install asciidoc asciidoc-dblatex-
</span></code></pre>
<p>You can also just install the bare minimum package by running apt with <code>--no-install-recommends</code></p>
<h3><a href="#djot" aria-hidden="true" class="anchor" id="djot"></a>Djot</h3>
<p>Some other commenters, both on reddit and HackerNews, have pointed out the <a href="https://djot.net/">djot</a> markup language as a markdown alternative. Djot was created by <a href="https://johnmacfarlane.net/beyond-markdown.html">John MacFarlane</a>, creator of Pandoc and driving author behind CommonMark. It's safe to say that he understands markdown more than almost anyone else, and Djot arose out of some of his ideas on how to &quot;fix&quot; markdown.</p>
<p>Djot does a lot well, some less-well, and some that I've yet to form an opinion on. I've used it even less than AsciiDoc, so take that into account. I'm mostly writing this after skimming the <a href="https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html">syntax cheat sheet</a>.</p>
<p>I really like that it attempts to put user-controllable overrides to basic markdown parsing. Things like intra-word emphasis (where you have an emphasized section within a word) are tricky in Markdown, due to how the parser binds left or right delimiters. In djot, if the defaults don't do what you want, you can <em>force</em> a delimiter to be left or right facing, by prepending/appending a <code>&lbrace;&rbrace;</code> as appropriate: <code>normal&lbrace;_emphasis_&rbrace;normal</code>.</p>
<p>The &quot;powers&quot; granted by the use of <code>&lbrace;&rbrace;</code> as meaningful characters allow for some new syntaxes, such as <code>&lbrace;+ins+&rbrace;</code> and <code>&lbrace;-del-&rbrace;</code>, for their respective HTML tags. Similarly, super and subscript are simpler to use, because the whole block will be wrapped in <code>&lbrace;&rbrace;</code></p>
<p>I like the approach to soft line breaks, which is something AsciiDoc <em>also</em> does better than markdown. I actually like the djot approach more, as its more inline with other tools that programmer-types might encounter. In djot you make a soft line break by ending a line with a <code>\</code> character.</p>
<p>I'm not sure if I like the idea of smart punctuation by default. I understand, most of the stuff we're going to write in a markup language will be prose, and for a long time a lot of us just ran our MD content through SmartyPants or a similar tool, but I've fought too many battles with the punctuation prettifier on macOS to feel entirely comfortable with this approach. At least, with djot, there's an easy escape hatch via the slash-escape (<code>\&quot;</code>), consistent with other special characters. And djot quotes also respect/listen to <code>&lbrace;&rbrace;</code>, similarly to how strong and emphasis characters do, to indicate the direction of a quote.</p>
<p>Attributes and raw sections (both block and inline) are welcome, although I haven't run into much need for output-conditional segments of a document before.</p>
<p>Djot's pipe tables are better than most markdown implementations, as they allow header-less tables to be constructed:</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1">| cell 1 | cell 2 |
</span><span class="line" data-line="2">| cell 3 | cell 4 |
</span></code></pre>
<p>Djot lets you have <em>multiple</em> headers in a table, which is nice for more complex applications.</p>
<p>Unfortunately, it doesn't improve on the ability to do col/row spans, so you're still stuck doing them by hand in pure HTML.</p>
<p>Finally, heading links get a tiny improvement, in that every single heading defines itself as a reference link. Reference links are a feature of djot (and markdown) where you can put the url somewhere in your document, typically at the bottom, with an identifier, and then only use that identifier at the individual link sites.</p>
<pre class="athl"><code class="language-plaintext" translate="no" tabindex="0"><span class="line" data-line="1">
</span><span class="line" data-line="2">This has a [reference link][]
</span><span class="line" data-line="3">
</span><span class="line" data-line="4">[reference link]: https://www.youtube.com/watch?v=t5JDypdHNnw
</span></code></pre>
<p>This solves the issue of having to figure out how the heading text would be transformed into an anchor tag, although I still would prefer the ability to make <em>explicit</em> reference links, complete with their own text, like AsciiDoc permits</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>Many commenters on hackernews and reddit have pointed out that while its true that Asciidoctor is a single implementation, Asciidoctor itself is a reimplementation of the original, python2 implementation of asciidoc. There is a python3 continuation of asciidoc, but the &quot;official&quot; one is Asciidoctor. But they are all attempting to adhere to the same standard, the same flavor, unlike Markdown. <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
    <item>
       <title>Finally, a good shower(head)</title>
       <link>https://pdx.su/blog/2023-01-30-a-good-shower</link>
       <pubDate>Mon, 30 Jan 2023 22:00:54 -07</pubDate>
       <guid>https://pdx.su/blog/2023-01-30-a-good-shower</guid>
       <description><![CDATA[ <h1><a href="#finally-a-good-shower" aria-hidden="true" class="anchor" id="finally-a-good-shower"></a>Finally, a good shower</h1>
<p>Showers are a part of my daily routine. And I've been putting up with a lousy shower head for too long. But I've finally found a new one, one that I like.</p>
<p>Yep, that's right, I've written an entire blog post about a <em>showerhead</em>. It's a bit of a review, a bit of me having a little nerd-out over a &quot;gadget&quot;, and hopefully an interesting article.</p>
<h2><a href="#the-past" aria-hidden="true" class="anchor" id="the-past"></a>The past</h2>
<p>A lot of people will agree with me when I say that showers don't feel as satisfying as they used to. Not much has changed, the US national shower maximum flow rate is still 2.5<abbr title="Gallons per Minute">gpm</abbr>, and has been since 1992<sup class="footnote-ref"><a href="#fn-1" id="fnref-1" data-footnote-ref>1</a></sup>. Some states have lower maximum standards, and so manufacturers have been targeting lower and lower flow rates.</p>
<p>Shower head manufacturers attempt to juice their showerheads, make them deliver a shower like the ones we used to have, by a variety of tricks. Aerating the water, decreasing the number of holes, spreading them out, nozzle design, and so forth. The problem is that these all fundamentally don't work. They make the streams feel weak, prickly, cold, dispersed, or otherwise unsatisfying.</p>
<p>I've tried many showerheads over the years, both as they come from the factory and with the flow reducer removed (in one case, drilled out). Things would get a little better, but still nowhere near as good as I remember the showers I had as a child at my grandparents house was. They had an old speakman showerhead, and that thing would just absolutely drench you in water.</p>
<p>Some people would point out that you can probably find such a showerhead on Ebay or at a Garage sale. That's true, you can buy an older shower head and experience torrential showers again, but there is something to be said about using a bit less water. At the very least, your water bill stays low.</p>
<h2><a href="#the-problem" aria-hidden="true" class="anchor" id="the-problem"></a>The problem</h2>
<p>In my home, we have 5 bathrooms. The previous owner left behind a variety of showerheads, the most common being simple <a href="https://amzn.to/3U0l4Hx">Waterpik brand sprayer</a>, with &quot;6 spray patterns&quot; proudly silk-screened onto the white plastic side. These are adequate, but ultimately unsatisfying.</p>
<p>In the master bathroom, I replaced it with a Moen magnetic showerhead, something we moved with us from our previous apartment. This shower works, has a wand <em>and</em> fixed unit, and was &quot;good enough&quot; for me to put up with.</p>
<p>I put a Delta equivalent of the Moen shower head in the main bathroom on the upper floor, which was mostly used by guests, and forgot about it. I never really cared for it, but it got the job done for a few years before being replaced by the Moen.</p>
<p>I hadn't given much thought about replacing any of the showerheads, until my father-in-law was visiting for Thanksgiving, and mentioned that one of the waterpik showers, in the kids jack-and-jill bathroom, was far superior to the Delta. He's visited us several times, but this was the first time it came up. Chagrined, I apologized for the lackluster shower, but didn't really plan to do anything about it, as modern showerheads are unilaterally pretty terrible.</p>
<p>I mentioned it on the phone to my parents, who, when visiting us, typically sleep in a bed on the ground floor, not upstairs, and have their own small bathroom adjoining. This bathroom has a waterfall style showerhead. They told me that this showerhead was very unsatisfying as well.</p>
<p>So I've now had two complaints about crappy showers. This nagged at me, and eventually I decided to see if I could fix it.</p>
<h2><a href="#the-fix" aria-hidden="true" class="anchor" id="the-fix"></a>The fix</h2>
<p>Browsing online, I searched <code>wirecutter shower</code> to see if Wirecutter had a new review on showerheads. They'd praised the Moen and Delta showerheads I'd had before, so I wasn't really hoping for much, but lo and behold they'd updated <a href="https://www.nytimes.com/wirecutter/reviews/the-best-shower-head/">their review</a>. They still push the multi-head units I'd used before, but in the &quot;also great&quot; section, they mentioned a new brand, High Sierra.</p>
<p>The review was generally positive, but the showerhead was critiqued for its lack of features. I've never used anything other than &quot;spray,&quot; and so this didn't bother me. Figuring I could return it if I didn't like it, I bought one of the <a href="https://amzn.to/3PXhDQH">2.0gpm fixed sprayers</a>.</p>
<h2><a href="#the-future" aria-hidden="true" class="anchor" id="the-future"></a>The future</h2>
<p><img src="/.netlify/images?url=/postimages/shower.jpg" alt="&quot;showerhead&quot;" /></p>
<p>When the shower arrived, I was initially impressed. Inside its extremely minimal packaging, it sits, with no flair. It's heavier than the other showers I have, which are physically larger, but made of plastic. It's got very simple construction, and everything feels well-made. The ball pivot joint moves freely, the toggle is smooth and easy to articulate, and installation was a breeze, as the mount has two flat sides, so you can get a pair of smooth-jawed pliers around it, to cinch down and prevent leaks.</p>
<p>Turning the water on is surprising. You'd expect a 2gpm shower to have less presence than a 2.5gpm one, but that was not the case. Immediately a thick, wide spray of water came cascading down. Angling the shower up to spray my head, and not my torso (being tall has its own issues), I was very quickly soaked. Toggling the water off, I lathered up with a shampoo bar, rubbed some Dial soap on the important areas, and turned the water back on. Rinsing off was a treat, the water easily carried the soap and grime away.</p>
<p>Taking a moment to just relax and enjoy the shower, I noted that it generates a fantastic amount of steam and vapor. This made the whole shower feel warm, not just the area under the spray.</p>
<p>Stepping out, I decided to purchase these for <em>all</em> 5 of my showers.</p>
<p><img src="/.netlify/images?url=/postimages/shower2.jpg" alt="&quot;master bath shower combo unit, showing two showerheads&quot;" /></p>
<p>For the master, I've installed a <a href="https://amzn.to/3vnuNiY">combo unit</a>, which has two 2.0gpm units, one fixed and one on a hose. You can turn them on and off independently, so if you want you can have a 4gpm shower.</p>
<p>For the upstairs main, I ordered a <a href="https://amzn.to/4aqBQq8">handheld unit</a>, as it is a shower tub, and the handhelds are useful for cleaning the tub, a dirty child, and filling mop buckets.</p>
<p>For the other two bathrooms, I've ordered more of the fixed unit that prompted me to write this article.</p>
<h2><a href="#you-seriously-wrote-an-article-about-showers" aria-hidden="true" class="anchor" id="you-seriously-wrote-an-article-about-showers"></a>You seriously wrote an article about showers?</h2>
<p>Yup. I've always had a strong sense of &quot;If you're going to do something, do it right.&quot; Particularly for things you do <em>every day</em> (hopefully). Programmers endlessly tune their programming environments, changing and configuring editors, shells, terminal emulators, browsers, tooling, and endless other changes, designed to improve their experience. Why not apply this to the rest of your life?</p>
<p>Additionally, I just feel impressed by the company that makes the shower heads. They're a small company, making their showers here in the US, using metal, and not going in for heavy marketing or packaging budgets, or really anything <em>other than the showerhead itself</em>. I want to see them continue to succeed, and so I'm waxing poetic about them.</p>
<section class="footnotes" data-footnotes>
<ol>
<li id="fn-1">
<p>The U.S. Energy Policy Act of 1992 established this limit. <a href="#fnref-1" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
</li>
</ol>
</section> ]]></description>
    </item>
  </channel>
</rss>
