<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://brendonmatheson.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://brendonmatheson.com/" rel="alternate" type="text/html" /><updated>2026-09-20T14:25:24+00:00</updated><id>https://brendonmatheson.com/feed.xml</id><title type="html">Brendon Matheson</title><subtitle>The personal blog of Brendon Matheson, since 2001.</subtitle><entry xml:lang="[[English]]"><title type="html">How I Replaced JIRA with a Very Small Shell Script - ticketscli</title><link href="https://brendonmatheson.com/2026/09/19/how-i-replaced-jira-with-a-very-small-shell-script.html" rel="alternate" type="text/html" title="How I Replaced JIRA with a Very Small Shell Script - ticketscli" /><published>2026-09-19T00:00:00+00:00</published><updated>2026-09-19T00:00:00+00:00</updated><id>https://brendonmatheson.com/2026/09/19/how-i-replaced-jira-with-a-very-small-shell-script</id><content type="html" xml:base="https://brendonmatheson.com/2026/09/19/how-i-replaced-jira-with-a-very-small-shell-script.html"><![CDATA[<p>tl;dr: I built <strong>ticketscli</strong> - an open source, terminal-based issue tracking system that takes the place of things like <a href="https://www.atlassian.com/software/jira">JIRA</a>, tracks tickets as <a href="https://daringfireball.net/projects/markdown/">Markdown</a> files in <a href="https://git-scm.com/">Git</a> and can be used as the basis for your <a href="https://developer.microsoft.com/blog/spec-driven-development-ai-native-engineering/">spec-driven development</a> workflow.<!-- excerpt end -->  If that sounds interesting and you want to learn more, see the quick demo below or the full docs on <a href="https://github.com/polycloudinc/ticketscli">GitHub</a>.  After the quick demo is the background on how I got here.</p>

<h1 id="quick-demo-of-ticketscli">Quick Demo of ticketscli</h1>

<p>Say we’re starting a new project called “FaceSpace” - the first thing we do is install the ticketscli <a href="https://microsoft.github.io/apm/">APM</a> package targeting our particular AI coding agent, in this case <a href="https://opencode.ai/">OpenCode</a>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir </span>facespace
<span class="nb">cd </span>facespace
git init <span class="nb">.</span>

apm <span class="nb">install </span>https://github.com/polycloudinc/ticketscli.git <span class="nt">--dev</span> <span class="nt">--target</span> opencode
</code></pre></div></div>

<p>This installs the following agent skills:</p>
<ul>
  <li>tickets-init</li>
  <li>tickets-create</li>
  <li>tickets-list</li>
  <li>tickets-review</li>
  <li>tickets-execution-plan</li>
  <li>tickets-transition</li>
  <li>tickets-rank</li>
</ul>

<p>Next we’ll initialize the tickets system for this project.  This is a one-time step that sets the ticket code prefix:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx @polycloudinc/ticketscli@latest init <span class="nt">--code-prefix</span> FCSP
</code></pre></div></div>

<p>This means our tickets will get codes <code class="language-plaintext highlighter-rouge">FCSP001</code>, <code class="language-plaintext highlighter-rouge">FCPC002</code>, etc.  This command created the storage directory and s couple of files:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">ls</span> .tickets/

settings.yaml  statistics.yaml
</code></pre></div></div>

<p>Next we fire up the AI coding agent and create our first ticket with a prompt such as:</p>

<pre><code class="language-prompt">Create a ticket for scaffolding the backend of FaceSpace using express js
</code></pre>

<p>The coding agent identifies that it should use the <code class="language-plaintext highlighter-rouge">tickets-create</code> agent skills, and uses that skill to help it correctly create the new ticket.  The agent then uses it’s model to determine the name for the ticket based on the user prompt, and uses the CLI to create the ticket:</p>

<pre><code class="language-agent">Thought: Using ticket creation skill · 1.4s
  → Skill "tickets-create"
  + Thought: Inspecting command requirements · 953ms

I’ll create the ticket with the requested Express.js backend scaffolding scope, then verify and complete its generated template fields.

Thought · 258ms
  $ npx @polycloudinc/ticketscli@latest create -n "Scaffold FaceSpace Backend With Express.js"
  Created: /workspaces/facespace/.tickets/FCSP001 - Scaffold FaceSpace Backend With Express.js.md

Thought: Reviewing ticket details · 1.9s
  → Read .tickets/FCSP001 - Scaffold FaceSpace Backend With Express.js.md
  ✱ Glob "**/Ticket.md" in . (1 match)

Thought: Editing project requirements · 1.1s

The ticket was created as FCSP001; its front matter already matches the template. I’m replacing the generated placeholders with the project purpose, outcome requirements, and the explicit Express.js decision.
</code></pre>

<p>Next, following the skill instructions, the agent fills in the Introduction, Requirements and Technical Solution sections based on the initial prompt:</p>

<pre><code class="language-agent">Thought: Revising patch context · 996ms

  ← Patched /workspaces/facespace/.tickets/FCSP001 - Scaffold FaceSpace Backend With Express.js.md
    11   ticket_completed:
    12   ---
    13   # Introduction
    14
    15 - TODO: A one to two sentence explanation of the purpose of the ticket without getting into deep requirements and without getting into the solution.
    15 + Establish the initial backend foundation for FaceSpace so server-side features can be developed consistently. The scaffold should provide a runnable Express.js application with a clear structure for future API work.
    16
    17   # Requirements
    18
    19 - TODO: Detailed requirements captured in a way that focuses on outcomes while keeping the solution negotiable.  Note the Requirements section may be organized into subsectiona if appropriate.
    19 + - The FaceSpace backend can be installed and started through documented project commands.
    20 + - The application exposes a basic health or readiness endpoint that confirms the server is running.
    21 + - The backend has a maintainable structure for adding routes, middleware, configuration, and services.
    22 + - The scaffold includes appropriate baseline error handling and JSON request/response support.
    23 + - The setup provides a foundation that can be extended with FaceSpace domain APIs without restructuring the project.
    24
    25   # Technical Solution
    26
    23 - TODO: Capture enough detail on the technical solution for there to be no ambiguities when it comes time to implement the solution.  This may mean capturing key technical decisions, architecture outline, project structure layout, interface descriptions or other elements that define the shape of the implementation.  The Technical Solution section should never contain large blocks of code.
    27 + Use Express.js as the HTTP server framework for the FaceSpace backend. Create a minimal application entry point and separate the server startup from the Express app configuration where practical.  Organize the scaffold so routes, middleware, configuration, and service logic have clear locations, and add a simple health endpoint plus baseline JSON parsing and error handling.
</code></pre>

<p>Before calling the job done, the agent uses the <code class="language-plaintext highlighter-rouge">validate</code> CLI subcommand to check for any structural issues with the ticket:</p>

<pre><code class="language-agent">Thought: Verifying fields and validation · 1.3s
  → Read .tickets/FCSP001 - Scaffold FaceSpace Backend With Express.js.md
  $ npx @polycloudinc/ticketscli@latest validate

    Error: --all or --ticket is required
    Usage: tickets validate [--all | --ticket &lt;code&gt;] [options]
    Options:
      -a, --all                 Validate all tickets
      -t, --ticket &lt;code&gt;       Validate a single ticket by code
      -d, --tickets-dir &lt;path&gt;  Path to tickets directory (default: _tickets)
      -h, --help                Show this help message

Thought · 218ms
  $ npx @polycloudinc/ticketscli@latest validate --ticket FCSP001
    Validating: .tickets/FCSP001 - Scaffold FaceSpace Backend With Express.js.md
    Created ticket FCSP001: .tickets/FCSP001 - Scaffold FaceSpace Backend With Express.js.md

    Validation passed.
</code></pre>

<p>We can see the new ticket in our backlog with the <code class="language-plaintext highlighter-rouge">list</code> CLI subcommand:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx @polycloudinc/ticketscli@latest list

Code     Subject                                                  Rank Status
<span class="nt">--------</span> <span class="nt">-------------------------------------------------------</span> <span class="nt">-----</span> <span class="nt">------------</span>
FCSP001  Scaffold FaceSpace Backend With Express.js                  1 backlog
<span class="nt">--------</span> <span class="nt">-------------------------------------------------------</span> <span class="nt">-----</span> <span class="nt">------------</span>
1 matching from 1 total tickets
</code></pre></div></div>

<p>From here we typically go into a review and prompt cycle to refine the requirements and factor our decisions into the ticket.  Once those sections are to our liking we render the ticket down to an execution plan:</p>

<pre><code class="language-prompt">Define execution plan
</code></pre>

<p>The agent follows the <code class="language-plaintext highlighter-rouge">tickets-execution-plan</code> skill instructions to produce a linear execution plan that is broken into logical phases with verification steps for each phase:</p>

<pre><code class="language-agent">Thought · 231ms
  → Skill "tickets-execution-plan"
  + Thought: Defining an editing plan · 996ms

I’ll replace the placeholder on FCSP001 with phased, checkbox-based implementation tasks and deterministic verification steps for each phase.

  → Read .tickets/FCSP001 - Scaffold FaceSpace Backend With Express.js.md
  + Thought: Planning patch phases · 2.0s

← Patched /workspaces/facespace/.tickets/FCSP001 - Scaffold FaceSpace Backend With Express.js.md
  27   Use Express.js as the HTTP server framework for the FaceSpace backend. Create a minimal application entry point and separate the server startup from the Express app configuration where practical.  Organize the scaffold so routes, middleware, configuration, and service logic have clear locations, and add a simple health endpoint plus baseline JSON parsing and error handling.
  28
  29   # Execution Plan
  30
  31 - TODO
  31 + ## Project Foundation
  32 +
  33 + ### Tasks
  34 + - [ ] Define the backend package metadata, dependency installation, and development/start commands.
  35 + - [ ] Create the Express application entry point and separate server startup from app configuration.
  36 + - [ ] Add environment configuration with a safe default port and documented local setup.
  37 +
  38 + ### Verification Steps
  39 + - [ ] Install dependencies and start the backend using the documented command; confirm the process listens on the configured port.
  40 +
  41 + ## API Baseline
  42 +
  43 + ### Tasks
  44 + - [ ] Add JSON request parsing and the initial route registration structure.
  45 + - [ ] Implement a health or readiness endpoint with a stable JSON response.
  46 + - [ ] Add a not-found response and centralized error-handling middleware.
  47 +
  48 + ### Verification Steps
  49 + - [ ] Run a scripted HTTP smoke test that checks the health endpoint, JSON response, unknown-route handling, and malformed-request behavior.
  50 +
  51 + ## Maintainability And Documentation
  52 +
  53 + ### Tasks
  54 + - [ ] Organize routes, middleware, configuration, and service directories for future FaceSpace APIs.
  55 + - [ ] Add or update the backend README with setup, run, endpoint, and configuration instructions.
  56 + - [ ] Add automated tests for the app bootstrap and health endpoint using the project test framework.
  57 +
  58 + ### Verification Steps
  59 + - [ ] Run the backend test suite and confirm all tests pass from a clean dependency installation.

Thought · 389ms

  $ npx @polycloudinc/ticketscli@latest validate --ticket FCSP001
  Validating: .tickets/FCSP001 - Scaffold FaceSpace Backend With Express.js.md

Defined the execution plan for FCSP001 with three phases:
  - Project Foundation
  - API Baseline
  - Maintainability And Documentation

Each phase includes implementation tasks and deterministic verification steps. Ticket validation passed.
</code></pre>

<p>From here we review the execution plan and make any necessary adjustments, then ask the agent to execute that plan to deliver the work.  We might ask it to execute task-by-task, phase-by-phase or end-to-end depending on the level of oversight we want to give the agent.  Regardless of the scope of execution the agent will use the <code class="language-plaintext highlighter-rouge">tickets-execute</code> skill for guidance and guardrails.</p>

<p>See the full docs for ticketscli ⁵on <a href="https://github.com/polycloudinc/ticketscli">GitHub</a>.</p>

<h1 id="atlassian">Atlassian</h1>

<p>I first used Atlassian <a href="https://www.atlassian.com/software/jira">JIRA</a> and <a href="https://www.atlassian.com/software/confluence">Confluence</a> in 2005 and worked at several shops that used those products over the ensuing years.  I also operated my own instances for about a decade, however I moved away from those tools to Git-tracked Markdown by around 2022.</p>

<h1 id="move-to-markdown">Move to Markdown</h1>

<p>I started practicing and promoting the use of Markdown for software specs and design docs in the same repo as code sometime around 2014/2015 with the main driver being versioning design docs alongside the code it pertains to.  I even got non-technical users in my team to follow this practice, made easier using the pretty nice WYSIWYG Markdown tool <a href="https://typora.io/">Typora</a> which at the time was in beta and was free to use.  Through this practice I was able to move all the content from the Confluence servers I managed out to Git-tracked Markdown by around 2018.</p>

<h1 id="obsidian">Obsidian</h1>

<p>Around 2020 while working at <a href="https://aws.amazon.com/">AWS</a> I was introduced to <a href="https://obsidian.md/">Obsidian</a> and that opened up a whole new set of possibilities with Markdown.  Through a combination of Obsidian’s built-in templates system, and the <a href="https://community.obsidian.md/plugins/dataview">Dataview</a> and <a href="https://community.obsidian.md/plugins/obsidian-tasks-plugin">Tasks</a> plugins I came up with a ticketing workflow where each ticket is a Markdown file with YAML front matter for the structured metadata about the ticket such as it’s status.  By around 2023 I had migrated all the tickets from my JIRA servers to Git-tracked Markdown as well.  I still use that Obsidian workflow to this day for my personal projects and life management as well as for professional purposes.</p>

<h1 id="spec-driven-development"><a href="https://developer.microsoft.com/blog/spec-driven-development-ai-native-engineering/">Spec-Driven Development</a></h1>

<p>With the advent of coding agents in early 2024, I started using those Markdown tickets as my way of doing SDD - and once we had Opus 4.5, that started working remarkably well.  Not because my system is doing anything special, just because the models are so good now, and you don’t need megabytes of specs to steer them effectively.</p>

<p>For a while I was quite happy with nothing more than a template Ticket.md and some simple instructions in <a href="https://agents.md/">AGENTS.md</a>.  I could tell the agent something like “Create a ticket for adding database connection pooling to the FooBar microservice” and it would create the fresh ticket in the designated <code class="language-plaintext highlighter-rouge">.tickets/</code> directory and from there I could iterate with the agent to flesh out the requirements, stipulate technical design decisions and eventually render a flat, linear execution plan which it would then execute for me.</p>

<h1 id="rough-edges">Rough Edges</h1>

<p>It worked great but over time a few small issues came up:</p>
<ol>
  <li>Sometimes the agent would mess up the ticket creation or other tasks, picking the wrong sequence number, using the wrong filename convention or some other little thing that I would have to steer back.</li>
  <li>Sometimes I had to be overly verbose as well as repetitive in my prompts to get the agent to follow the intended workflow.</li>
  <li>Once the number of tickets in <code class="language-plaintext highlighter-rouge">.tickets/</code> gets beyond 20 or so it becomes difficult to understand and plan through the backlog, and to understand what’s done already.</li>
</ol>

<h1 id="ticketscli">ticketscli</h1>

<p>To solve these issues I decided to formalize my tickets system with:</p>
<ul>
  <li>A CLI that handles the mechanical aspects such as creating tickets and transitioning status.</li>
  <li>A set of agent skills that provides much crisper workflow guidance for the coding agent.</li>
</ul>

<p>Thus was born <a href="https://github.com/polycloudinc/ticketscli">ticketscli</a>.</p>

<p>Today it’s very functional for building and curating a backlog and delivering work SDD style, and I have a backlog of improvements coming that should help it scale up beyond a solo developer scenario and handle projects that run for multiple years.  I have personally been using this workflow for about two years and using the <code class="language-plaintext highlighter-rouge">ticketscli</code> implementation of the workflow since I first built it in roughly March 2026.</p>

<p>See the full docs on <a href="https://github.com/polycloudinc/ticketscli">GitHub</a>.</p>]]></content><author><name></name></author><category term="jekyll_post" /><category term="software_engineering" /><category term="artificial_intelligence" /><category term="open_source" /><summary type="html"><![CDATA[tl;dr: I built ticketscli - an open source, terminal-based issue tracking system that takes the place of things like JIRA, tracks tickets as Markdown files in Git and can be used as the basis for your spec-driven development workflow.]]></summary></entry><entry xml:lang="[[English]]"><title type="html">modver CLI and Skills</title><link href="https://brendonmatheson.com/2026/06/15/modver-cli-and-skills.html" rel="alternate" type="text/html" title="modver CLI and Skills" /><published>2026-06-15T00:00:00+00:00</published><updated>2026-06-15T00:00:00+00:00</updated><id>https://brendonmatheson.com/2026/06/15/modver-cli-and-skills</id><content type="html" xml:base="https://brendonmatheson.com/2026/06/15/modver-cli-and-skills.html"><![CDATA[<p><a href="/2019/10/23/modver.html">modver</a> is my way of versioning software components.  I have published a small CLI and companion agent skills for using modver in a project: <a href="https://github.com/polycloudinc/modver">GitHub</a>.</p>]]></content><author><name></name></author><category term="jekyll_post" /><category term="software_engineering" /><category term="open_source" /><summary type="html"><![CDATA[modver is my way of versioning software components. I have published a small CLI and companion agent skills for using modver in a project: GitHub.]]></summary></entry><entry xml:lang="[[English]]"><title type="html">OPMC CLI and Agent Skills</title><link href="https://brendonmatheson.com/2026/06/13/opmc-cli-and-agent-skills.html" rel="alternate" type="text/html" title="OPMC CLI and Agent Skills" /><published>2026-06-13T00:00:00+00:00</published><updated>2026-06-13T00:00:00+00:00</updated><id>https://brendonmatheson.com/2026/06/13/opmc-cli-and-agent-skills</id><content type="html" xml:base="https://brendonmatheson.com/2026/06/13/opmc-cli-and-agent-skills.html"><![CDATA[<p><a href="/2019/10/18/modular-product-code-with-the-opmc-model.html">OPMC</a> is an attempt to standardize the coordinates for software components.  I have published a small CLI and companion agent skills for using OPMC in a project: <a href="https://github.com/polycloudinc/opmc">GitHub</a>.</p>]]></content><author><name></name></author><category term="jekyll_post" /><category term="software_engineering" /><category term="open_source" /><summary type="html"><![CDATA[OPMC is an attempt to standardize the coordinates for software components. I have published a small CLI and companion agent skills for using OPMC in a project: GitHub.]]></summary></entry><entry xml:lang="[[English]]"><title type="html">Fridge Bottle Rack</title><link href="https://brendonmatheson.com/2022/06/17/fridge-bottle-rack.html" rel="alternate" type="text/html" title="Fridge Bottle Rack" /><published>2022-06-17T21:41:00+00:00</published><updated>2022-06-17T21:41:00+00:00</updated><id>https://brendonmatheson.com/2022/06/17/fridge-bottle-rack</id><content type="html" xml:base="https://brendonmatheson.com/2022/06/17/fridge-bottle-rack.html"><![CDATA[<p>Earlier this year I solved the problem of untidy drinking water bottles in my refridgerator - here’s how</p>

<center>
<iframe width="560" height="315" src="https://www.youtube.com/embed/Cr42MsEULzg" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
</center>
<!-- excerpt end -->]]></content><author><name></name></author><category term="3dprinting" /><category term="jekyll_post" /><summary type="html"><![CDATA[Earlier this year I solved the problem of untidy drinking water bottles in my refridgerator - here’s how]]></summary></entry><entry><title type="html">Updated Grafana Dashboard for pfSense Network Activity</title><link href="https://brendonmatheson.com/2022/06/11/updated-grafana-dashboard-pfsense-network-activity.html" rel="alternate" type="text/html" title="Updated Grafana Dashboard for pfSense Network Activity" /><published>2022-06-11T18:52:00+00:00</published><updated>2022-06-11T18:52:00+00:00</updated><id>https://brendonmatheson.com/2022/06/11/updated-grafana-dashboard-pfsense-network-activity</id><content type="html" xml:base="https://brendonmatheson.com/2022/06/11/updated-grafana-dashboard-pfsense-network-activity.html"><![CDATA[<p>During the week an engineer from France reached out to me via LinkedIn asking if I could share the Grafana dashboard for pfSense that I screenshotted in my post on <a href="/2021/02/07/step-by-step-guide-to-connecting-prometheus-to-pfsense-via-snmp.html">connecting Prometheus to pfSense via SNMP</a>.  As it happens I’ve just been making updates to that dashboard so it’s a good time to share the code and explain how to adapt it.</p>

<p>You can get the Grafana dashboard code for this dashboard from here: <a href="/content/2022/2022-06-11-updated-grafana-dashboard-pfsense-network-activity/dashboard.json">dashboard.json</a></p>

<p>The details of the dashboard are very specific to my environment in terms of network interfaces and hosts so it’s not a drop-in solution, but with an understanding of how it works it’s trivial to customize it to your needs.</p>

<p>To recap - this dashboard shows network statistics pulled from my <a href="https://www.pfsense.org/">pfSense</a> router by Prometheus via SNMP.  Actually it also shows stats from key interfaces on my <a href="https://store.ui.com/products/usw-24-poe">UniFi 24-port managed switch</a> similarly obtained via SNMP.</p>

<p><img src="/content/2022/2022-06-11-updated-grafana-dashboard-pfsense-network-activity/dashboard.png" alt="" class="img-fluid" /></p>

<p>There’s a bit going on here but there’s only three distinct types of content:</p>

<ul>
  <li>Network traffic - showing the traffic in and out from an interface - this accounts for most of the panels</li>
  <li>pfSense State Table - the number of tracked states and the percentage of state table memory used</li>
  <li>Firewall Actions - showing the number of bytes passed or blocked across all interfaces</li>
</ul>

<p>Let’s look at each in turn and how to customize the dashboard:</p>

<h2 id="network-traffic">Network Traffic</h2>

<p>In my dashboard there are several network traffic panels, which are all the same but just for different interfaces:</p>

<ul>
  <li>bkk80router WAN</li>
  <li>bkk80router GREEN</li>
  <li>bkk80router PURPLE</li>
  <li>etc</li>
</ul>

<p>As a side-note - many many years ago my first homelab firewall was based on Smoothwall where interfaces are usually described as GREEN, RED, etc.  I still use that terminology out of habit although I’m considering changing it to improve the readability of my firewall rules. GREEN is LAN, PURPLE is trusted WiFi, BLUE is IoT WiFi,etc.</p>

<p>Each network panel shows octets (8-bit bytes) in and out with in colored blue and out colored purple and both plotted from a common zero axis but with independent vertical scales - this is so that when there’s a lot of traffic in one direction, you can still see the peaks on the other.  It does mean you need to pay attention to the Y-axis scales which may not be to everyone’s liking but it’s how I want to see the data:</p>

<p><img src="/content/2022/2022-06-11-updated-grafana-dashboard-pfsense-network-activity/network_activity.png" alt="" class="img-fluid" /></p>

<p>If you want both directions to use the same scale you can move the OUT metric to the left axis.</p>

<p>Note that I’m still using the now-legacy <a href="https://grafana.com/docs/grafana/next/visualizations/graph-panel/">Graph</a> visualization and not the newer <a href="https://grafana.com/docs/grafana/next/visualizations/time-series/">Time Series</a> visualization because although Time Series supports dual (actually multiple) axes, I haven’t been able to get get it to center zero yet but I will be updating my dashboard once I figure that out.</p>

<p>Each network panel has two queries, one for IN and one for OUT like this:</p>

<p><img src="/content/2022/2022-06-11-updated-grafana-dashboard-pfsense-network-activity/network_activity_queries.png" alt="" class="img-fluid" /></p>

<p>These queries are very similar:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># IN
irate(ifInOctets{job="snmp_pfsense",instance="10.80.2.2",ifIndex="2"}[5m])

# OUT
irate(ifOutOctets{job="snmp_pfsense",instance="10.80.2.2",ifIndex="2"}[5m])*-1
</code></pre></div></div>

<p>For IN we’re doing an irate() of ifInOctets and for OUT of ifOutOctets.  We filter on the job and instance labels to get to the right device (in my case either my pfSense box or my UniFi switch) and then ifIndex (interface index) for the specific interface we want to monitor.</p>

<p>Note I am using <a href="https://prometheus.io/docs/prometheus/latest/querying/functions/#irate">irate()</a> not <a href="https://prometheus.io/docs/prometheus/latest/querying/functions/#rate">rate()</a>.  irate() works off the last 2 metrics regardless of window size and rate() works off the entire window.  If you prefer you could do a rate() of a small window - say 30s - e.g.:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># IN
rate(ifInOctets{job="snmp_pfsense",instance="10.80.2.2",ifIndex="2"}[30s])
</code></pre></div></div>

<p>The question is how do we know what to use for ifIndex?  You can find out the index of each interface by querying for <code class="language-plaintext highlighter-rouge">ifDescr</code> in your Prometheus console:</p>

<p><img src="/content/2022/2022-06-11-updated-grafana-dashboard-pfsense-network-activity/prometheus_ifdescr.png" alt="" class="img-fluid" /></p>

<p>Here we can see that, for example:</p>

<ul>
  <li>re0 - my WAN interface is index 1</li>
  <li>re1 - my LAN interface (aka GREEN) is index 2</li>
  <li>re1.90 - VLAN tag 90 on my LAN interface which is my IoT WiFi network is index 11</li>
  <li>tun_wg0 - my WireGuard site-to-site VPN is index 7</li>
  <li>etc</li>
</ul>

<p>Now that you know the index, it’s easy to adapt the queries above to show the data for the interface you’re interested in.</p>

<h2 id="pfsense-state-table">pfSense State Table</h2>

<p>In the early days of my adoption of pfSense I had a performance issue that seemed to be correlated to a state table with more than 2% utilization - for that reason I wanted to have this in my dashboard.</p>

<p>I have two stat panels - one showing the number of states and the other showing the utilization of the state table:</p>

<p><img src="/content/2022/2022-06-11-updated-grafana-dashboard-pfsense-network-activity/state_table.png" alt="" class="img-fluid" /></p>

<p>The count of states is obtained from one of the pfSense-specific SNMP metrics:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pfStateTableCount{job="snmp_pfsense",instance="10.80.2.2"}
</code></pre></div></div>

<p>While the utilization is the percentage of the total size:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pfStateTableCount{job="snmp_pfsense",instance="10.80.2.2"}/360000
</code></pre></div></div>

<p>360000 is hardcoded as it’s not available from a SNMP metric.  I got this number from the pfSense console:</p>

<p><img src="/content/2022/2022-06-11-updated-grafana-dashboard-pfsense-network-activity/state_table_size.png" alt="" class="img-fluid" /></p>

<p>This does vary based on RAM so check your state table size and update as appropriate.</p>

<h2 id="firewall-actions">Firewall Actions</h2>

<p>Firewall actions are shown on a two axis graph similar to the network activity graphs, with PASS traffic in blue and BLOCK traffic in purple.</p>

<p><img src="/content/2022/2022-06-11-updated-grafana-dashboard-pfsense-network-activity/firewall_activity.png" alt="" class="img-fluid" /></p>

<p>The queries are:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># PASS
sum(irate(pfInterfacesIf4BytesInPass[5m]))

# BLOCK
sum(irate(pfInterfacesIf4BytesInBlock[5m]))*-1
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>This network dashboard gives me a high-level idea of what traffic volume looks like on my home network.  If I see something interesting in this high-level view and want to know the details then the next step would be to take a look at the pfSense state table to see recently passed traffic, or run a packet capture in pfSense or remotely with Wireshark over SSH into the router.</p>

<p>Let me know if this helps you or if you notice bugs or have ideas to improve it.  In the future I may do another update as I continue to enhance it for my needs.</p>]]></content><author><name></name></author><category term="infrastructure" /><category term="observability" /><summary type="html"><![CDATA[During the week an engineer from France reached out to me via LinkedIn asking if I could share the Grafana dashboard for pfSense that I screenshotted in my post on connecting Prometheus to pfSense via SNMP. As it happens I’ve just been making updates to that dashboard so it’s a good time to share the code and explain how to adapt it.]]></summary></entry><entry xml:lang="[[English]]"><title type="html">Google Pixel 3A XL Phone Stand</title><link href="https://brendonmatheson.com/2022/01/16/google-pixel-3axl-stand.html" rel="alternate" type="text/html" title="Google Pixel 3A XL Phone Stand" /><published>2022-01-16T17:07:00+00:00</published><updated>2022-01-16T17:07:00+00:00</updated><id>https://brendonmatheson.com/2022/01/16/google-pixel-3axl-stand</id><content type="html" xml:base="https://brendonmatheson.com/2022/01/16/google-pixel-3axl-stand.html"><![CDATA[<p>I’ve been playing with alternative phone platforms on and off for a few months now and I’m pretty much ready to make my <a href="https://www.gsmarena.com/google_pixel_3a_xl-9690.php">Google Pixel 3A XL</a> running <a href="https://grapheneos.org/">GrapheneOS</a> my daily driver, so I need a couple of stands for it - one for my bedside and one for my desk.</p>

<p>I decided to design my stands around a mag-safe-style USB cable, and ended up grabbing three of these “elough”-brand cables:</p>

<p><img src="/content/2022/2022-01-16-google-pixel-3axl-stand/IMG_1940.JPG" alt="" class="img-fluid" /></p>

<p>As usual the design process started with taking a bunch of measurements of the cable and phone:</p>

<p><img src="/content/2022/2022-01-16-google-pixel-3axl-stand/measurements.jpg" alt="" class="img-fluid" /></p>

<p>These measurements then were defined as parameters in Fusion360:</p>

<p><img src="/content/2022/2022-01-16-google-pixel-3axl-stand/parameters.png" alt="" class="img-fluid" /></p>

<p>The modelling process was a bit finicky because of the 10-degree angle I needed to put on the stand bed, and the various cutouts I had to make for the cable track.  The final design looked something like this:</p>

<p><img src="/content/2022/2022-01-16-google-pixel-3axl-stand/final_stand.png" alt="" class="img-fluid" /></p>

<p>Finally here’s the print in white PLA with 20% infill:</p>

<p><img src="/content/2022/2022-01-16-google-pixel-3axl-stand/IMG_20220116_211739.jpg" alt="" class="img-fluid" /></p>

<p>The tolerances on the cable channel and the socket for the magnetic plug were a bit tight on the first print, but after enlarging with a blade and a power drill I got it to friction-fit snugly into place:</p>

<p><img src="/content/2022/2022-01-16-google-pixel-3axl-stand/IMG_20220116_214854.jpg" alt="" class="img-fluid" /></p>

<p>Finally with the phone mounted in it:</p>

<p><img src="/content/2022/2022-01-16-google-pixel-3axl-stand/IMG_1948.JPG" alt="" class="img-fluid" /></p>

<p>The design came out great.  The only issue is the base is very light given the small amount of plastic, so when you grab the phone the whole stand comes with it.  Actually I had the same problem with my official Apple phone stand for my iPhone and just solved it with Blutac (of course) so I’ll probably do the same here.</p>]]></content><author><name></name></author><category term="gear" /><category term="3dprinting" /><category term="jekyll_post" /><summary type="html"><![CDATA[I’ve been playing with alternative phone platforms on and off for a few months now and I’m pretty much ready to make my Google Pixel 3A XL running GrapheneOS my daily driver, so I need a couple of stands for it - one for my bedside and one for my desk.]]></summary></entry><entry><title type="html">Librespeed Prometheus Exporter</title><link href="https://brendonmatheson.com/2021/11/28/librespeed-prometheus-exporter.html" rel="alternate" type="text/html" title="Librespeed Prometheus Exporter" /><published>2021-11-28T15:22:00+00:00</published><updated>2021-11-28T15:22:00+00:00</updated><id>https://brendonmatheson.com/2021/11/28/librespeed-prometheus-exporter</id><content type="html" xml:base="https://brendonmatheson.com/2021/11/28/librespeed-prometheus-exporter.html"><![CDATA[<p>This weekend I put together a Prometheus exporter to wrap the <a href="https://github.com/librespeed/speedtest-cli">librespeed-cli</a>.  This is an alternative to the <a href="/2020/08/22/speedtest-multiple-servers-in-prometheus-grafana-dashboard.html">Ookla speedtest-cli</a> which I’ve been using until now with the advantages that it is open-source, and you can run your own private speedtest servers using the companion <a href="https://github.com/librespeed/speedtest">librespeed / speedtest project</a>.</p>

<p>The sources are available under the Apache License 2.0 from the <a href="https://github.com/brendonmatheson/prometheus-librespeed-exporter">GitHub project</a> and a pre-built Docker image supporting armv7 and amd64 architectures is available from <a href="https://hub.docker.com/r/brendonmatheson/prometheus-librespeed-exporter">Docker Hub</a>.</p>

<p>So far I’m using this in my homelab to measure transfer speeds between my two sites (BKK80 and HEA92) and from public servers in London, New York and Singapore to my two sites:</p>

<p><img src="/content/2021/2021-11-28-librespeed-prometheus-exporter/dashboard.png" alt="" class="img-fluid" /></p>]]></content><author><name></name></author><category term="infrastructure" /><category term="observability" /><category term="open_source" /><summary type="html"><![CDATA[This weekend I put together a Prometheus exporter to wrap the librespeed-cli. This is an alternative to the Ookla speedtest-cli which I’ve been using until now with the advantages that it is open-source, and you can run your own private speedtest servers using the companion librespeed / speedtest project.]]></summary></entry><entry><title type="html">A Prometheus Exporter for 52pi UPS Boards</title><link href="https://brendonmatheson.com/2021/07/10/piups_exporter.html" rel="alternate" type="text/html" title="A Prometheus Exporter for 52pi UPS Boards" /><published>2021-07-10T18:27:00+00:00</published><updated>2021-07-10T18:27:00+00:00</updated><id>https://brendonmatheson.com/2021/07/10/piups_exporter</id><content type="html" xml:base="https://brendonmatheson.com/2021/07/10/piups_exporter.html"><![CDATA[<p>I’m waiting for a final part (specifically 10 meters of four-core telephone cable) so that I can continue my weather station build, but one part that has finally arrived is the replacement UPS board.  I got a <a href="https://wiki.52pi.com/index.php/UPS_(With_RTC_%26_Coulometer)_For_Raspberry_Pi_SKU:_EP-0118">52pi EP-0118 EPS “UPS with RTC and Coulometer”</a>.</p>

<p>The install was uneventful and went according to the documentation so there’s nothing much to add there.</p>

<p>Their docs provide a sample Python script for reading the energy values via the I2C bus and this was also easy to get going.  After getting the sample running I decided to put together a Prometheus exporter based on the <a href="https://github.com/ricoberger/script_exporter">script_exporter by Rico Berger</a>.  This is fully working now:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://localhost:9469/probe?script<span class="o">=</span>piups

<span class="c"># HELP script_success Script exit status (0 = error, 1 = success).</span>
<span class="c"># TYPE script_success gauge</span>
script_success<span class="o">{</span><span class="nv">script</span><span class="o">=</span><span class="s2">"piups"</span><span class="o">}</span> 1
<span class="c"># HELP script_duration_seconds Script execution time, in seconds.</span>
<span class="c"># TYPE script_duration_seconds gauge</span>
script_duration_seconds<span class="o">{</span><span class="nv">script</span><span class="o">=</span><span class="s2">"piups"</span><span class="o">}</span> 0.280330
<span class="c"># HELP piups_bus_voltage_volts The voltage supplied to the connected device(s)</span>
<span class="c"># TYPE piups_bus_voltage_volts gauge</span>
piups_bus_voltage_volts 5.208
<span class="c"># HELP piups_bus_current_milliamps The current drawn by the connected device(s)</span>
<span class="c"># TYPE piups_bus_current_milliamps gauge</span>
piups_bus_current_milliamps 136.805
<span class="c"># HELP piups_power_milliwatts The power consumed by the connected devices in milli-watts</span>
<span class="c"># TYPE piups_power_milliwatts gauge</span>
piups_power_milliwatts 737.561
<span class="c"># HELP piups_shunt_voltage_millivolts TBD</span>
<span class="c"># TYPE piups_shunt_voltage_millivolts gauge</span>
piups_shunt_voltage_millivolts 7.080
</code></pre></div></div>

<p>In my weather station dashboard I’ve added a side-bar showing the energy consumption of the station as queried every 15 seconds from the UPS board:</p>

<p><img src="/content/2021/2021-07-10-piups_exporter/grafana.png" alt="" class="img-fluid" /></p>

<p>I have packaged my solution as <a href="https://github.com/brendonmatheson/piups_exporter">piups_exporter on GitHub</a> and the README in that project elaborates on how to get it setup, which is very easy.</p>

<p>It was only after I got my UPS board that I realized there’s a newer model than the one that I ordered - the <a href="https://wiki.52pi.com/index.php/UPS_Plus_SKU:_EP-0136">EP-0136 “UPS Plus”</a> which provides a lot more metrics - so naturally I’ve ordered one of those and once I have it I will come back and update piups_exporter to also ship those metrics.</p>]]></content><author><name></name></author><category term="observability" /><category term="opensource" /><category term="iot" /><category term="open_source" /><summary type="html"><![CDATA[I’m waiting for a final part (specifically 10 meters of four-core telephone cable) so that I can continue my weather station build, but one part that has finally arrived is the replacement UPS board. I got a 52pi EP-0118 EPS “UPS with RTC and Coulometer”.]]></summary></entry><entry><title type="html">Weather Station v2</title><link href="https://brendonmatheson.com/2021/06/05/weather-station-v2.html" rel="alternate" type="text/html" title="Weather Station v2" /><published>2021-06-05T09:55:00+00:00</published><updated>2021-06-05T09:55:00+00:00</updated><id>https://brendonmatheson.com/2021/06/05/weather-station-v2</id><content type="html" xml:base="https://brendonmatheson.com/2021/06/05/weather-station-v2.html"><![CDATA[<p>Time to catch up on the additions and improvements on the weather station since I built the <a href="/2021/05/23/weather-station-v1.html">first MVP</a>.</p>

<h1 id="hardware">Hardware</h1>

<h2 id="overview">Overview</h2>

<p>So here’s what the weather station looks like currently:</p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/IMG_0890.JPG" alt="" class="img-fluid" /></p>

<p>The new things are:</p>

<ul>
  <li>PoE+ adapter and 3D-printed mounting bracket</li>
  <li>Replacement SSD enclosure and 3D-printed mounting bracket</li>
  <li>LED indicators for UPS board power, Pi power status and SSD activity</li>
  <li>SI1145 light sensor</li>
</ul>

<p>The AS3935 sensor plugged in at the right isn’t connected yet - that’s just so I don’t misplace it.</p>

<h2 id="poe-adapter">PoE+ Adapter</h2>

<p>I have a few these PoE+ adapters in my parts cabinet so I just threw one on to see what would happen with power being supplied by my <a href="https://store.ui.com/products/unifiswitch-24-250w">UniFi 24 port switch</a>.  It worked, but after about 5 days the system lost power.</p>

<p>After fully recharging the batteries via the barrel jack I hooked up my Ruideng USB in-line multimeter to determine that when the UPS board is already fully charged it draws about 1 milliamp from the PoE+ adapter:</p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/IMG_0921.JPG" alt="" class="img-fluid" /></p>

<p>Even though the Pi is running and drawing around half an amp:</p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/IMG_0911.JPG" alt="" class="img-fluid" /></p>

<p>In other words the UPS is driving the board off batteries even when connected to power.  Eventually it hits a threshold and starts charging again, but apparently not effectively enough because eventually the batteries run flat and the system dies.  In short - this is not a UPS, it’s a powerbank.</p>

<p>I have ordered a <a href="https://wiki.52pi.com/index.php/UPS_(With_RTC_%26_Coulometer)_For_Raspberry_Pi_SKU:_EP-0118">52pi UPS board</a>.  I believe it’s a better unit - but it’s really hard to tell from the data sheet so has to be tested.  This board has a built-in RTC (a nice-to-have for my application) and a power meter so you can read draw and battery level via I2C which will be cool if I can get that to work.  Ideally I will read those values and export them to Prometheus.</p>

<h2 id="replacement-ssd-enclosure">Replacement SSD Enclosure</h2>

<p>On Tuesday morning last week I woke up to a dead weather station.  From the Prometheus-exported host telemetry I could see that it went offline around 2:20am local time.</p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/ssd_failure.png" alt="" class="img-fluid" /></p>

<p>Through a process of elimination I determined that it was the interface board in the SSD enclosure that had failed and that the SSD itself was fine so I ordered in a new <a href="https://www.lazada.co.th/products/i2360916754-s7982440996.html">Orico brand enclosure</a>.  This one was a  different size to the original so I had to print a new bracket for it:</p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/IMG_0873.JPG" alt="" class="img-fluid" /></p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/IMG_0875.JPG" alt="" class="img-fluid" /></p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/IMG_0876.JPG" alt="" class="img-fluid" /></p>

<h2 id="led-indicators">LED Indicators</h2>

<p>Eventually this thing is going to be mounted in a cabinet on a pole in a field, so it would be helpful if the power and HDD lights were easy to see.  Fortunately Raspberry Pi supports breaking out LED’s so it was a 5 minute job to add external power and HDD activity LED’s.  I also connected a LED to one of the JST outputs on the UPS board to indicate when the board is supplying power:</p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/IMG_0933.JPG" alt="" class="img-fluid" /></p>

<h1 id="endurance-test">Endurance Test</h1>

<p>Even though the current UPS board is no good, I ran an endurance test to see how long these two 18650 cells could run the host for and the result was just under 16 hours.</p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/endurance_test_1.png" alt="" class="img-fluid" /></p>

<p>Once I get the replacement UPS board I will re-run the endurance test, but this is already a very good indicative result - being able to survive a power outage of more than half-a-day will make this a very robust system.</p>

<h1 id="sensors">Sensors</h1>

<p>I added one new sensor in this iteration.  Originally I was going to add the I2C lightning sensor which was already in my parts bin but during the week last week the GY/SI1145 light sensor came in so I hooked that up instead.</p>

<p>I broke out the I2C bus to a breadboard and reconnected the BME280 plus added the SI1145 - blue is data and orange is clock:</p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/IMG_0925.JPG" alt="" class="img-fluid" /></p>

<p>Verified connectivity and found the SI1145 at 0x60 while the BME280 is still at 0x76:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pi@hea92weather01:~ <span class="nv">$ </span>i2cdetect <span class="nt">-y</span> 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span>
10: <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span>
20: <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span>
30: <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span>
40: <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span>
50: <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span>
60: 60 <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span>
70: <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> <span class="nt">--</span> 76 <span class="nt">--</span>
</code></pre></div></div>

<p>Then added the code to read the SI1145 and include it in the Influx line protocol message published to MQTT:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">SI1145.SI1145</span> <span class="k">as</span> <span class="n">SI1145</span>

<span class="bp">...</span>

<span class="c1">#
# GY1145 / SI1145
#
</span>
<span class="n">si1145</span> <span class="o">=</span> <span class="n">SI1145</span><span class="p">.</span><span class="nc">SI1145</span><span class="p">()</span>

<span class="bp">...</span>

<span class="c1"># SI1145
</span><span class="n">lightVisible</span> <span class="o">=</span> <span class="n">si1145</span><span class="p">.</span><span class="nf">readVisible</span><span class="p">()</span>
<span class="n">lightUV</span> <span class="o">=</span> <span class="n">si1145</span><span class="p">.</span><span class="nf">readUV</span><span class="p">()</span>
<span class="n">lightIR</span> <span class="o">=</span> <span class="n">si1145</span><span class="p">.</span><span class="nf">readIR</span><span class="p">()</span>

<span class="c1"># Publish
</span><span class="n">influx</span> <span class="o">=</span> <span class="sh">"</span><span class="s">weather,station=</span><span class="sh">"</span> <span class="o">+</span> <span class="n">weather_station_id</span> <span class="o">+</span> <span class="sh">"</span><span class="s"> </span><span class="sh">"</span> <span class="o">+</span> \
	<span class="sh">"</span><span class="s">humidity=</span><span class="sh">"</span> <span class="o">+</span> <span class="nf">str</span><span class="p">(</span><span class="n">humidity</span><span class="p">)</span> <span class="o">+</span> \
	<span class="sh">"</span><span class="s">,pressure=</span><span class="sh">"</span> <span class="o">+</span> <span class="nf">str</span><span class="p">(</span><span class="n">pressure</span><span class="p">)</span> <span class="o">+</span> \
	<span class="sh">"</span><span class="s">,temperature=</span><span class="sh">"</span> <span class="o">+</span> <span class="nf">str</span><span class="p">(</span><span class="n">temperature</span><span class="p">)</span> <span class="o">+</span> \
	<span class="sh">"</span><span class="s">,visible=</span><span class="sh">"</span> <span class="o">+</span> <span class="nf">str</span><span class="p">(</span><span class="n">lightVisible</span><span class="p">)</span> <span class="o">+</span> \
	<span class="sh">"</span><span class="s">,uv=</span><span class="sh">"</span> <span class="o">+</span> <span class="nf">str</span><span class="p">(</span><span class="n">lightUV</span><span class="p">)</span> <span class="o">+</span> \
	<span class="sh">"</span><span class="s">,ir=</span><span class="sh">"</span> <span class="o">+</span> <span class="nf">str</span><span class="p">(</span><span class="n">lightIR</span><span class="p">)</span>
</code></pre></div></div>

<p>And added some panels to my weather station dashboard in my main Grafana instances:</p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/grafana_light_panels.png" alt="" class="img-fluid" /></p>

<h1 id="data-plane">Data Plane</h1>

<h2 id="overview-1">Overview</h2>

<p>Most of my time on this iteration was spent on the data plane.  The improvements here were:</p>

<ul>
  <li>Handle SIGTERM</li>
  <li>MQTT Auto-Reconnect</li>
  <li>Containerized the sensor-reader Python script</li>
  <li>Added a custom bridge network to connect the three Docker Compose stacks</li>
  <li>Added an on-prem MQTT broker and InfluxDB storage</li>
  <li>Configured the edge MQTT broker to bridge to the on-prem MQTT broker</li>
  <li>Connect Grafana to use the on-prem InfluxDB instead of edge InfluxDB</li>
  <li>Connected my Home Assistant instance to MQTT and added visualizations</li>
  <li>De-hardcode sensor-reader</li>
</ul>

<p>The data plane now looks like this:</p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/architecture.png" alt="" class="img-fluid" /></p>

<h2 id="handle-sigterm">Handle SIGTERM</h2>

<p>In order to be a good container citizen I added support for SIGTERM and SIGINT Linux signals - this is the skeleton for signal handling:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">class</span> <span class="nc">SensorReader</span><span class="p">:</span>

	<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
		<span class="n">self</span><span class="p">.</span><span class="n">stopped</span> <span class="o">=</span> <span class="bp">False</span>
        
	<span class="k">def</span> <span class="nf">run</span><span class="p">():</span>
		<span class="k">while</span> <span class="ow">not</span> <span class="n">self</span><span class="p">.</span><span class="n">stopped</span><span class="p">:</span>
            <span class="c1"># Process sensors
</span>            <span class="nf">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

	<span class="k">def</span> <span class="nf">stop</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">signal</span><span class="p">,</span> <span class="n">frame</span><span class="p">):</span>
		<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Stopping SensorReader</span><span class="sh">"</span><span class="p">)</span>
		<span class="n">self</span><span class="p">.</span><span class="n">stopped</span> <span class="o">=</span> <span class="bp">True</span>
        
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
	<span class="n">sensorReader</span> <span class="o">=</span> <span class="nc">SensorReader</span><span class="p">()</span>
	<span class="n">signal</span><span class="p">.</span><span class="nf">signal</span><span class="p">(</span><span class="n">signal</span><span class="p">.</span><span class="n">SIGINT</span><span class="p">,</span> <span class="n">sensorReader</span><span class="p">.</span><span class="n">stop</span><span class="p">)</span>
	<span class="n">signal</span><span class="p">.</span><span class="nf">signal</span><span class="p">(</span><span class="n">signal</span><span class="p">.</span><span class="n">SIGTERM</span><span class="p">,</span> <span class="n">sensorReader</span><span class="p">.</span><span class="n">stop</span><span class="p">)</span>
	<span class="n">sensorReader</span><span class="p">.</span><span class="nf">run</span><span class="p">()</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
	<span class="nf">main</span><span class="p">()</span>
</code></pre></div></div>

<p>This was adapted from a post but I’ve misplaced the reference.</p>

<h2 id="mqtt-auto-reconnect">MQTT Auto-Reconnect</h2>

<p>My original MQTT implementation was bare bones.  I’ve added code based on some helpful articles from <a href="http://www.steves-internet-guide.com/">Steves Internet Guide</a> to track connection state and leverage <a href="https://pypi.org/project/paho-mqtt/">Paho MQTT’s</a> built-in auto-reconnect functionality:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>		<span class="c1">#
</span>		<span class="c1"># Configure MQTT CLient
</span>		<span class="c1">#
</span>
		<span class="k">def</span> <span class="nf">mqtt_on_connect</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">userdata</span><span class="p">,</span> <span class="n">flags</span><span class="p">,</span> <span class="n">rc</span><span class="p">):</span>
			<span class="k">if</span> <span class="n">rc</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
				<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Connected to MQTT broker</span><span class="sh">"</span><span class="p">)</span>
				<span class="n">client</span><span class="p">.</span><span class="n">connected_flag</span> <span class="o">=</span> <span class="bp">True</span>
			<span class="k">else</span><span class="p">:</span>
				<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Failed to connect</span><span class="sh">"</span><span class="p">)</span>

		<span class="k">def</span> <span class="nf">mqtt_on_disconnect</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">userdata</span><span class="p">,</span> <span class="n">rc</span><span class="p">):</span>
			<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Disconnecting reason </span><span class="sh">"</span> <span class="o">+</span> <span class="nf">str</span><span class="p">(</span><span class="n">rc</span><span class="p">))</span>
			<span class="n">client</span><span class="p">.</span><span class="n">connected_flag</span> <span class="o">=</span> <span class="bp">False</span>
			<span class="n">client</span><span class="p">.</span><span class="n">disconnect_flag</span> <span class="o">=</span> <span class="bp">True</span>

		<span class="n">client</span> <span class="o">=</span> <span class="n">mqtt</span><span class="p">.</span><span class="nc">Client</span><span class="p">(</span><span class="n">weather_station_id</span><span class="p">)</span>
		<span class="n">client</span><span class="p">.</span><span class="n">on_connect</span> <span class="o">=</span> <span class="n">mqtt_on_connect</span>
		<span class="n">client</span><span class="p">.</span><span class="n">on_disconnect</span> <span class="o">=</span> <span class="n">mqtt_on_disconnect</span>
		<span class="n">client</span><span class="p">.</span><span class="n">connected_flag</span> <span class="o">=</span> <span class="bp">False</span>
		<span class="n">client</span><span class="p">.</span><span class="nf">username_pw_set</span><span class="p">(</span><span class="n">broker_username</span><span class="p">,</span> <span class="n">broker_password</span><span class="p">)</span>
		<span class="n">client</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="n">broker_host_name</span><span class="p">,</span> <span class="n">broker_port</span><span class="p">)</span>
		<span class="n">client</span><span class="p">.</span><span class="nf">loop_start</span><span class="p">()</span>

		<span class="k">while</span> <span class="ow">not</span> <span class="n">self</span><span class="p">.</span><span class="n">stopped</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">client</span><span class="p">.</span><span class="n">connected_flag</span><span class="p">:</span>
			<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Waiting for connection</span><span class="sh">"</span><span class="p">)</span>
			<span class="nf">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

         <span class="k">while</span> <span class="ow">not</span> <span class="n">self</span><span class="p">.</span><span class="n">stopped</span><span class="p">:</span>

			<span class="nf">if </span><span class="p">(</span><span class="n">client</span><span class="p">.</span><span class="n">connected_flag</span><span class="p">):</span>
                
				<span class="c1"># Read sensors and publish to MQTT
</span>
				<span class="nf">sleep</span><span class="p">(</span><span class="mi">15</span><span class="p">)</span>

			<span class="k">else</span><span class="p">:</span>
				<span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Connection lost.  Waiting for reconnection</span><span class="sh">"</span><span class="p">)</span>
				<span class="nf">sleep</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>

		<span class="n">client</span><span class="p">.</span><span class="nf">loop_stop</span><span class="p">()</span>
		<span class="n">client</span><span class="p">.</span><span class="nf">disconnect</span><span class="p">()</span>
</code></pre></div></div>

<p>This now means if the local MQTT broker goes down, sensor-reader will retry until it can connect again and the sensor readings will flow once again.</p>

<h2 id="containerize-sensor-reader">Containerize sensor-reader</h2>

<p>The daemon that reads sensors and publishes the values to MQTT is called <code class="language-plaintext highlighter-rouge">sensor-reader</code> and it’s implemented as a simple Python3 script.  To Dockerize this I just had to create a requirements.txt to enumerate the packages that I had previously installed manually:</p>

<pre><code class="language-txt">RPi.bme280
paho-mqtt
SI1145
</code></pre>

<p>Then created a simple Dockerfile based on the official <a href="https://hub.docker.com/_/python">Python</a> image:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> python:3.9.5-buster</span>

<span class="k">RUN </span><span class="nb">mkdir</span> /app
<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="k">COPY</span><span class="s"> main.py ./</span>
<span class="k">COPY</span><span class="s"> requirements.txt ./</span>
<span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">--no-cache-dir</span> <span class="nt">-r</span> requirements.txt

<span class="k">CMD</span><span class="s"> [ "python3", "./main.py" ]</span>
</code></pre></div></div>

<p>I initially wanted to use the alpine-based image but there is a problem running it on 32-bit RPi OS without privileged mode, so I fell back to the Debian Buster based image instead.</p>

<p>On first run I got this error:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pi@hea92weather01:~/als_sys_host_031_hea92weather01/sensor-reader <span class="nv">$ </span>./run.sh
Traceback <span class="o">(</span>most recent call last<span class="o">)</span>:
  File <span class="s2">"/app/./main.py"</span>, line 92, <span class="k">in</span> &lt;module&gt;
    main<span class="o">()</span>
...
FileNotFoundError: <span class="o">[</span>Errno 2] No such file or directory: <span class="s1">'/dev/i2c-1'</span>
</code></pre></div></div>

<p>Which was solved by mapping that device through when running the container:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>docker run <span class="se">\</span>
    <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
    <span class="nt">--device</span> /dev/i2c-1 <span class="se">\</span>
    sensor-reader:latest
</code></pre></div></div>

<p>After successfully testing the image I created a docker-compose config for easier startup:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>
<span class="na">services</span><span class="pi">:</span>

  <span class="c1">#</span>
  <span class="c1"># SensorReader</span>
  <span class="c1">#</span>

  <span class="na">sensorreader</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s2">"</span><span class="s">aleisium/sensor-reader:latest"</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s2">"</span><span class="s">always"</span>
    <span class="na">devices</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">/dev/i2c-1:/dev/i2c-1"</span>
</code></pre></div></div>

<h2 id="custom-bridge-network">Custom Bridge Network</h2>

<p>I intentionally have three separate docker-compose stacks - the reason for this is that I want to be able to easily start and stop the MQTT broker, the local InfluxDB storage and the sensor-reader independently.  This could also be achieved by having a single stack with convenience scripts to selectively stop and remove containers, and I may yet do that, but for now it’s nicer having separate stacks.</p>

<p>The problem with separate stacks is if you make them communicate via the physical <code class="language-plaintext highlighter-rouge">eth0</code> interface, connectivity is lost when the network cable is pulled.</p>

<p>To solve this I created a custom bridge network in Docker and connected the three stacks to it.</p>

<p>The bridge is created with the script <code class="language-plaintext highlighter-rouge">network/up.sh</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="nb">sudo </span>docker network create <span class="nt">--driver</span><span class="o">=</span>bridge <span class="nt">--subnet</span><span class="o">=</span>10.12.1.0/16 weather0
</code></pre></div></div>

<p>Then each of the three stacks are connected to that network - for example, <code class="language-plaintext highlighter-rouge">sensor-reader</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3"</span>
<span class="na">services</span><span class="pi">:</span>

  <span class="c1">#</span>
  <span class="c1"># SensorReader</span>
  <span class="c1">#</span>

  <span class="na">sensorreader</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s2">"</span><span class="s">aleisium/sensor-reader:latest"</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">weather0"</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s2">"</span><span class="s">always"</span>
    <span class="na">devices</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">/dev/i2c-1:/dev/i2c-1"</span>

<span class="na">networks</span><span class="pi">:</span>
  <span class="na">weather0</span><span class="pi">:</span>
    <span class="na">external</span><span class="pi">:</span> <span class="kc">true</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">weather0"</span>
</code></pre></div></div>

<h2 id="on-prem-mqtt-broker">On-Prem MQTT Broker</h2>

<p>I established a new “central” MQTT broker on a VM under ProxMox.  I also now have an InfluxDB 1.8 instance with a Telegraf agent that reads from that central MQTT and stores measurements on-prem.</p>

<p>The central MQTT, Influx and Telegraf configs are separate from my weather station GitHub but I’ll be adding a similar config soon to make it easier for others to re-use.</p>

<h2 id="edge-mqtt-bridge-to-on-prem-mqtt">Edge MQTT Bridge to On-Prem MQTT</h2>

<p>I’m using the venerable Mosquitto MQTT broker which supports bridging - meaning an instance of the broker can be instructed to act as a client to another broker to read and / or write messages.</p>

<p>I added the following bridge config to <code class="language-plaintext highlighter-rouge">broker/config/mosquitto.conf</code> for the MQTT broker on the weather station:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>connection central
address 10.80.2.60:1883
remote_username hea92weather01
remote_password password
topic # out
</code></pre></div></div>

<p>This causes the weather station to forward all messages it receives to the broker at 10.80.2.60 using the specified credentials.  In this syntax # is not a comment marker but a wildcard indicating “all” messages.</p>

<p>References:</p>

<ul>
  <li><a href="https://mosquitto.org/man/mosquitto-conf-5.html">mosquitto_conf Documentation</a></li>
</ul>

<h2 id="point-grafana-to-on-prem-influxdb">Point Grafana to On-Prem InfluxDB</h2>

<p>As I use <a href="https://grafana.com/docs/grafana/latest/administration/provisioning/">Grafana Provisioning</a> for infrastructure-as-code of my telemetry stack, I just had to commit the address change to the data source definition in Git:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="m">1</span>

<span class="c1"># list of datasources that should be deleted from the database</span>
<span class="na">deleteDatasources</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">hea92weather01"</span>
    <span class="na">orgId</span><span class="pi">:</span> <span class="m">1</span>

<span class="c1"># list of datasources to insert/update depending</span>
<span class="c1"># what's available in the database</span>
<span class="na">datasources</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">hea92weather01"</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s2">"</span><span class="s">influxdb"</span>
    <span class="na">access</span><span class="pi">:</span> <span class="s2">"</span><span class="s">proxy"</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s2">"</span><span class="s">weather"</span>
    <span class="na">user</span><span class="pi">:</span> <span class="s2">"</span><span class="s">grafana"</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s2">"</span><span class="s">password"</span>
    <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http://10.80.2.60:8086"</span>
</code></pre></div></div>

<p>Then re-launch the telemetry stack.</p>

<h2 id="home-assistant-integration">Home Assistant Integration</h2>

<p>Defined a connection to the central MQTT broker in Home Assistant’s configuration.yaml:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">mqtt</span><span class="pi">:</span>
  <span class="na">broker</span><span class="pi">:</span> <span class="s2">"</span><span class="s">10.80.2.60"</span>
  <span class="na">port</span><span class="pi">:</span> <span class="m">1883</span>
  <span class="na">client_id</span><span class="pi">:</span> <span class="s2">"</span><span class="s">bkk80ha"</span>
  <span class="na">username</span><span class="pi">:</span> <span class="s2">"</span><span class="s">bkk80ha"</span>
  <span class="na">password</span><span class="pi">:</span> <span class="s2">"</span><span class="s">password"</span>
</code></pre></div></div>

<p>Added sensors to subscribe to values of interest from MQTT:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="pi">-</span> <span class="na">platform</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mqtt"</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">HEA92</span><span class="nv"> </span><span class="s">Temperature"</span>
    <span class="na">state_topic</span><span class="pi">:</span> <span class="s2">"</span><span class="s">weather/hea92weather01/temperature"</span>
    <span class="na">device_class</span><span class="pi">:</span> <span class="s2">"</span><span class="s">temperature"</span>
    <span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">°C"</span>
    <span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0"</span>
  <span class="pi">-</span> <span class="na">platform</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mqtt"</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">HEA92</span><span class="nv"> </span><span class="s">Humidity"</span>
    <span class="na">state_topic</span><span class="pi">:</span> <span class="s2">"</span><span class="s">weather/hea92weather01/humidity"</span>
    <span class="na">device_class</span><span class="pi">:</span> <span class="s2">"</span><span class="s">humidity"</span>
    <span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">%"</span>
    <span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0"</span>
  <span class="pi">-</span> <span class="na">platform</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mqtt"</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">HEA92</span><span class="nv"> </span><span class="s">Pressure"</span>
    <span class="na">state_topic</span><span class="pi">:</span> <span class="s2">"</span><span class="s">weather/hea92weather01/pressure"</span>
    <span class="na">device_class</span><span class="pi">:</span> <span class="s2">"</span><span class="s">pressure"</span>
    <span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mbar"</span>
    <span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0"</span>
  <span class="pi">-</span> <span class="na">platform</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mqtt"</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">HEA92</span><span class="nv"> </span><span class="s">Visible</span><span class="nv"> </span><span class="s">Light"</span>
    <span class="na">state_topic</span><span class="pi">:</span> <span class="s2">"</span><span class="s">weather/hea92weather01/visible"</span>
    <span class="na">device_class</span><span class="pi">:</span> <span class="s2">"</span><span class="s">illuminance"</span>
  <span class="pi">-</span> <span class="na">platform</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mqtt"</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">HEA92</span><span class="nv"> </span><span class="s">UV"</span>
    <span class="na">state_topic</span><span class="pi">:</span> <span class="s2">"</span><span class="s">weather/hea92weather01/uv"</span>
    <span class="na">device_class</span><span class="pi">:</span> <span class="s2">"</span><span class="s">illuminance"</span>
  <span class="pi">-</span> <span class="na">platform</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mqtt"</span>
    <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">HEA92</span><span class="nv"> </span><span class="s">IR"</span>
    <span class="na">state_topic</span><span class="pi">:</span> <span class="s2">"</span><span class="s">weather/hea92weather01/ir"</span>
    <span class="na">device_class</span><span class="pi">:</span> <span class="s2">"</span><span class="s">illuminance"</span>

</code></pre></div></div>

<p>Finally added some cards to a dashboard in Home Assistant:</p>

<p><img src="/content/2021/2021-06-03-weather-station-v2/ha.png" alt="" class="img-fluid" /></p>

<p>Further elaboration is available in the <a href="https://github.com/brendonmatheson/weather-station">README.md</a>.</p>

<p>References:</p>

<ul>
  <li><a href="https://www.home-assistant.io/docs/mqtt/broker/">Home Assistant MQTT Broker</a></li>
  <li><a href="https://www.home-assistant.io/integrations/sensor.mqtt/">Home Assistant MQTT Sensor</a></li>
</ul>

<h2 id="de-hardcode-sensor-reader">De-hardcode sensor-reader</h2>

<p>Finally as a move towards making my weather station stack easily re-usable by others, I started parameterizing some of the hardcoded values in sensor-reader and moving them to environment variables so that they can be passed through the docker-compose file:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">sensorreader</span><span class="pi">:</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">WEATHER_STATION_ID=hea92weather01"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">BROKER_USERNAME=hea92weather01"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">BROKER_PASSWORD=password"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">SAMPLE_INTERVAL=15"</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s2">"</span><span class="s">aleisium/sensor-reader:latest"</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">weather0"</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s2">"</span><span class="s">always"</span>
    <span class="na">devices</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">/dev/i2c-1:/dev/i2c-1"</span>
</code></pre></div></div>

<h1 id="next-steps">Next Steps</h1>

<p>The next improvements are:</p>

<ul>
  <li>Processing of light readings into meaningful values</li>
  <li>Improve documentation and scripts to make it easier for others to re-use my configuration</li>
  <li>More sensors</li>
  <li>Test the new UPS board when it arrives</li>
  <li>Enclosure for the electronics</li>
  <li>Trial field deployment (on our condo balcony)</li>
</ul>]]></content><author><name></name></author><category term="observability" /><category term="farming" /><category term="iot" /><category term="weather_station" /><summary type="html"><![CDATA[Time to catch up on the additions and improvements on the weather station since I built the first MVP.]]></summary></entry><entry><title type="html">Irrigation Pipe Clips</title><link href="https://brendonmatheson.com/2021/05/30/irrigation-pipe-clips.html" rel="alternate" type="text/html" title="Irrigation Pipe Clips" /><published>2021-05-30T14:20:00+00:00</published><updated>2021-05-30T14:20:00+00:00</updated><id>https://brendonmatheson.com/2021/05/30/irrigation-pipe-clips</id><content type="html" xml:base="https://brendonmatheson.com/2021/05/30/irrigation-pipe-clips.html"><![CDATA[<p>Here’s a quick post about another functional print for our Bangkok condo.  This is an example of what I love about 3D printing - the ability to come up with elegant, purpose-built solutions for problems that have an addressable market of 1.</p>

<p>A few weeks back we renovated our balcony in Bangkok and as part of that exercise I completely re-installed the drip irrigation system that waters our many plants.</p>

<p>In the original install I had the flexible pipe just running along the tiles behind the pots.  The problem with this that the pipe becomes a dirt trap, collecting soil particles and leaves behind it.  For the re-install I wanted to get the pipe off the ground and thought maybe I could fabricate some clips to hold it under the lowest level of the balcony railing which would also put it somewhat out of sight as an added benefit.</p>

<p>I took some quick dimensions of the pipe and the railing:</p>

<p><img src="/content/2021/2021-05-30-irrigation-pipe-clips/IMG_0799.JPG" alt="" class="img-fluid" /></p>

<p><img src="/content/2021/2021-05-30-irrigation-pipe-clips/IMG_0797.JPG" alt="" class="img-fluid" /></p>

<p>Then sketched the shape in Fusion360 - this took all of 5 minutes:</p>

<p><img src="/content/2021/2021-05-30-irrigation-pipe-clips/design.png" alt="" class="img-fluid" /></p>

<p>It took 3 or 4 iterations of printing and testing to get the shape dialed in.  The adjustments were reducing the inner dimension to make the clip hold onto the railing tightly, and progressively thickening the shape and increasing infill to make it stronger.</p>

<p>Here’s the test of the one that became the final version:</p>

<p><img src="/content/2021/2021-05-30-irrigation-pipe-clips/IMG_0802.JPG" alt="" class="img-fluid" /></p>

<p><img src="/content/2021/2021-05-30-irrigation-pipe-clips/IMG_0803.JPG" alt="" class="img-fluid" /></p>

<p>With the model finalized I printed 14 of them out and did the final installation:</p>

<p><img src="/content/2021/2021-05-30-irrigation-pipe-clips/IMG_0879.JPG" alt="" class="img-fluid" /></p>

<p><img src="/content/2021/2021-05-30-irrigation-pipe-clips/IMG_0880.JPG" alt="" class="img-fluid" /></p>

<p>The result was good.  The clips are strong and haven’t deformed after being under load for a couple of weeks now, and the irrigation pipe is now mostly out of sight and more importantly is off the tiles making it easier for us to keep the balcony clean.</p>

<p>It remains to be seen if these PLA prints last long-term being exposed to the rain and sun - will be interesting to check back in on them in 6 months.</p>]]></content><author><name></name></author><category term="3dprinting" /><category term="farming" /><summary type="html"><![CDATA[Here’s a quick post about another functional print for our Bangkok condo. This is an example of what I love about 3D printing - the ability to come up with elegant, purpose-built solutions for problems that have an addressable market of 1.]]></summary></entry></feed>