Files
rsmsn_blog/public/index.xml
2024-07-03 18:21:05 -04:00

3037 lines
290 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>Norm-working Packets 💾</title>
<link>/</link>
<description>Recent content on Norm-working Packets 💾</description>
<generator>Hugo -- gohugo.io</generator>
<language>en-us</language>
<lastBuildDate>Wed, 03 Jul 2024 17:41:15 -0400</lastBuildDate>
<atom:link href="/index.xml" rel="self" type="application/rss+xml" />
<item>
<title>Create a GIF from a video - Right from the Command Line!</title>
<link>/posts/create_gif_on_commandline.html</link>
<pubDate>Wed, 03 Jul 2024 17:41:15 -0400</pubDate>
<guid>/posts/create_gif_on_commandline.html</guid>
<description>create_gif_on_commandline</description>
<content:encoded><![CDATA[<p>Finding this little set of commands is one of the main reasons why I love the Command Line. Once I realized that almost everything that I was doing in a UI was possible on the CLI, the world opened up. One of the things I return to from time to time is when I can&rsquo;t find a GIF I want to use in a message or email. Every time, I look up how to convert a video to a GIF and every time, I&rsquo;m given a new solution. Imgur&rsquo;s <a href="https://imgur.com/vidgif">video-to-gif</a> used to be reliable, but the last few times it hasn&rsquo;t been working as expected.</p>
<p>This last time, I was trying to convert a very important It&rsquo;s Always Sunny in Philadelphia clip to a reaction gif. Can you believe there is no &ldquo;thank you&rdquo; reaction gif from when <a href="https://www.youtube.com/watch?v=ROCKGuuviis">Dennis reads Charlie&rsquo;s speech?!</a>. Here&rsquo;s the gif for your own collections.</p>
<p><img loading="lazy" src="../posts/img/thankyou.gif" alt="Charlie &amp;amp; Dennis Thank you Gif" />
</p>
<p>The command line functions I found came to get this done came mostly from <a href="https://www.funkycloudmedina.com/2022/06/convert-a-video-file-to-a-gif-using-a-macos-automator-task/">Funky Cloud Medina&rsquo;s</a> post on this. I didn&rsquo;t want to use automator, but just write out a few commands, so here is what I did (on MacOS):</p>
<p>First, install ffmpeg and gifsicle with Homebrew. <code>brew install ffmpeg gifsicle</code></p>
<p>Next, navigate to the directory where your video is. If you plan on doing this regularly, you can create some permanent directories, but I started with creating two temporary directories for the images and then the final gifs.</p>
<pre tabindex="0"><code class="language-conf" data-lang="conf">mkdir pngs/ gifs/
</code></pre><p>This will create both <code>gifs</code> and <code>pngs</code> folders in the directory you&rsquo;re currently in.</p>
<p>Next, we&rsquo;ll process the movie.</p>
<pre tabindex="0"><code>ffmpeg -i Untitled.mov -r 10 pngs/out%04d.png
</code></pre><p><code>Untitled.mov</code> is the name of the video file in the directory you&rsquo;re currently in and outputs each frame to the <code>pngs</code> folder with increment digits. The incrementing digits are so you don&rsquo;t overwrite everything and end up with just a single picture file.</p>
<p>Next, we&rsquo;ll use <code>sips</code>, the Scriptable Image Processing System. (Check out <code>man sips</code> from your own CLI for more info!)</p>
<pre tabindex="0"><code>sips -s format gif pngs/*.png --out gifs
</code></pre><p>Almost there! This is processing all the image files into the gifs folder. Let&rsquo;s now move into the gifs folder with cd: <code>cd gifs</code></p>
<p>And now we can use <a href="https://www.lcdf.org/gifsicle/">gifsicle</a> to merge all the images into a single gif! We&rsquo;ll do that with the following command:</p>
<pre tabindex="0"><code>gifsicle --optimize=3 --delay=10 --loopcount *.gif &gt; ~/Documents/thankyou.gif
</code></pre><p>And voila! You should have a auto-playing gif file ready for use in all your reactions and emails. Enjoy!</p>
]]></content:encoded>
</item>
<item>
<title>How to Revisit your Terminal Session&#39;s History</title>
<link>/posts/save_terminal_to_file.html</link>
<pubDate>Tue, 19 Mar 2024 11:00:53 -0400</pubDate>
<guid>/posts/save_terminal_to_file.html</guid>
<description>Go beyond zsh/bash_history and save both your commands and the output to a file for later review! This command will help any homelabber that struggles with documentation for their network and services.</description>
<content:encoded><![CDATA[<p>I can&rsquo;t believe I didn&rsquo;t know about this command beforehand. When I first got into self-hosting and homelabbing, one of the app ideas I had that would help me with internal documentation is a terminal program that saves your input and output to a file for you to review later and extract the key commands you used. I also had the bonus idea that you could add comments as you were writing out commands.</p>
<p>Little did I know at the time that comments from the CLI were already possible! I&rsquo;ve already begun using comments which has been helpful if I need to look back at my <code>zsh_history</code> file. Here&rsquo;s an example of a command I would use with my docker services.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker logs <span style="color:#f92672">{</span>container_name<span style="color:#f92672">}</span> --since 5m ;: The container is failing <span style="color:#ae81ff">5</span> minutes after startup.
</span></span></code></pre></div>
<style type="text/css">
.box-shortcode {
padding: 1.6em;
padding-top: 1.4em;
line-height: 1.4em;
margin-top: 1em;
margin-bottom: 2em;
border-radius: 4px;
color: #444;
background: #f3ebe850;
}
.box-title {
margin: -18px -18px 12px;
padding: 4px 18px;
border-radius: 4px 4px 0 0;
font-weight: 700;
color: #fff;
background: #6ab0de;
}
.box-shortcode.warning .box-title {
background: #ff6b6b;
}
.box-shortcode.warning {
background: #ff6b6b4f;
}
.box-shortcode.info .box-title {
background: #0089e488;
}
.box-shortcode.info {
background: #0089e41c;
box-shadow: 3px 3px 5px #0089e410;
}
.box-shortcode.important .box-title {
background: #f7ec2c;
}
.box-shortcode.important {
background: #f7ec2c7d;
}
.box-shortcode.tip .box-title {
background: #a3ffa34d;
}
.box-shortcode.tip {
background: #a3ffa34d;
box-shadow: 3px 3px 5px #0089e410;
}
.icon-box {
display: inline-flex;
align-self: center;
margin-right: 8px;
}
.icon-box img,
.icon-box svg {
height: 1em;
width: 1em;
fill: currentColor;
}
.icon-box img,
.icon-box.baseline svg {
top: 0.125em;
position: relative;
}
.box-shortcode p {
margin-bottom: 0.6em;
}
.box-shortcode p:first-of-type {
display: inline;
}
.box-shortcode p:nth-of-type(2) {
margin-top: 0.6em;
}
.box-shortcode p:last-child {
margin-bottom: 0;
}
</style>
<svg width="0" height="0" display="none" xmlns="http://www.w3.org/2000/svg">
<symbol id="tip-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"/>
</symbol>
<symbol id="important-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</symbol>
<symbol id="warning-box" viewBox="0 0 576 512" preserveAspectRatio="xMidYMid meet">
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</symbol>
<symbol id="info-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"/>
</symbol>
</svg><div class="box box-shortcode info" >
<span class="icon-box baseline">
<svg><use href="#info-box"></use></svg>
</span>
<p>You can write comments in <code>bash</code> and <code>powershell</code> as well!</p>
<ul>
<li>Powershell: <code>rem</code></li>
<li>ZSH: <code>;:</code></li>
<li>Bash: <code>#</code></li>
</ul>
</div>
<p>Turns out, I don&rsquo;t need a fancy app to log the input/output of a terminal session! It exists in most unix based systems already and the command is simply this: <code>script</code>. That&rsquo;s it!</p>
<p>Unlike <code>.zsh_history</code> or <code>.bash_history</code> which only saves the commands you input into the terminal, <code>script</code> will save both your commands <em>and</em> it&rsquo;s output. Why didn&rsquo;t I know about this before I started setting up my homelab?!</p>
<p>The next time I want to setup a new service or debug something on a server, I will now make sure I start my session with <code>script {date}-{service_name}.txt</code> and then start writing out commands. As long as I can remember to write my inline comments during the session, looking back and trying to figure out what I was thinking at the time should be a breeze! I can dump these files into my internal wiki as placeholders. Then, ideally, I&rsquo;ll remove the commands that lead me down a dead end, clean up and expand comments, and easily keep my wiki growing. Ideally being the key word here&hellip; I&rsquo;ll take a dump of history files for the time being.</p>
<p>Either way, I will literally have the output of my brain when doing CLI work in a file. Incredible.</p>
<p>Here&rsquo;s a quick overview about script, taken from <code>man script</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span> SCRIPT<span style="color:#f92672">(</span>1<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>NAME
</span></span><span style="display:flex;"><span> script make typescript of terminal session
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>SYNOPSIS
</span></span><span style="display:flex;"><span> script <span style="color:#f92672">[</span>-aeFkqr<span style="color:#f92672">]</span> <span style="color:#f92672">[</span>-t time<span style="color:#f92672">]</span> <span style="color:#f92672">[</span>file <span style="color:#f92672">[</span>command ...<span style="color:#f92672">]]</span>
</span></span><span style="display:flex;"><span> script -p <span style="color:#f92672">[</span>-deq<span style="color:#f92672">]</span> <span style="color:#f92672">[</span>-T fmt<span style="color:#f92672">]</span> <span style="color:#f92672">[</span>file<span style="color:#f92672">]</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>DESCRIPTION
</span></span><span style="display:flex;"><span> The script utility makes a typescript of everything printed on your terminal. It is useful <span style="color:#66d9ef">for</span> students who need a hardcopy record of an interactive session as proof of an assignment, as the typescript file can be
</span></span><span style="display:flex;"><span> printed out later with lpr<span style="color:#f92672">(</span>1<span style="color:#f92672">)</span>.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> If the argument file is given, script saves all dialogue in file. If no file name is given, the typescript is saved in the file typescript.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> If the argument command is given, script will run the specified command with an optional argument vector instead of an interactive shell.
</span></span></code></pre></div><style>
.box-shortcode {
color: #e8e8e8;
border: none;
}
.post-content img {
margin: auto
}
details {
margin: 0;
padding: 0;
border: none;
background: transparent;
box-shadow: none;
}
</style>
]]></content:encoded>
</item>
<item>
<title>Google Sheets to Slides with Scripts: an Automation</title>
<link>/posts/google_scripts_sheets_to_slides.html</link>
<pubDate>Sun, 25 Feb 2024 10:14:30 -0500</pubDate>
<guid>/posts/google_scripts_sheets_to_slides.html</guid>
<description>Learn about running a quick automation that turns rows in your Google Sheets and plugs it into a Google Slide template to easily share more attractive content.</description>
<content:encoded><![CDATA[<h1 id="overview">Overview</h1>
<p>Recently, my wife needed help in sharing weekly content with a group of people. The original way this group was sharing
content was a PDF export of a Google Doc. From a User Experience perspective, it wasn&rsquo;t great. Someone would received this
long PDF, they would have to scroll to find the date or topic of the next additional_notes, and overall didn&rsquo;t look great.</p>
<p>While she had developed the Google Sheets and Google Slide system to make everything a bit more legible and navigable, there
was still an element of copy and paste from the Sheet to the Slide. She had asked if I knew of any ways to make this process
easier. This was a perfect mini-project for Google Apps Scripts!</p>
<style type="text/css">
.box-shortcode {
padding: 1.6em;
padding-top: 1.4em;
line-height: 1.4em;
margin-top: 1em;
margin-bottom: 2em;
border-radius: 4px;
color: #444;
background: #f3ebe850;
}
.box-title {
margin: -18px -18px 12px;
padding: 4px 18px;
border-radius: 4px 4px 0 0;
font-weight: 700;
color: #fff;
background: #6ab0de;
}
.box-shortcode.warning .box-title {
background: #ff6b6b;
}
.box-shortcode.warning {
background: #ff6b6b4f;
}
.box-shortcode.info .box-title {
background: #0089e488;
}
.box-shortcode.info {
background: #0089e41c;
box-shadow: 3px 3px 5px #0089e410;
}
.box-shortcode.important .box-title {
background: #f7ec2c;
}
.box-shortcode.important {
background: #f7ec2c7d;
}
.box-shortcode.tip .box-title {
background: #a3ffa34d;
}
.box-shortcode.tip {
background: #a3ffa34d;
box-shadow: 3px 3px 5px #0089e410;
}
.icon-box {
display: inline-flex;
align-self: center;
margin-right: 8px;
}
.icon-box img,
.icon-box svg {
height: 1em;
width: 1em;
fill: currentColor;
}
.icon-box img,
.icon-box.baseline svg {
top: 0.125em;
position: relative;
}
.box-shortcode p {
margin-bottom: 0.6em;
}
.box-shortcode p:first-of-type {
display: inline;
}
.box-shortcode p:nth-of-type(2) {
margin-top: 0.6em;
}
.box-shortcode p:last-child {
margin-bottom: 0;
}
</style>
<svg width="0" height="0" display="none" xmlns="http://www.w3.org/2000/svg">
<symbol id="tip-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"/>
</symbol>
<symbol id="important-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</symbol>
<symbol id="warning-box" viewBox="0 0 576 512" preserveAspectRatio="xMidYMid meet">
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</symbol>
<symbol id="info-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"/>
</symbol>
</svg><div class="box box-shortcode info" >
<span class="icon-box baseline">
<svg><use href="#info-box"></use></svg>
</span>
<p>In case you haven&rsquo;t heard of Google Apps Scripts, it is a built in Google IDE that uses Javascript to make various
Google products available via automation and scripting. While Google saves the files as <code>.gs</code> files, it&rsquo;s just javascript,
don&rsquo;t worry! You can learn more about it <a href="https://www.google.com/script/start/">here</a> and <a href="https://developers.google.com/apps-script/overview">here</a>.</p>
</div>
<p>Below, I&rsquo;ll share the pieces of the script to explain what is going on. If you know what you&rsquo;re doing and just want the
script, feel free to head to the bottom of the page to see the <a href="#full-script">full script</a>.</p>
<h1 id="goal">Goal</h1>
<p>The goal here was pretty straight forward. I wanted to add a button on the Google Sheet that allows the user to create a
Slides presentation from a subset of data within the sheet. The data will be date based, as this is what most end users need,
and the user will be able to pick from the currently available dates &amp; populated data.</p>
<h1 id="setup">Setup</h1>
<p>Getting setup, you need both a Sheet with a few headings and an empty Slide presentation with a few empty text boxes. The
empty text boxes will be key to helping us connect the data in the sheet to the correct placement in the slide. Here are two
screenshots of what this looks like for my example:</p>
<p><img loading="lazy" src="../posts/img/google_sheets_for_scripts_example.png" alt="Google Sheets Example" />
</p>
<p><img loading="lazy" src="../posts/img/google_slides_for_scripts_example.png" alt="Google Slides Example" />
<img src="./img/google_slides_for_scripts_example.png" alt="Example image"></p>
<h2 id="sheets-setup">Sheets Setup</h2>
<p>Ignoring any design from my screenshots - all credit goes to my much more creative wife - the setup for the sheet is fairly
simple. You need various headings in Row A of the sheet which we will be using to reference data. In this tutorial, our
headings are <code>Date, Topic 1, Topic 2, Topic 3, Additional Notes</code>. Whether you start on Row 1 or after that doesn&rsquo;t matter too
much.</p>
<p>For the date column, we&rsquo;ll be formatting our date like this: &ldquo;February 26, 2024&rdquo;. You&rsquo;ll see why in a little bit.</p>
<p>On top of the actual data in the Sheet, the Apps Script is going to live in this document and just push data to the Slide. To
access your App Scripts, click Extensions &gt; Apps Scripts. A new tab will open with a blank IDE style interface and an empty
<code>myFunction</code>.</p>
<h2 id="slides-setup">Slides Setup</h2>
<p>Don&rsquo;t worry too much about the design for Slides, you can change that later. But the important step is creating the empty
text boxes. After you create your text boxes (4 will be used in this tutorial), right click one of them and select &ldquo;Format
Options&rdquo;. A panel on the left hand-side should slide out. Click the &ldquo;Alt Text&rdquo; drop down, and then &ldquo;Advanced Options&rdquo;. That
little text box is the title for your text box; it is not used in the visual representation of the box, we will just be using
it as a reference point.</p>
<p>For ease of this tutorial, make the Title of the text box the same as the Header row from when we set up the Sheet, above.
Once you&rsquo;ve added title to each of the text boxes, let&rsquo;s head into the code.</p>
<h2 id="onopen-function">onOpen Function</h2>
<p>The first function you need for creating a UI change in the Google Sheet is an <code>onOpen</code> function that will setup the UI when
the Sheet is open.</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">onOpen</span>(<span style="color:#a6e22e">e</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">SpreadsheetApp</span>.<span style="color:#a6e22e">getUi</span>()
</span></span><span style="display:flex;"><span> .<span style="color:#a6e22e">createMenu</span>(<span style="color:#e6db74">&#39;Create Presentation&#39;</span>)
</span></span><span style="display:flex;"><span> .<span style="color:#a6e22e">addItem</span>(<span style="color:#e6db74">&#39;Create Presentation&#39;</span>, <span style="color:#e6db74">&#39;askDate&#39;</span> )
</span></span><span style="display:flex;"><span> .<span style="color:#a6e22e">addToUi</span>();
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>In this function (which we&rsquo;ll later trigger in the script settings), when someone opens up the Sheet, it will create the UI
button in the top level menu. Here&rsquo;s what&rsquo;s happening:</p>
<ul>
<li><code>SpreadsheetApp.getUi()</code> &ndash;&gt; Instantiate class and get available methods for <code>getUi()</code>.</li>
<li><code>createMenu()</code> &ndash;&gt; Creates a menu item called &ldquo;Create Presentation&rdquo;.</li>
<li><code>addItem()</code> &ndash;&gt; Adds an item to that menu that when pressed, calls the <code>askDate</code> function.</li>
<li><code>addtoUi()</code> &ndash;&gt; Add it! Now people can see and click on it.</li>
</ul>
<p><img loading="lazy" src="../posts/img/google_sheets_menu_example_2.png" alt="Google Sheets Menu Example" />
</p>
<h2 id="askdate-function">askDate Function</h2>
<p>This is the main function and a bit long, so I&rsquo;ll split it up into a few sections.</p>
<h3 id="section-1">Section 1</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">7
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">ui</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">SpreadsheetApp</span>.<span style="color:#a6e22e">getUi</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">sheet</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">SpreadsheetApp</span>.<span style="color:#a6e22e">getActiveSheet</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">lastRow</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">sheet</span>.<span style="color:#a6e22e">getLastRow</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">dates</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">sheet</span>.<span style="color:#a6e22e">getRange</span>(<span style="color:#ae81ff">2</span>,<span style="color:#ae81ff">2</span>,<span style="color:#a6e22e">lastRow</span>,<span style="color:#ae81ff">1</span>).<span style="color:#a6e22e">getDisplayValues</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// get today&#39;s epoch time for easier calculations
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">epochDate</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Date().<span style="color:#a6e22e">getTime</span>()
</span></span></code></pre></td></tr></table>
</div>
</div><p>This first section we&rsquo;re just getting the sheet ready for analysis. With the <code>dates</code> variable, we&rsquo;re just looking at the
second column. The reason we&rsquo;re using the <code>getDisplayValues()</code> method is because Google will automatically convert the dates
to include time zone, time, etc. We want to keep the date in the same format for a better user experience.</p>
<p>For that last line, we&rsquo;re getting the current date &amp; time in epoch time so we can run a comparison further down the script.
For this use case, we don&rsquo;t need to include any dates in the past.</p>
<h3 id="section-2">Section 2</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span> <span style="color:#a6e22e">menuOptions</span> <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">re</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> RegExp(<span style="color:#e6db74">&#34;^[A-Za-z]{3,15}\\s\\d{1,2},\\s\\d{2,4}&#34;</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> <span style="color:#f92672">&lt;</span> <span style="color:#a6e22e">dates</span>.<span style="color:#a6e22e">length</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> ) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">date</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">dates</span>[<span style="color:#a6e22e">i</span>][<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">epochSheetDate</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Date(<span style="color:#a6e22e">date</span>).<span style="color:#a6e22e">getTime</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">date</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;&#34;</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">dateMatch</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">re</span>.<span style="color:#a6e22e">test</span>(<span style="color:#a6e22e">date</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">dateMatch</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">true</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">epochDate</span> <span style="color:#f92672">&lt;</span> <span style="color:#a6e22e">epochSheetDate</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">menuOptions</span>.<span style="color:#a6e22e">push</span>(<span style="color:#a6e22e">date</span>)
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span></code></pre></td></tr></table>
</div>
</div><p>In this section we&rsquo;re creating an empty array and instantiating a RegExp to ensure we have an actual date in the cell. See
<a href="https://regex101.com">Regex101</a> to learn more about Regex and test different regex syntax.</p>
<div class="box box-shortcode tip" >
<span class="icon-box baseline">
<svg><use href="#tip-box"></use></svg>
</span>
<p>Something that threw me off when first writing this regex function was the way the Google IDE manages escape character and
slashes. If you take the second line above and input it into <a href="https://regex101.com">Regex101</a> you&rsquo;ll see the <code>\s</code> or <code>\d</code>
become dark gray, basically skipping over that token. However, for Google, you&rsquo;ll need an additional backslash to escape and
make the token become used by the function.</p>
<p>Here&rsquo;s the &ldquo;correct&rdquo; RegExp string for Regex101.com: <code>^[A-Za-z]{3,15}\s\d{1,2},\s\d{2,4}</code></p>
</div>
<p>Next, we dive into a for loop, looping through the <code>dates</code> column of values (which we just called earlier). After attributing
each value to the <code>var date</code> variable, we also convert that same value into epoch time (by creating a new <code>Date().getTime()</code>)
so that we can compare it with today&rsquo;s date.</p>
<p>After the variables are setup we need to check that the date isn&rsquo;t empty; we don&rsquo;t need any rows where a date hasn&rsquo;t been assigned to it
yet. If we have a non-empty date value, let&rsquo;s compare it using the regex string. All we&rsquo;re doing here is asking &ldquo;Is this date
in the format I&rsquo;m expecting it?&rdquo; If true, let&rsquo;s keep the value and continue using it. If not, just ignore it.</p>
<p>So we&rsquo;ve now found a value that&rsquo;s in the date format we expect, let&rsquo;s now take that same value in epoch time (referenced by
the <code>epochSheetDate</code> variable) and compare it to today&rsquo;s epoch time date. If today&rsquo;s date is less than the value in the
sheet, that means the date in the sheet is in the future.</p>
<p>So now we have a date in the correct format and that is at some future date from today. Fantastic! Once we&rsquo;ve gone through
those checks, we&rsquo;re ready to add the date to the array we created at the top of this section. Push on!</p>
<h3 id="section-3">Section 3</h3>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">menuOptions</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">stringList</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">menuOptions</span>.<span style="color:#a6e22e">toString</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">formattedDates</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">stringList</span>.<span style="color:#a6e22e">replaceAll</span>(<span style="color:#e6db74">&#34;2024,&#34;</span>, <span style="color:#e6db74">&#34;2024 \n &#34;</span>);
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">prompt</span>(<span style="color:#e6db74">&#39;Which date would you like to create a presentation for? Please copy and paste the date exactly as you see it in the options below. Options: \n \n&#39;</span><span style="color:#f92672">+</span><span style="color:#a6e22e">formattedDates</span>);
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">getSelectedButton</span>() <span style="color:#f92672">==</span> <span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">Button</span>.<span style="color:#a6e22e">OK</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">dateChosen</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">getResponseText</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">dateChosen</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;&#34;</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">alert</span>(<span style="color:#e6db74">&#34;Woohoo! Let&#39;s make a presentation!&#34;</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span>(<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">y</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">y</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">dates</span>.<span style="color:#a6e22e">length</span>;<span style="color:#a6e22e">y</span><span style="color:#f92672">++</span>){
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">dates</span>[<span style="color:#a6e22e">y</span>][<span style="color:#ae81ff">0</span>] <span style="color:#f92672">==</span> <span style="color:#a6e22e">dateChosen</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Logger.log(&#34;Row:&#34;+(y+2));
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">presRow</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">y</span><span style="color:#f92672">+</span><span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">presData</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">sheet</span>.<span style="color:#a6e22e">getRange</span>(<span style="color:#a6e22e">presRow</span>,<span style="color:#ae81ff">3</span>,<span style="color:#ae81ff">1</span>,<span style="color:#ae81ff">4</span>).<span style="color:#a6e22e">getValues</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> (<span style="color:#a6e22e">presCopy</span> <span style="color:#66d9ef">of</span> <span style="color:#a6e22e">presData</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">topic1</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">presCopy</span>[<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">topic2</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">presCopy</span>[<span style="color:#ae81ff">1</span>];
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">topic3</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">presCopy</span>[<span style="color:#ae81ff">2</span>];
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">additional_notes</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">presCopy</span>[<span style="color:#ae81ff">3</span>];
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">createPresentation</span>(<span style="color:#a6e22e">dateChosen</span>, <span style="color:#a6e22e">topic1</span>, <span style="color:#a6e22e">topic2</span>, <span style="color:#a6e22e">topic3</span>, <span style="color:#a6e22e">additional_notes</span>)
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">alert</span>(<span style="color:#e6db74">&#34;Ouch. Looks like you entered something incorrectly (i.e. you entered nothing). Try again.&#34;</span>)
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">alert</span>(<span style="color:#e6db74">&#34;No Dates Available. Check Spreadsheet&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>This next section might feel long, but it&rsquo;s really easy to follow, don&rsquo;t worry. It&rsquo;s a lot of data organization to ensure
we&rsquo;re giving the users a smooth experience &amp; to make sure when the data hits the Slide, it&rsquo;s in the right spot.</p>
<p>First things first - let&rsquo;s only run this if we actually have items in the <code>menuOptions</code> array! No need to give the user an
affirmative message when there&rsquo;s no data to process. If there are no dates available, we call the else of this if, which is
the 3rd to last line in this section.</p>
<div class="box box-shortcode tip" >
<span class="icon-box baseline">
<svg><use href="#tip-box"></use></svg>
</span>
<p>A <code>ui.alert</code> is just a modal that offers no interaction to the user.</p>
</div>
<p>The next few lines are for processing that array of dates we grabbed from the sheet to making them presentable to the user.
Without the <code>stringList</code> and <code>formattedDates</code> variables, Google&rsquo;s modal just shows a wall of text of dates which makes it
difficult to parse for the user. By using <code>replaceAll()</code> we remove the comma and insert a new line after every portion of the
string that contains 2024 and a comma.</p>
<p>So now, instead of the modal showing:</p>
<pre tabindex="0"><code>February 2, 2024,February 3, 2024,February 4,2024... etc
</code></pre><p>We now see the much easier to read:</p>
<pre tabindex="0"><code>February 2, 2024
February 3, 2024
February 4, 2024
</code></pre><p>After that&rsquo;s formatted nicely, we can now have the prompt show for the user which asks them what date they would like to
process into a Slide presentation. Since we haven&rsquo;t visited any ui options since the top of this tutorial, this is the modal
that will show up after a user clicks &ldquo;Create Presentation&rdquo; in the top level menu in Google Sheets.</p>
<p>Ok, so now moving onto line 5, we need to get the text for what the user enters into the modal box. If they don&rsquo;t enter
anything, we should let them know that nothing will be done. That message comes from the second to last <code>else</code> statement in
this section. Instead, if they enter some data let&rsquo;s parse it!</p>
<div class="box box-shortcode warning" >
<span class="icon-box baseline">
<svg><use href="#warning-box"></use></svg>
</span>
<p>If you want to test yourself by adding in some new code try this exercise:</p>
<p><em>How can we ensure that the user entered a date in the format we expect? Tip: It is something reusable from another portion
of the script.</em></p>
</div>
<p>For lines 9 and 10, we&rsquo;re now looping back through the same set of rows that we did in section 2. The reason we&rsquo;re
re-entering this for loop is because we&rsquo;ve already exited the loop. So we have to get the values again. Once we have them,
let&rsquo;s match the date that the user entered to the correct row. We&rsquo;re able to do that by grabbing the index and adding 2 to
it.</p>
<div class="box box-shortcode info" >
<span class="icon-box baseline">
<svg><use href="#info-box"></use></svg>
</span>
<p>I haven&rsquo;t fully looked into <em>why</em> we need to add 2 to get the correct row number. As we loop through the dates, the array
length (<code>dates.length</code>) is all the available rows in the sheet. So you would think that <code>y</code> is the row that the value is on;
but it&rsquo;s not.</p>
<p>My only guess so far has to do with the frozen row at the top that I have in my Sheet. So the frozen row is not used, and row
2 is now index 0. Which means index 41 is technically row 43, and why we have to add 2 to the value to get the <em>visually
correct</em> row number. We need this number to be correct so that we can get the rest of the values int he sheet.</p>
</div>
<p>Now that we have the correct row number, we&rsquo;re going to shift from getting values from each row and getting them from each
column. By calling <code>var presData = sheet.getRange(presRow,3,1,4).getValues();</code> we are grabbing an array. <code>getRange</code> is
looking for the following values, in the following order:</p>
<ul>
<li>Row number (which we got from the <code>y+2</code>)</li>
<li>Column Number (which is Column C in this case)</li>
<li>Number of Rows (we only need this row)</li>
<li>Number of Columns (we know how many more columns we need).</li>
</ul>
<p>Next up, we&rsquo;re jumping into another for loop, now for all the data we just pulled from the column. This part is just parsing
each column&rsquo;s data into it&rsquo;s own variable for easier management when we create the presentation. We&rsquo;ve also grabbed the
additional notes column, but we&rsquo;re not using it right now. This column&rsquo;s data may be valuable by adding it to the speaker
notes of the Slide Presentation. Check out these two resources:</p>
<ul>
<li><a href="https://developers.google.com/slides/api/guides/notes">Working with Speaker Notes</a></li>
<li><a href="https://developers.google.com/slides/api/reference/rest/v1/presentations.pages#Page.NotesProperties">Notes Properties API</a></li>
</ul>
<p>Once that&rsquo;s done, call the next function: <code>createPresentation()</code>! We&rsquo;re almost done.</p>
<h2 id="createpresentation-function">createPresentation Function</h2>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">createPresentation</span>(<span style="color:#a6e22e">dateChosen</span>, <span style="color:#a6e22e">topic1</span>, <span style="color:#a6e22e">topic2</span>, <span style="color:#a6e22e">topic3</span>, <span style="color:#a6e22e">additionalNotes</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">slides</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">SlidesApp</span>.<span style="color:#a6e22e">openById</span>(<span style="color:#e6db74">&#39;ENTER_ID_OF_SLIDE_PRESENTATION&#39;</span>);
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">slide</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">slides</span>.<span style="color:#a6e22e">getSlides</span>()[<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">elements</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">slide</span>.<span style="color:#a6e22e">getPageElements</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> <span style="color:#f92672">&lt;</span> <span style="color:#a6e22e">elements</span>.<span style="color:#a6e22e">length</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">titles</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">elements</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">getTitle</span>()
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">titles</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;Date&#34;</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">dateText</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">elements</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">asShape</span>().<span style="color:#a6e22e">getText</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">dateText</span>.<span style="color:#a6e22e">clear</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">dateText</span>.<span style="color:#a6e22e">setText</span>(<span style="color:#a6e22e">dateChosen</span>);
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">titles</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;Topic 1&#34;</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">topic1Text</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">elements</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">asShape</span>().<span style="color:#a6e22e">getText</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">topic1Text</span>.<span style="color:#a6e22e">clear</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">topic1Text</span>.<span style="color:#a6e22e">setText</span>(<span style="color:#a6e22e">topic1</span>);
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">titles</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;Topic 2&#34;</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">topic2Text</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">elements</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">asShape</span>().<span style="color:#a6e22e">getText</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">topic2Text</span>.<span style="color:#a6e22e">clear</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">topic2Text</span>.<span style="color:#a6e22e">setText</span>(<span style="color:#a6e22e">topic2</span>);
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">titles</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;Topic 3&#34;</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">topic3Text</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">elements</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">asShape</span>().<span style="color:#a6e22e">getText</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">topic3Text</span>.<span style="color:#a6e22e">clear</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">topic3Text</span>.<span style="color:#a6e22e">setText</span>(<span style="color:#a6e22e">topic3</span>);
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>This function may be the shortest to explain. All we are doing is taking those variables from the previous function (the
variables that contain each topic&rsquo;s text value) and doing the following:</p>
<ol>
<li>Opening the Presentation (line 2)</li>
<li>Getting the first slide (line 3)</li>
<li>Grabbing all the <a href="https://developers.google.com/apps-script/reference/slides/slides/page-element">page elements</a> (line 4)</li>
<li>Looping through the page elements &amp; grabbing all the titles that we set during <a href="#slides-setup">setup</a> (line
5 &amp; 6)</li>
<li>Then we compare the title of the Slide text box to the expected string (Topic 1-3), and if we get a match, retrieve the text
of the box as a variable, clear the text, and input our new text with the variables we created in the previous function.</li>
</ol>
<p>And just like that, you should have all your boxes filled in with the data you input to the Google Sheet!</p>
<div class="box box-shortcode tip" >
<span class="icon-box baseline">
<svg><use href="#tip-box"></use></svg>
</span>
<p><strong>Bonus exercises:</strong></p>
<ul>
<li>Instead of overwriting this presentation each time, how can you add a new slide with the same text boxes? How would you
find that slide since we&rsquo;re right now grabbing the first slide?</li>
<li>How can you change the name of this Slide Presentation to the date from the Sheet?</li>
<li>Could you automatically export this Slide as a PDF and save it to a user&rsquo;s Drive?</li>
</ul>
</div>
<style>
details summary {
min-width: 200px;
font-weight: 600;
cursor: pointer;
}
details summary > * {
display: inline;
}
details {
margin: 1em;
border-radius: 5px;
padding: 1em;
overflow: hidden;
box-shadow: 0 .1rem 1rem -.5rem rgba(0,0,0,.4);
}
summary {
padding: 0 1rem .3em 1.2rem;
display: block;
position: relative;
cursor: pointer;
}
summary:before {
content: '>';
position: absolute;
top: .1rem;
left: 0.1rem;
transform: rotate(0);
transition: .3s transform ease;
}
details[open] > summary:before {
transform: rotate(180deg);
content: '-';
}
details summary::-webkit-details-marker {
display:none;
}
</style><details><summary><h2 id="full-script">Full Script:</h2>
</summary>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">45
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">46
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">47
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">48
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">49
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">50
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">51
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">52
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">53
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">54
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">55
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">56
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">57
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">58
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">59
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">60
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">61
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">62
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">63
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">64
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">65
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">66
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">67
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">68
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">69
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">70
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">71
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">72
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">73
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">74
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">75
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">76
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">77
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">78
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">79
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">80
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">81
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">82
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">83
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">84
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">85
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">86
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">87
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-js" data-lang="js"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">onOpen</span>(<span style="color:#a6e22e">e</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">SpreadsheetApp</span>.<span style="color:#a6e22e">getUi</span>()
</span></span><span style="display:flex;"><span> .<span style="color:#a6e22e">createMenu</span>(<span style="color:#e6db74">&#39;Create Presentation&#39;</span>)
</span></span><span style="display:flex;"><span> .<span style="color:#a6e22e">addItem</span>(<span style="color:#e6db74">&#39;Create Presentation&#39;</span>, <span style="color:#e6db74">&#39;askDate&#39;</span> )
</span></span><span style="display:flex;"><span> .<span style="color:#a6e22e">addToUi</span>();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">askDate</span>() {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">ui</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">SpreadsheetApp</span>.<span style="color:#a6e22e">getUi</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">sheet</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">SpreadsheetApp</span>.<span style="color:#a6e22e">getActiveSheet</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">lastRow</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">sheet</span>.<span style="color:#a6e22e">getLastRow</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">dates</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">sheet</span>.<span style="color:#a6e22e">getRange</span>(<span style="color:#ae81ff">2</span>,<span style="color:#ae81ff">2</span>,<span style="color:#a6e22e">lastRow</span>,<span style="color:#ae81ff">1</span>).<span style="color:#a6e22e">getDisplayValues</span>();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// get today&#39;s epoch time for easier calculations
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">epochDate</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Date().<span style="color:#a6e22e">getTime</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">menuOptions</span> <span style="color:#f92672">=</span> []
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">re</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> RegExp(<span style="color:#e6db74">&#34;^[A-Za-z]{3,15}\\s\\d{1,2},\\s\\d{2,4}&#34;</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> <span style="color:#f92672">&lt;</span> <span style="color:#a6e22e">dates</span>.<span style="color:#a6e22e">length</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> ) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">date</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">dates</span>[<span style="color:#a6e22e">i</span>][<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">epochSheetDate</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> Date(<span style="color:#a6e22e">date</span>).<span style="color:#a6e22e">getTime</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">date</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;&#34;</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">dateMatch</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">re</span>.<span style="color:#a6e22e">test</span>(<span style="color:#a6e22e">date</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">dateMatch</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">true</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">epochDate</span> <span style="color:#f92672">&lt;</span> <span style="color:#a6e22e">epochSheetDate</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">menuOptions</span>.<span style="color:#a6e22e">push</span>(<span style="color:#a6e22e">date</span>)
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">menuOptions</span>.<span style="color:#a6e22e">length</span> <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">0</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">stringList</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">menuOptions</span>.<span style="color:#a6e22e">toString</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">stringlist</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">stringList</span>.<span style="color:#a6e22e">replaceAll</span>(<span style="color:#e6db74">&#34;2024,&#34;</span>, <span style="color:#e6db74">&#34;2024 \n &#34;</span>);
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">response</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">prompt</span>(<span style="color:#e6db74">&#39;Which date would you like to create a presentation for? Please copy and paste the date exactly as you see it in the options below. Options: \n \n&#39;</span><span style="color:#f92672">+</span><span style="color:#a6e22e">stringlist</span>);
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">getSelectedButton</span>() <span style="color:#f92672">==</span> <span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">Button</span>.<span style="color:#a6e22e">OK</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">dateChosen</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">response</span>.<span style="color:#a6e22e">getResponseText</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">dateChosen</span> <span style="color:#f92672">!=</span> <span style="color:#e6db74">&#34;&#34;</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">alert</span>(<span style="color:#e6db74">&#34;Woohoo! Let&#39;s make a presentation!&#34;</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span>(<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">y</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">y</span><span style="color:#f92672">&lt;</span><span style="color:#a6e22e">dates</span>.<span style="color:#a6e22e">length</span>;<span style="color:#a6e22e">y</span><span style="color:#f92672">++</span>){
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">dates</span>[<span style="color:#a6e22e">y</span>][<span style="color:#ae81ff">0</span>] <span style="color:#f92672">==</span> <span style="color:#a6e22e">dateChosen</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Logger.log(&#34;Row:&#34;+(y+2));
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">presRow</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">y</span><span style="color:#f92672">+</span><span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">presData</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">sheet</span>.<span style="color:#a6e22e">getRange</span>(<span style="color:#a6e22e">presRow</span>,<span style="color:#ae81ff">3</span>,<span style="color:#ae81ff">1</span>,<span style="color:#ae81ff">4</span>).<span style="color:#a6e22e">getValues</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> (<span style="color:#a6e22e">presCopy</span> <span style="color:#66d9ef">of</span> <span style="color:#a6e22e">presData</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">topic1</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">presCopy</span>[<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">topic2</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">presCopy</span>[<span style="color:#ae81ff">1</span>];
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">topic3</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">presCopy</span>[<span style="color:#ae81ff">2</span>];
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">additional_notes</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">presCopy</span>[<span style="color:#ae81ff">3</span>];
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">createPresentation</span>(<span style="color:#a6e22e">dateChosen</span>, <span style="color:#a6e22e">topic1</span>, <span style="color:#a6e22e">topic2</span>, <span style="color:#a6e22e">topic3</span>, <span style="color:#a6e22e">additionalNotes</span>)
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">alert</span>(<span style="color:#e6db74">&#34;Ouch. Looks like you entered something incorrectly (i.e. you entered nothing). Try again.&#34;</span>)
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>} <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">ui</span>.<span style="color:#a6e22e">alert</span>(<span style="color:#e6db74">&#34;No Dates Available. Check Spreadsheet&#34;</span>)
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">createPresentation</span>(<span style="color:#a6e22e">dateChosen</span>, <span style="color:#a6e22e">topic1</span>, <span style="color:#a6e22e">topic2</span>, <span style="color:#a6e22e">topic3</span>, <span style="color:#a6e22e">additionalNotes</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">slides</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">SlidesApp</span>.<span style="color:#a6e22e">openById</span>(<span style="color:#e6db74">&#39;ENTER_ID_OF_SLIDE_PRESENTATION&#39;</span>);
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">slide</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">slides</span>.<span style="color:#a6e22e">getSlides</span>()[<span style="color:#ae81ff">0</span>];
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">elements</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">slide</span>.<span style="color:#a6e22e">getPageElements</span>();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> (<span style="color:#66d9ef">let</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> <span style="color:#f92672">&lt;</span> <span style="color:#a6e22e">elements</span>.<span style="color:#a6e22e">length</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">titles</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">elements</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">getTitle</span>()
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">titles</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;Date&#34;</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">dateText</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">elements</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">asShape</span>().<span style="color:#a6e22e">getText</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">dateText</span>.<span style="color:#a6e22e">clear</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">dateText</span>.<span style="color:#a6e22e">setText</span>(<span style="color:#a6e22e">dateChosen</span>);
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">titles</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;topic1&#34;</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">topic1Text</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">elements</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">asShape</span>().<span style="color:#a6e22e">getText</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">topic1Text</span>.<span style="color:#a6e22e">clear</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">topic1Text</span>.<span style="color:#a6e22e">setText</span>(<span style="color:#a6e22e">topic1</span>);
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">titles</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;topic2&#34;</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">topic2Text</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">elements</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">asShape</span>().<span style="color:#a6e22e">getText</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">topic2Text</span>.<span style="color:#a6e22e">clear</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">topic2Text</span>.<span style="color:#a6e22e">setText</span>(<span style="color:#a6e22e">topic2</span>);
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">titles</span> <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;topic3&#34;</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">topic3Text</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">elements</span>[<span style="color:#a6e22e">i</span>].<span style="color:#a6e22e">asShape</span>().<span style="color:#a6e22e">getText</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">topic3Text</span>.<span style="color:#a6e22e">clear</span>();
</span></span><span style="display:flex;"><span> <span style="color:#a6e22e">topic3Text</span>.<span style="color:#a6e22e">setText</span>(<span style="color:#a6e22e">topic3</span>);
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div>
</details>
<style>
.box-shortcode {
color: #e8e8e8;
border: none;
}
.post-content img {
margin: auto
}
details {
margin: 0;
padding: 0;
border: none;
background: transparent;
box-shadow: none;
}
</style>
]]></content:encoded>
</item>
<item>
<title>QuickHits: OpenResty and Package Updates</title>
<link>/posts/openresty_and_package_update_issues.html</link>
<pubDate>Mon, 04 Dec 2023 10:32:58 -0500</pubDate>
<guid>/posts/openresty_and_package_update_issues.html</guid>
<description>Every time my OpenResty package tries to update, I can&amp;#39;t restart the service with weird errors that don&amp;#39;t point exactly to the problem. Here&amp;#39;s how I&amp;#39;ve learned to fix it.</description>
<content:encoded><![CDATA[<p>Turns out I wrote this back in November, but on a different machine and I never pushed my changes up to git. So here it is
now!</p>
<p>The last two times I&rsquo;ve run <code>apt update &amp;&amp; apt upgrade -y</code> on my web server and <a href="https://openresty.org/">OpenResty</a> has an update to push out, I will
get some errors including Openresty is not setup correctly. There are a few other errors in the <code>systemctl status openresty</code>
output such as <code>process 223478 [nginx] is still running</code>. I might be slightly paraphrasing the errors, but that&rsquo;s
roughly what I&rsquo;m finding.</p>
<p>Like any debug session, I make sure nginx is disabling and not running (<code>systemctl disable</code> and <code>systemctl stop</code>), which I
can confirm. Now, Openresty does use Nginx under the hood, so that errors makes me think it&rsquo;s just conflicting services
trying to run on top of each other.</p>
<p>The weirder part is when it warns me that OpenResty is not setup correctly. I didn&rsquo;t change anything&hellip; so what is the
update/upgrade trying to setup?</p>
<p>Next, I&rsquo;ll look through my config files (i.e. <code>nginx.conf</code>) - no changes there either and nothing that stands out as out of
the ordinary.</p>
<p>One of my bad habits is that when doing this sort of debugging and running something like <code>journalctl --since 21:45:00</code>, I&rsquo;ll
look at the logs more closely with each try and fail. I should just look more closely from the beginning! But I digress.</p>
<p>Since the nginx process and openresty setup errors are the most plentiful but yield nothing, I&rsquo;ll look back through the logs
for the single lines that I miss on my first few passes. That&rsquo;s when I see it, buried between the other errors. A single line
that says my pid file can&rsquo;t be found.</p>
<p>My pid file for OpenResty and Nginx is stored in my <code>/run</code> directory, but for whatever reason, whenever OpenResty pushes out
an update, it overwrites my systemd file and starts looking for the pid file under <code>/usr/local/openresty/nginx/logs/nginx.pid</code>.
Why the file would be located under a logs directory is still beyond me, but a quick update to the systemd file, a
<code>systemctl daemon-reload</code> and then <code>systemctl start openresty</code> and all my public facing services are back in action.</p>
<p>After I fixed it for the second time, I realized I hadn&rsquo;t written this down in my documentation. So I&rsquo;ve now recorded it and
thought I&rsquo;d share it here in case it helps anyone else. I also did a bit of research after and found that even in this
<a href="https://www.digitalocean.com/community/tutorials/how-to-use-the-openresty-web-framework-for-nginx-on-ubuntu-16-04">Digital Ocean post</a>,
it has OpenResty/Nginx&rsquo;s PID file in a different location too. I couldn&rsquo;t find OpenResty&rsquo;s documentation on this yet,
but <a href="https://nginx.org/en/docs/control.html">Nginx&rsquo;s Official Docs</a> have the PID file in the
same location as I am storing it. Perhaps this is just a mis-config between nginx coming natively on my linux distro and then
OpenResty being installed on top of it?</p>
<p>Either way - problem solved, and documentation captured. That&rsquo;s a win for my day.</p>
]]></content:encoded>
</item>
<item>
<title>Neovim Subtitute Magic</title>
<link>/posts/find_and_replace_in_neovim.html</link>
<pubDate>Wed, 15 Nov 2023 08:08:49 -0500</pubDate>
<guid>/posts/find_and_replace_in_neovim.html</guid>
<description>I was able to speed up some of my workflows by learning how to search and replace specifics in Neovim!</description>
<content:encoded><![CDATA[<p>This week, I&rsquo;ve had to make some changes to an automation we had setup for a customer in <a href="https://workato.com">Workato</a>. The
original recipe was made by a co-worker with a bunch of javascript nodes. Even though I&rsquo;m customer facing and generally not
considered a technical employee, I knew the engineer who worked on this was swamped, so I decided to jump in.</p>
<p>In order to greatly reduce the number of nodes needed in the single recipe, I created a list of dictionaries in python (with
one of the values being another list!) and a quick for loop to grab all the appropriate elements from the various
dictionaries. I had thankfully already copied all the JS nodes to a single file for easy reference and retrieval. That one
file had over 8,000 lines, and I needed to replace all the javascript elements with python.</p>
<p>Here&rsquo;s one node of Javascript that I was working with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#75715e">// @param input fields supplied in the recipe step, as an object
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// @return object matching the output schema
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">// Eg: Code for returning time zone for an IP address
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">exports</span>.<span style="color:#a6e22e">main</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">async</span> ({ <span style="color:#a6e22e">user_email</span> }) =&gt; {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">all_domains</span> <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domain_one&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domain_two&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domain_three&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domain_four&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domain_five&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domain_six&#39;</span>,
</span></span><span style="display:flex;"><span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">user_domain</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">user_email</span>.<span style="color:#a6e22e">slice</span>(<span style="color:#a6e22e">user_email</span>.<span style="color:#a6e22e">indexOf</span>(<span style="color:#e6db74">&#39;@&#39;</span>));
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">let</span> <span style="color:#a6e22e">domain_found</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">all_domains</span>.<span style="color:#a6e22e">indexOf</span>(<span style="color:#a6e22e">user_domain</span>) <span style="color:#f92672">&gt;</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> { <span style="color:#a6e22e">domain_found</span> };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Most nodes were very similar, all except that their <code>all_domains</code> array had anywhere from 1 to 500+ elements. Thanks to the
neovim plugin <a href="https://github.com/jonarrien/telescope-cmdline.nvim">Telescope Cmdline</a> by <a href="https://github.com/jonarrien">Jon Arrien</a>,
you can very easily see your past history of Neovim commands. I&rsquo;ve gone back through and pulled out a few special ones that
helped and that I was very surprised how well they worked. I don&rsquo;t claim that this was the fastest or best way to clean up a
large file with repeating functions, but I definitely learned a bunch about searching-and-replacing in Neovim, and any future
needs for this will be <em>way</em> faster! One thing that has been really helpful with Neovim is that as you run the first portion
of all the commands below, Neovim should highlight what it finds. This is really helpful for real-time debugging of your
regex.</p>
<h2 id="commands-and-quick-explanations">Commands and Quick Explanations</h2>
<hr>
<p><strong>Command:</strong> <code>:g/\//+2/d</code></p>
<p><strong>Explanation:</strong> The g in the command stands for global (hint, run <code>:h :g</code> in vim!) and will search across the entire buffer
you have open. In this command I&rsquo;m looking for all Javascript single-line comments. Since the forward slash is a special
character for neovim, you need to escape it with a backwards slash, hence the odd looking <code>\/</code>. The second forward slash is
the next parameter neovim is looking for, and by adding +{num}, you can search for the pattern plus a number of lines.
Closing out the command (again, next parameter comes after the forward slash) with <code>d</code> for delete will then delete everything
with you searched for, plus the additional number of lines.</p>
<p>Bonus: if you wanted to make sure that you only pulled lines that <em>began</em> with a comment, you&rsquo;d add a carrot to the beginning
of the command. <code>:g/^\//+2/d</code></p>
<hr>
<p><strong>Command:</strong> <code>:%s/let all_domains = /&quot;domains&quot; : /g</code></p>
<p><strong>Explanation:</strong> Moving on from <code>g</code> to <code>s</code>, the <code>s</code> here stands for substitute. There&rsquo;s one big difference in using <code>g</code> and
<code>s</code>, though. While <code>g</code> will search globally, a singular <code>s</code> will only search on your current line. To search the entire
file/buffer, you need to include the percent character before the <code>s</code>. The rest of this is fairly straight forward, I am
searching for the declaration of a javascript variable and then substituting it with a python dictionary key. Since both
javascript and python declare arrays with square brackets, I wanted to make sure I kept that after the substitution. Lastly,
unless you only want the subsitute to change the first occurrence of what it finds, you&rsquo;ll need to close out the command with
the <code>g</code> flag. Neovim&rsquo;s docs say:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span>[g] Replace all occurrences in the line. Without this argument,
</span></span><span style="display:flex;"><span> replacement occurs only for the first occurrence in each line. If the
</span></span><span style="display:flex;"><span> &#39;gdefault&#39; option is on, this flag is on by default and the [g]
</span></span><span style="display:flex;"><span> argument switches it off.
</span></span></code></pre></div><hr>
<p><strong>Command:</strong> <code>:%s/^exports.main.*/{ &quot;uuid&quot; :/g</code></p>
<p><strong>Explanation:</strong> Really similar to the above command, but in this case, there are no characters I needed to save
post-substitution. We already know what <code>%s</code> does, so the pattern I&rsquo;m looking for is all lines that start with
<code>exports.main</code>. Since I don&rsquo;t need anything after that start of the line, I included <code>.*</code> which looks for any character until
the end of the line. After the pattern I write out what I need to replace it with, which is the first portion of the
dictionaries I need. Finally, we close it out with a global change so it changes it everywhere.</p>
<p><strong>Command:</strong> <code>:%s/return.*/],</code>
<strong>Explanation:</strong> Finally, I used this one to close out the domains list/array that I&rsquo;m using.</p>
<hr>
<p><strong>Bonus Command:</strong> <code>%s/&quot;props&quot;: \[\(.*\)\]\,/&quot;props&quot; : \1,/g</code></p>
<p><strong>Explanation:</strong> I won&rsquo;t go over all the characters and regex commands as I&rsquo;m reusing a lot of them from the above commands.
What is different here is that I&rsquo;ve lumped the wild card in parenthesis like so: <code>(.*\)</code>. Now, I can call back what is
pulled by using <code>\1</code>. The command above is looking for a dictionary item with a list as the value. I didn&rsquo;t need the list,
just a string, since all values in my list of dictionaries are single values. By searching for <code>&quot;props&quot;: [ string ],</code> and
using the <code>\1</code>, I&rsquo;m able to keep the string intact and the result is <code>&quot;props&quot;: string,</code>.</p>
<p>This was the milestone command to learn for me and helped unlock so much potential in my brain for other use
cases. For all the other commands I&rsquo;ve gone over above, I&rsquo;m fairly simply just replacing characters I no longer need that
exist either <em>before or after</em> characters I do need. Now that I can keep strings intact for substitutions, I bet I could
revisit all the above commands and make them even more efficient.</p>
<p>What I&rsquo;d like to learn next is if I can use an &ldquo;or&rdquo; statement in the regex. Let&rsquo;s say I have the same situation as above - a
list of dictionaries with some dict values being a list. Well, if the lists are all formatted differently, some on new lines,
others on a single line, can I select both in a single command? Something like: <code>:%s/&quot;domains&quot;: \[\(.* OR \n\)/</code>.</p>
<p>That will be a post for another time! Let me know on Mastodon if you have any other Neovim search and replace tips and tricks
that have become invaluable to your workflow. I&rsquo;m always amazed and impressed with this community and how differently people
are able to use the same tool.</p>
<hr>
<p>By the way, here&rsquo;s the final output of running the above commands on the sample code above, converted to Python.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>mappings <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;uuid&#39;</span>: <span style="color:#e6db74">&#34;1234-1234-1234-1234&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domains&#39;</span>: [
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domain_one&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domain_two&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domain_three&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domain_four&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domain_five&#39;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;domain_six&#39;</span>,
</span></span><span style="display:flex;"><span> ],
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">&#39;props&#39;</span>: <span style="color:#e6db74">&#39;property-mapping&#39;</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="resources">Resources</h2>
<p>Here are some various links of resources that I found helpful when learning about the above commands. I&rsquo;m assuming you&rsquo;ve
already read through anything in <a href="https://neovim.io/doc/user/">Neovim&rsquo;s Helpdocs</a> (<code>:h</code>), but sometimes you need a different take and explanation.</p>
<ul>
<li><a href="https://www.reddit.com/r/neovim/comments/i5iptq/vim_search_find_and_replace_a_detailed_guide/">Vim Search, Find &amp; Replace - Reddit</a></li>
<li><a href="https://vim.fandom.com/wiki/Search_across_multiple_lines">Search Across Multiple Lines - Vim Tips Wiki</a></li>
<li><a href="https://thevaluable.dev/vim-search-find-replace/">Vim Search and Replace with Examples - The Valuable Dev</a></li>
<li><a href="https://stackoverflow.com/questions/62751398/how-to-remove-specific-characters-in-vi-or-vim-editor">Removing Specific Characters in Vim - Stack Overflow</a></li>
</ul>
]]></content:encoded>
</item>
<item>
<title>Mini Neovim</title>
<link>/posts/mini_neovim.html</link>
<pubDate>Fri, 20 Oct 2023 18:38:13 -0400</pubDate>
<guid>/posts/mini_neovim.html</guid>
<description>Neovim is already super efficient and lightweight, but sometimes I need a barebones config for my servers and remote, miniature development environments.</description>
<content:encoded><![CDATA[<p>If I&rsquo;m being honest, when I started this quick project to reduce my Neovim plugin footprint size, I thought I was going to be
able to get rid of <em>way</em> more plugins! Inspired by <a href="https://www.reddit.com/r/neovim/comments/179zawc/my_new_config_based_entirely_on_mininvim/">this Reddit
post</a> that setup a Neovim config
using only <a href="https://github.com/echasnovski">echasnovski&rsquo;s</a> <a href="https://github.com/echasnovski/mini.nvim">mini</a> library, I was
reminded that some of my remote server environments were still using Packer as their plugin manager, where my main machine
has upgraded to Lazy. In addition, I have since expanded my main config with more applicable plugins and key maps and the
Packer config was in a single neovim repo, as opposed to my more recently implemented <a href="https://github.com/Normanras/dotfiles">dotfiles</a> setup.</p>
<p>Right now, I am using a whopping 77 plugins. That&rsquo;s mostly because I&rsquo;m not great at cleaning out old plugins I don&rsquo;t use as
much anymore. Since I am client facing, a lot of them are around making my Markdown notes easier to manage, and Python plugins
to help with scripting and file management in the terminal. This poll shows 10-25 plugins being the most common amount.</p>
<blockquote class="reddit-embed-bq" style="height:240px" data-embed-height="240"><a href="https://www.reddit.com/r/neovim/comments/14jdkbp/how_many_plugins_in_your_config_list_your/">How many plugins in your config. List your favorites!</a><br> by<a href="https://www.reddit.com/user/Bamseg/">u/Bamseg</a> in<a href="https://www.reddit.com/r/neovim/">neovim</a></blockquote><script async="" src="https://embed.reddit.com/widgets.js" charset="UTF-8"></script>
<p></p>
<p>Once I got my dotfiles to my server environment, I got to work and got rid of all the superfluous plugins like additional
color schemes, todo-trouble, or others that I considered nice-to-have. After all, across all my VMs I&rsquo;m mostly editing yaml,
json, and python scripts and I&rsquo;m not in there for a very long time. If I was a real pro, I would just use out-of-the-box Vim,
but I really like the speed some key maps and plugins have given me.</p>
<p>You can check out my git branch <a href="https://github.com/Normanras/dotfiles/tree/neovim-only-minimal">here</a>. If you decide to clone it, make sure you symlink <code>.config</code> folder from a <code>~/.dotfiles/</code> directory to <code>~/.config/</code>. I&rsquo;ve been using
<a href="https://www.gnu.org/software/stow/manual/stow.html">stow</a>, but any symlink tool will work.</p>
<p>If you have a neovim config that you&rsquo;d like to &ldquo;minimize&rdquo;, see some steps below. For any dotfile config - Neovim, tmux, zsh -
I like to make these changes in a clean environment. I want to replicate as closely as possible what a fresh install and
setup would be like. Here&rsquo;s how I do it:
<style type="text/css">
.box-shortcode {
padding: 1.6em;
padding-top: 1.4em;
line-height: 1.4em;
margin-top: 1em;
margin-bottom: 2em;
border-radius: 4px;
color: #444;
background: #f3ebe850;
}
.box-title {
margin: -18px -18px 12px;
padding: 4px 18px;
border-radius: 4px 4px 0 0;
font-weight: 700;
color: #fff;
background: #6ab0de;
}
.box-shortcode.warning .box-title {
background: #ff6b6b;
}
.box-shortcode.warning {
background: #ff6b6b4f;
}
.box-shortcode.info .box-title {
background: #0089e488;
}
.box-shortcode.info {
background: #0089e41c;
box-shadow: 3px 3px 5px #0089e410;
}
.box-shortcode.important .box-title {
background: #f7ec2c;
}
.box-shortcode.important {
background: #f7ec2c7d;
}
.box-shortcode.tip .box-title {
background: #a3ffa34d;
}
.box-shortcode.tip {
background: #a3ffa34d;
box-shadow: 3px 3px 5px #0089e410;
}
.icon-box {
display: inline-flex;
align-self: center;
margin-right: 8px;
}
.icon-box img,
.icon-box svg {
height: 1em;
width: 1em;
fill: currentColor;
}
.icon-box img,
.icon-box.baseline svg {
top: 0.125em;
position: relative;
}
.box-shortcode p {
margin-bottom: 0.6em;
}
.box-shortcode p:first-of-type {
display: inline;
}
.box-shortcode p:nth-of-type(2) {
margin-top: 0.6em;
}
.box-shortcode p:last-child {
margin-bottom: 0;
}
</style>
<svg width="0" height="0" display="none" xmlns="http://www.w3.org/2000/svg">
<symbol id="tip-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"/>
</symbol>
<symbol id="important-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</symbol>
<symbol id="warning-box" viewBox="0 0 576 512" preserveAspectRatio="xMidYMid meet">
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</symbol>
<symbol id="info-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"/>
</symbol>
</svg><div class="box box-shortcode info" >
<span class="icon-box baseline">
<svg><use href="#info-box"></use></svg>
</span>
<p>Any curly brackets in the instructions below denote a word where you can insert whatever you&rsquo;d like. But, most likely you
want to use the curly brackets in the final string that you use.</p>
</div>
</p>
<ol>
<li>Navigate to the machine/vm/server that you want to work on. For me, this VM doesn&rsquo;t have Neovim installed yet.</li>
<li><a href="https://github.com/neovim/neovim/wiki/Installing-Neovim">Install Neovim</a></li>
<li>Clone your main repository to either your <code>.config</code> or your <code>.dotfiles</code> directory. And run a few commands:</li>
</ol>
<ul>
<li><code>git branch {branch-name}</code></li>
<li><code>git checkout {branch-name}</code></li>
</ul>
<ol start="4">
<li>You&rsquo;ll now be in your branch, but everything will look the same. Start removing directories or files you don&rsquo;t need.</li>
<li>Navigate to your plugins folder or <code>init.lua</code> that lists all your plugins and start hacking and slashing.</li>
<li>Once you&rsquo;ve saved all those files and you can open and close neovim without any plugin errors, you&rsquo;re ready to go.</li>
<li>Navigate back to the root git directory and run a few more commands:</li>
</ol>
<ul>
<li><code>git add .</code></li>
<li><code>git commit -m {Insert a detailed commit message of what you changed and updated.}</code></li>
<li><code>git push {remote-name} {branch-name}</code></li>
</ul>
<ol start="8">
<li>And that&rsquo;s it! Go check your Github/Gitlab and check your branches and commits. You should see everything there! Congrats.</li>
</ol>
<p>For that last command, you can always check what your remote repo&rsquo;s push name is by running <code>git remote -v</code>. Most often, the name will be something like <code>origin</code> or <code>main</code>. If you saw <a href="/posts/multiple_git_remotes.html">my post on having multiple remote repos</a>, you may something like &ldquo;all&rdquo;. Just make sure to check before pushing!</p>
<style>
.box-shortcode {
color: #e8e8e8;
border: none;
}
.post-content img {
margin: auto
}
]]></content:encoded>
</item>
<item>
<title>ESP8266 Medicine Indicator Light</title>
<link>/posts/medicine_indicator_light.html</link>
<pubDate>Mon, 16 Oct 2023 11:34:14 -0400</pubDate>
<guid>/posts/medicine_indicator_light.html</guid>
<description>Learn how to make an ESP8266 and a few simple components into an indicator light.</description>
<content:encoded><![CDATA[<p>This is a quick treat! We recently learned that one of our children needs to take medicine twice a day for the foreseeable
future. He&rsquo;s too young to take it on his own, so the twice-a-day responsibility is split up between my partner and
I. However, sometimes our schedules don&rsquo;t overlap so succinctly, so we needed some sort of indicator to let the other know if
the previous dose was (or wasn&rsquo;t!) taken. Naturally, I sprung into action with an ESP device and components.</p>
<h2 id="device-overview">Device Overview</h2>
<p>The device works like this (<a href="/posts/medicine_indicator_light.html#pictures">see the pictures below</a>): Two <a href="https://www.adafruit.com/product/1260">single neopixel LEDs</a> and a
non-latching button are connected to the ESP8266. Inside the code there is a <a href="https://www.arduino.cc/reference/tr/language/structure/control-structure/switchcase/">switch-case</a>
section which changes states per high/low state of the button. On each press, the lights will change to one of the following:</p>
<ul>
<li>Red/Red</li>
<li>Green/Red</li>
<li>Red/Green</li>
<li>Green/Green</li>
</ul>
<p>For each case, I established a values for <code>morning</code> and <code>evening</code> which will be given a value of 0 or 1 - 0 to indicate that the medicine was
not taken, and 1 to indicate that it was taken. Along with the time server data, I send an MQTT payload to my <a href="https://mosquitto.org/">MQTT Broker</a>.</p>
<p>Here&rsquo;s what the payload looks like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;time&#34;</span>: <span style="color:#e6db74">&#34;1630&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;morning&#34;</span>: <span style="color:#e6db74">&#34;1&#34;</span>,
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;evening&#34;</span>: <span style="color:#e6db74">&#34;0&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="thoughts-and-background">Thoughts and Background</h2>
<p>I&rsquo;d like to write a longer post that explains each section of the code, but I&rsquo;m running out of
time - work has been quite busy lately. If you&rsquo;d like me to add a post with more code explanations, or just have questions,
feel free to mention me on <a href="https://fosstodon.org/@notnorm">Mastoton/Fosstodon</a> and we can chat.</p>
<p>The much shorter explanation is that I didn&rsquo;t want this device to be an isolated device in my network; this was part of the
impetus in using an ESP device as opposed to another microcontroller. So what I did was connected the ESP8266 to my wifi
network and imported an NTP (Network Time Protocol) server to ensure I get I have the current time. Then, look at the state
given by the case (and thus the external light indicator), and construct and transmit an MQTT payload with the state of the
light &amp; time, so that I can use it elsewhere. In other words, now with MQTT payloads being accessible to HomeAssistant, I can
send myself a notification, or make an announcement on a speaker.</p>
<p>What&rsquo;s neat about importing a time server is that at midnight, I reset both of the lights to red so that I don&rsquo;t have to
reset it manually when I get up in the morning.</p>
<p>In a separate post, I&rsquo;ll show how I use the MQTT payload in HomeAssistant to send <a href="https://pushover.net">Pushover Notifications</a>
to me and my partner in case we forgot to give my child the medicine! What&rsquo;s great about this little project is that it is
scalable (add more components and sensors!) and it isn&rsquo;t limited to a medicine indicator. You
could use the button to count how many times your dog (or child!) has gone to the bathroom, how many times you have eat or
drink water while working, and many more ideas.</p>
<p>Assuming everything has gone well for the day and we&rsquo;ve given my child both doses, we should be heading to bed with both of
those lights being green.</p>
<h2 id="materials-used">Materials Used</h2>
<p>Here is what I used to construct this:</p>
<ul>
<li><a href="https://www.amazon.com/HiLetgo-Internet-Development-Wireless-Micropython/dp/B081CSJV2V/ref=sr_1_1?crid=7JMC6TOMCS9I&amp;keywords=hiletgo+esp8255&amp;qid=1697229200&amp;sprefix=hiletgo+esp8255%2Caps%2C89&amp;sr=8-1">ESP8266 Dev
Board</a> - I tend to like HiLetgo from Amazon.</li>
<li>22 AWG Solid Wire</li>
<li>White Ping Pong ball - Cut in half</li>
<li>Balsa Wood</li>
<li><a href="https://www.adafruit.com/product/367">Tactile Button Switch</a></li>
<li><a href="https://www.adafruit.com/product/1260">Adafruit Flora RGB NeoPixel</a></li>
</ul>
<h2 id="pictures">Pictures</h2>
<div class="column">
<img src="../posts/img/esp_indicator_1.png">
<img src="../posts/img/esp_indicator_2.png">
</div>
<div class="column">
<img src="../posts/img/esp_indicator_3.png">
<img src="../posts/img/esp_indicator_wires.png">
</div>
<h2 id="full-code">Full Code</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c++" data-lang="c++"><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;Wire.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;Adafruit_NeoPixel.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;ESP8266WiFi.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;NTPClient.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;WiFiUdp.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;PubSubClient.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;ArduinoJson.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#include</span> <span style="color:#75715e">&lt;secrets.h&gt;</span><span style="color:#75715e">
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#define NEO_PIN 14 </span><span style="color:#75715e">// Pin for all the Neopixels
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e">#define SIG_PIX 2 </span><span style="color:#75715e">// Data pin for Neopixels
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#75715e">#define BUTTON_FWD_PIN 4
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">#define mqtt_topic &#34;home/medicine&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span><span style="color:#f92672">*</span> ssid <span style="color:#f92672">=</span> WIFI_SSID;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span><span style="color:#f92672">*</span> password <span style="color:#f92672">=</span> WIFI_PASS;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span><span style="color:#f92672">*</span> mqtt_server <span style="color:#f92672">=</span> MQTT_SERV;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span><span style="color:#f92672">*</span> mqtt_user <span style="color:#f92672">=</span> MQTT_USER;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">char</span><span style="color:#f92672">*</span> mqtt_password <span style="color:#f92672">=</span> MQTT_PASSWORD;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>boolean oldState <span style="color:#f92672">=</span> HIGH;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">int</span> mode <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; <span style="color:#75715e">// Active mode on startup
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">unsigned</span> <span style="color:#66d9ef">long</span> previousMillis <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">long</span> interval <span style="color:#f92672">=</span> <span style="color:#ae81ff">5000</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// NTP Definition
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">const</span> <span style="color:#66d9ef">long</span> utcOffsetInSeconds <span style="color:#f92672">=</span> <span style="color:#f92672">-</span><span style="color:#ae81ff">14400</span>;
</span></span><span style="display:flex;"><span>WiFiUDP ntpUDP;
</span></span><span style="display:flex;"><span>NTPClient <span style="color:#a6e22e">timeClient</span>(ntpUDP, <span style="color:#e6db74">&#34;north-america.pool.ntp.org&#34;</span>, utcOffsetInSeconds);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>WiFiClient espClient;
</span></span><span style="display:flex;"><span>PubSubClient <span style="color:#a6e22e">client</span>(espClient);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">long</span> lastMsg <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">char</span> msg[<span style="color:#ae81ff">50</span>];
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">float</span> value <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Setup defaults for the eventual MQTT payload
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">char</span> hours[] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">char</span> minutes[] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>;
</span></span><span style="display:flex;"><span>boolean morning_status <span style="color:#f92672">=</span> false;
</span></span><span style="display:flex;"><span>boolean evening_status <span style="color:#f92672">=</span> false;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">char</span> current_time[] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">int</span> int_time <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Necessary Setup for Neopixels
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>Adafruit_NeoPixel NeoJewel <span style="color:#f92672">=</span> Adafruit_NeoPixel(SIG_PIX, NEO_PIN, NEO_GRB <span style="color:#f92672">+</span> NEO_KHZ800);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">unsigned</span> <span style="color:#66d9ef">long</span> delayTime;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Taken Medicine Color
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">uint32_t</span> pineGreen <span style="color:#f92672">=</span> NeoJewel.Color(<span style="color:#ae81ff">14</span>, <span style="color:#ae81ff">170</span>, <span style="color:#ae81ff">26</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// Not Yet Taken Color
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">uint32_t</span> pureRed <span style="color:#f92672">=</span> NeoJewel.Color(<span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">setup</span>() {
</span></span><span style="display:flex;"><span> <span style="color:#75715e">//delayTime = 1000;
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> Serial.begin(<span style="color:#ae81ff">9600</span>);
</span></span><span style="display:flex;"><span> delay(<span style="color:#ae81ff">50</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">while</span> (<span style="color:#f92672">!</span>Serial) {
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// wait for serial port to connect. Needed for native USB port only
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Print Status of Wifi Connection to Serial Monitor
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> Serial.println(<span style="color:#e6db74">&#34;Attempting to connect to SSID: &#34;</span>);
</span></span><span style="display:flex;"><span> Serial.print(ssid);
</span></span><span style="display:flex;"><span> WiFi.begin(ssid, password);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Attempt to connect to WiFi network:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#66d9ef">while</span> (WiFi.status() <span style="color:#f92672">!=</span> WL_CONNECTED) {
</span></span><span style="display:flex;"><span> delay(<span style="color:#ae81ff">1000</span>);
</span></span><span style="display:flex;"><span> Serial.print(<span style="color:#e6db74">&#34;.&#34;</span>);
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> Serial.println(<span style="color:#e6db74">&#34;&#34;</span>);
</span></span><span style="display:flex;"><span> Serial.println(<span style="color:#e6db74">&#34;WiFi connected&#34;</span>);
</span></span><span style="display:flex;"><span> Serial.println(<span style="color:#e6db74">&#34;IP address: &#34;</span>);
</span></span><span style="display:flex;"><span> Serial.println(WiFi.localIP()); <span style="color:#75715e">//You can get IP address assigned to ESP
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> Serial.println(<span style="color:#e6db74">&#34;Mac Address: &#34;</span>);
</span></span><span style="display:flex;"><span> Serial.println(WiFi.macAddress());
</span></span><span style="display:flex;"><span> delay(<span style="color:#ae81ff">100</span>);
</span></span><span style="display:flex;"><span> client.setServer(mqtt_server, <span style="color:#ae81ff">1883</span>);
</span></span><span style="display:flex;"><span> WiFi.setAutoReconnect(true);
</span></span><span style="display:flex;"><span> WiFi.persistent(true);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Initialize Time
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> timeClient.begin();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Necessary code for button setup
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> pinMode(BUTTON_FWD_PIN, INPUT_PULLUP);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">//Initialization of the Jewel
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> NeoJewel.setBrightness(<span style="color:#ae81ff">100</span>);
</span></span><span style="display:flex;"><span> NeoJewel.begin();
</span></span><span style="display:flex;"><span> NeoJewel.clear();
</span></span><span style="display:flex;"><span> NeoJewel.show();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">loop</span>() {
</span></span><span style="display:flex;"><span> buttonpush();
</span></span><span style="display:flex;"><span> client.loop();
</span></span><span style="display:flex;"><span> timeClient.update();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">int</span> hours <span style="color:#f92672">=</span> timeClient.getHours();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">int</span> minutes <span style="color:#f92672">=</span> timeClient.getMinutes();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">int</span> int_time <span style="color:#f92672">=</span> (hours<span style="color:#f92672">*</span><span style="color:#ae81ff">100</span>)<span style="color:#f92672">+</span>minutes;
</span></span><span style="display:flex;"><span> String current_time <span style="color:#f92672">=</span> String(int_time);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (WiFi.status() <span style="color:#f92672">!=</span> WL_CONNECTED) {
</span></span><span style="display:flex;"><span> delay(<span style="color:#ae81ff">1000</span>);
</span></span><span style="display:flex;"><span> WiFi.disconnect();
</span></span><span style="display:flex;"><span> ESP.restart();
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>client.connected()) {
</span></span><span style="display:flex;"><span> reconnect();
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">unsigned</span> <span style="color:#66d9ef">long</span> currentMillis <span style="color:#f92672">=</span> millis();
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (currentMillis <span style="color:#f92672">-</span> previousMillis <span style="color:#f92672">&gt;=</span> interval) {
</span></span><span style="display:flex;"><span> previousMillis <span style="color:#f92672">=</span> currentMillis;
</span></span><span style="display:flex;"><span> Serial.print(int_time);
</span></span><span style="display:flex;"><span> publishmqtt(current_time, morning_status, evening_status);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Reset to red colors and new payload at midnight
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#66d9ef">if</span> (int_time <span style="color:#f92672">==</span> <span style="color:#ae81ff">0000</span>) {
</span></span><span style="display:flex;"><span> NeoJewel.clear();
</span></span><span style="display:flex;"><span> NeoJewel.fill(pureRed, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">2</span>);
</span></span><span style="display:flex;"><span> NeoJewel.show();
</span></span><span style="display:flex;"><span> morning_status <span style="color:#f92672">=</span> false;
</span></span><span style="display:flex;"><span> evening_status <span style="color:#f92672">=</span> false;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">buttonpush</span>() {
</span></span><span style="display:flex;"><span> boolean newState <span style="color:#f92672">=</span> digitalRead(BUTTON_FWD_PIN);
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span>((newState <span style="color:#f92672">==</span> LOW) <span style="color:#f92672">&amp;&amp;</span> (oldState <span style="color:#f92672">==</span> HIGH)) {
</span></span><span style="display:flex;"><span> delay(<span style="color:#ae81ff">20</span>);
</span></span><span style="display:flex;"><span> newState <span style="color:#f92672">=</span> digitalRead(BUTTON_FWD_PIN);
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span>(newState <span style="color:#f92672">==</span> LOW) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span>(<span style="color:#f92672">++</span>mode <span style="color:#f92672">&gt;</span> <span style="color:#ae81ff">4</span>) mode <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">switch</span>(mode) {
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#ae81ff">1</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Nothing Taken
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> { NeoJewel.clear();
</span></span><span style="display:flex;"><span> NeoJewel.fill(pureRed, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">2</span>);
</span></span><span style="display:flex;"><span> NeoJewel.show();
</span></span><span style="display:flex;"><span> morning_status <span style="color:#f92672">=</span> false;
</span></span><span style="display:flex;"><span> evening_status <span style="color:#f92672">=</span> false;
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>; }
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#ae81ff">2</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Morning Taken
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> { NeoJewel.clear();
</span></span><span style="display:flex;"><span> NeoJewel.fill(pineGreen, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">1</span>);
</span></span><span style="display:flex;"><span> NeoJewel.fill(pureRed, <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>);
</span></span><span style="display:flex;"><span> NeoJewel.show();
</span></span><span style="display:flex;"><span> morning_status <span style="color:#f92672">=</span> true;
</span></span><span style="display:flex;"><span> evening_status <span style="color:#f92672">=</span> false;
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>; }
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#ae81ff">3</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Afternoon Taken
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> { NeoJewel.clear();
</span></span><span style="display:flex;"><span> NeoJewel.fill(pineGreen, <span style="color:#ae81ff">1</span>, <span style="color:#ae81ff">2</span>);
</span></span><span style="display:flex;"><span> NeoJewel.fill(pureRed, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">1</span>);
</span></span><span style="display:flex;"><span> NeoJewel.show();
</span></span><span style="display:flex;"><span> morning_status <span style="color:#f92672">=</span> false;
</span></span><span style="display:flex;"><span> evening_status <span style="color:#f92672">=</span> true;
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>; }
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">case</span> <span style="color:#ae81ff">4</span><span style="color:#f92672">:</span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Both Taken
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> { NeoJewel.clear();
</span></span><span style="display:flex;"><span> NeoJewel.fill(pineGreen, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">2</span>);
</span></span><span style="display:flex;"><span> NeoJewel.show();
</span></span><span style="display:flex;"><span> morning_status <span style="color:#f92672">=</span> true;
</span></span><span style="display:flex;"><span> evening_status <span style="color:#f92672">=</span> true;
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">break</span>; }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Set the last-read button state to the old state (reset)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> oldState <span style="color:#f92672">=</span> newState;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">publishmqtt</span>(String current_time, boolean morning_status, boolean evening_status) {
</span></span><span style="display:flex;"><span> StaticJsonDocument<span style="color:#f92672">&lt;</span><span style="color:#ae81ff">200</span><span style="color:#f92672">&gt;</span> doc;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> doc[<span style="color:#e6db74">&#34;time&#34;</span>] <span style="color:#f92672">=</span> (String)current_time;
</span></span><span style="display:flex;"><span> doc[<span style="color:#e6db74">&#34;morning&#34;</span>] <span style="color:#f92672">=</span> (String)morning_status;
</span></span><span style="display:flex;"><span> doc[<span style="color:#e6db74">&#34;evening&#34;</span>] <span style="color:#f92672">=</span> (String)evening_status;
</span></span><span style="display:flex;"><span> serializeJsonPretty(doc, Serial);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">char</span> data[<span style="color:#ae81ff">200</span>];
</span></span><span style="display:flex;"><span> serializeJson(doc, data);
</span></span><span style="display:flex;"><span> client.publish(mqtt_topic, data, true);
</span></span><span style="display:flex;"><span> delay(<span style="color:#ae81ff">50</span>);
</span></span><span style="display:flex;"><span> yield();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">void</span> <span style="color:#a6e22e">reconnect</span>() {
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Loop until we&#39;re reconnected
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> <span style="color:#66d9ef">while</span> (<span style="color:#f92672">!</span>client.connected()) {
</span></span><span style="display:flex;"><span> Serial.println(<span style="color:#e6db74">&#34;Connecting to MQTT...&#34;</span>);
</span></span><span style="display:flex;"><span> <span style="color:#75715e">// Create a random client ID
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span> String clientId <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ESP8266Client-&#34;</span>;
</span></span><span style="display:flex;"><span> clientId <span style="color:#f92672">+=</span> String(random(<span style="color:#ae81ff">0xffff</span>), HEX);
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">if</span> (client.connect(clientId.c_str(), mqtt_user, mqtt_password)) {
</span></span><span style="display:flex;"><span> Serial.println(<span style="color:#e6db74">&#34;connected&#34;</span>);
</span></span><span style="display:flex;"><span> } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span> Serial.println(<span style="color:#e6db74">&#34;failed with state &#34;</span>);
</span></span><span style="display:flex;"><span> Serial.print(client.state());
</span></span><span style="display:flex;"><span> delay(<span style="color:#ae81ff">2000</span>);
</span></span><span style="display:flex;"><span> ESP.restart();
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><style>
.column {
display: grid;
width: 100%;
height: 100%;
grid-template-rows: auto auto;
grid-template-columns: auto auto;
}
img {
padding: 5px;
border-radius: 10px !important;
}
</style>
]]></content:encoded>
</item>
<item>
<title>BirdNET-PI &amp; HomeAssistant: Part 2</title>
<link>/posts/birdnet_homeassistant_part2.html</link>
<pubDate>Wed, 04 Oct 2023 10:35:23 -0400</pubDate>
<guid>/posts/birdnet_homeassistant_part2.html</guid>
<description>A Follow up from the previous post, this tutorial takes all the sensors we created and loads them into a beautiful dashboard!</description>
<content:encoded><![CDATA[<h2 id="checking-for-entities">Checking for Entities</h2>
<p>If you&rsquo;re following up on this from <a href="/posts/birdnet_homeassistant.html">my first post</a>, you&rsquo;ve already added your AppDaemon script and confirmed that the AppDaemon logs don&rsquo;t show any errors. Now is the true test if it&rsquo;s working: do you have the
new sensors in HomeAssistant?!</p>
<style type="text/css">
.box-shortcode {
padding: 1.6em;
padding-top: 1.4em;
line-height: 1.4em;
margin-top: 1em;
margin-bottom: 2em;
border-radius: 4px;
color: #444;
background: #f3ebe850;
}
.box-title {
margin: -18px -18px 12px;
padding: 4px 18px;
border-radius: 4px 4px 0 0;
font-weight: 700;
color: #fff;
background: #6ab0de;
}
.box-shortcode.warning .box-title {
background: #ff6b6b;
}
.box-shortcode.warning {
background: #ff6b6b4f;
}
.box-shortcode.info .box-title {
background: #0089e488;
}
.box-shortcode.info {
background: #0089e41c;
box-shadow: 3px 3px 5px #0089e410;
}
.box-shortcode.important .box-title {
background: #f7ec2c;
}
.box-shortcode.important {
background: #f7ec2c7d;
}
.box-shortcode.tip .box-title {
background: #a3ffa34d;
}
.box-shortcode.tip {
background: #a3ffa34d;
box-shadow: 3px 3px 5px #0089e410;
}
.icon-box {
display: inline-flex;
align-self: center;
margin-right: 8px;
}
.icon-box img,
.icon-box svg {
height: 1em;
width: 1em;
fill: currentColor;
}
.icon-box img,
.icon-box.baseline svg {
top: 0.125em;
position: relative;
}
.box-shortcode p {
margin-bottom: 0.6em;
}
.box-shortcode p:first-of-type {
display: inline;
}
.box-shortcode p:nth-of-type(2) {
margin-top: 0.6em;
}
.box-shortcode p:last-child {
margin-bottom: 0;
}
</style>
<svg width="0" height="0" display="none" xmlns="http://www.w3.org/2000/svg">
<symbol id="tip-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"/>
</symbol>
<symbol id="important-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</symbol>
<symbol id="warning-box" viewBox="0 0 576 512" preserveAspectRatio="xMidYMid meet">
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</symbol>
<symbol id="info-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"/>
</symbol>
</svg><div class="box box-shortcode info" >
<span class="icon-box baseline">
<svg><use href="#info-box"></use></svg>
</span>
<p>The best way to do this is by just type <code>e</code> from any screen in the HomeAssistant UI! That will bring up a list of entities.
Start typing &ldquo;bird&rdquo; or &ldquo;birdnet&rdquo; and you should see the new entities listed there.</p>
</div>
<h2 id="dashboard-overview--dependencies">Dashboard Overview &amp; Dependencies</h2>
<p>Now that we have the correct entities, lets take a look at what we&rsquo;re working with. Full disclosure that once I got this
working, I haven&rsquo;t really revisited it, refactored it, or made any improvements. I&rsquo;m sure you&rsquo;ll find ways to use less YAML,
but I wanted to get this out there sooner than later!</p>
<p><img loading="lazy" src="../posts/img/birdnet-homeassistant-dash-full.png" alt="HomeAssistant BirdNet-Pi Dashboard - Full View" />
</p>
<p>I&rsquo;ve included the code for all the cards at the bottom of this post. You can find them <a href="/posts/birdnet_homeassistant_part2.html#dashboard-yaml">here</a>.
This dashboard is pretty simple, it brings in almost all of the sensors
we created in the first post and organizes them in an as-pleasant-as-possible view. I&rsquo;m definitely not a designer, so some of
the colors could be worked on&hellip;</p>
<p>Sensors in the dashboard:</p>
<ul>
<li><a href="/posts/birdnet_homeassistant_part2.html#overview-card">Overview Card:</a>
<ul>
<li>sensor.bird_common_name (only used to generate the picture)</li>
<li>camera.birdnet_flickr</li>
<li>sensor.bird_common_name</li>
<li>sensor.bird_science_name</li>
</ul>
</li>
<li>Data Card:
<ul>
<li>sensor.bird_time_seen</li>
<li>sensor.bird_confidence</li>
<li>sensor.bird_last_seen</li>
</ul>
</li>
<li>Weather Card:
<ul>
<li>weather.pirateweather</li>
</ul>
</li>
<li>Description Card:
<ul>
<li>sensor.birdnet_wiki</li>
</ul>
</li>
</ul>
<p>There are also two HomeAssistant dashboard dependencies that you&rsquo;ll need for this dashboard:</p>
<ul>
<li><a href="https://github.com/bramkragten/weather-card">Custom Weather Card</a></li>
<li><a href="https://github.com/custom-cards/button-card">Custom Button Card</a></li>
<li><a href="https://github.com/thomasloven/lovelace-card-mod">Custom Card Mod</a></li>
</ul>
<p>Now that we&rsquo;ve got that squared away, let&rsquo;s jump into each card.</p>
<h2 id="overview-card">Overview Card</h2>
<p><img loading="lazy" src="../posts/img/birdnet-homeassistant-overview-card.png" alt="Overview Card" />
</p>
<p>You&rsquo;ll notice from the few dependencies listed above, that I use this button card. <strong>A lot.</strong>
<a href="https://github.com/RomRider">RomRider</a> did a fantastic job of adding in a ton of flexibility into the card. For the overview
card, we&rsquo;re taking one of the entities, in this case, <code>bird_common_name</code> and attaching the Flickr picture/sensor to it. Then,
on the right, I&rsquo;m displaying the name and common name. Here&rsquo;s the overview card&rsquo;s yaml.</p>
<p>The tricky or tedious part of this card is making sure most of the card&rsquo;s attributes (icon, name, state, label) are set to
false. Another option for the Common and Scientific name on the right is to use a markdown card. I couldn&rsquo;t get the
formatting just right when using that card and some grid-card tricks, so I opted to reuse the super flexible button-card.</p>
<p>Pay attention to the styles for the image (lines 24-40). Those are what keep the border around the image along with the image
a certain height and width so that it looks proportional on the page.</p>
<p>One additional thing I have been toying with but hadn&rsquo;t finalized was messing with <code>[img_cell][border]</code>. By adding some
javascript in that section (see other cards), you could change the color of the border based on another parameter. Perhaps
you look at the state of <code>sensor.bird_common_name</code> and if there is the name of a color in there, that&rsquo;s the border&rsquo;s color.
Feel free to get crazy and creative with this!</p>
<style>
details summary {
min-width: 200px;
font-weight: 600;
cursor: pointer;
}
details summary > * {
display: inline;
}
details {
margin: 1em;
border-radius: 5px;
padding: 1em;
overflow: hidden;
box-shadow: 0 .1rem 1rem -.5rem rgba(0,0,0,.4);
}
summary {
padding: 0 1rem .3em 1.2rem;
display: block;
position: relative;
cursor: pointer;
}
summary:before {
content: '>';
position: absolute;
top: .1rem;
left: 0.1rem;
transform: rotate(0);
transition: .3s transform ease;
}
details[open] > summary:before {
transform: rotate(180deg);
content: '-';
}
details summary::-webkit-details-marker {
display:none;
}
</style><details><summary>Overview Card YAML</summary>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">type</span>: <span style="color:#ae81ff">horizontal-stack</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">cards</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">sensor.bird_common_name</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">triggers_update</span>: <span style="color:#ae81ff">all</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_state</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_label</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">none</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">width</span>: <span style="color:#ae81ff">215px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">height</span>: <span style="color:#ae81ff">175px</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">custom_fields</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">picture</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">camera.birdnet_flickr</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_entity_picture</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">height</span>: <span style="color:#ae81ff">100</span><span style="color:#ae81ff">%</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">width</span>: <span style="color:#ae81ff">100</span><span style="color:#ae81ff">%</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">padding</span>: <span style="color:#ae81ff">0px 15px 0px 15px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border-radius</span>: <span style="color:#ae81ff">3px 3px 15px 3px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">none</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">overflow</span>: <span style="color:#ae81ff">visible</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">img_cell</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">width</span>: <span style="color:#ae81ff">180px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">height</span>: <span style="color:#ae81ff">160px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border-radius</span>: <span style="color:#ae81ff">69</span><span style="color:#ae81ff">%</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">3px solid grey</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity_picture</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">width</span>: <span style="color:#ae81ff">215px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">height</span>: <span style="color:#ae81ff">100</span><span style="color:#ae81ff">%</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">vertical-stack</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">cards</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">sensor.bird_common_name</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_entity_picture</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_state</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">none</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">margin-top</span>: <span style="color:#ae81ff">35px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">font-size</span>: <span style="color:#ae81ff">25px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">width</span>: <span style="color:#ae81ff">auto</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">sensor.bird_science_name</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_entity_picture</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_state</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">black</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">width</span>: <span style="color:#ae81ff">auto</span>
</span></span></code></pre></div>
</details>
<h2 id="data-card">Data Card</h2>
<p><img loading="lazy" src="../posts/img/birdnet-homeassistant-data-card.png" alt="Data Card" />
</p>
<p>This card is fairly straight forward in that it&rsquo;s showing 3 key data points: the time of detection, detection confidence,
how long ago it was seen. This could be fairly redundant since we already have the time of detection, but when you&rsquo;re just
quickly glancing at the dashboard, minutes ago is much faster brain processing than comparing the timestamp and the current
time.</p>
<p>You&rsquo;ve likely picked up by now that in the previous post, we never sent a payload to create the <code>sensor.bird_last_seen</code>
entity. Here&rsquo;s how you can do it.</p>
<h3 id="creating-bird-last-seen-entity">Creating Bird Last Seen Entity</h3>
<p>When I first set out to create this sensor, I was messing with jinja templates for timestamp, datetime, strptime, and more.
Here are a few code blocks I saved in my notes in case I went too far down the wrong path. Here are a few of them.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jinja" data-lang="jinja"><span style="display:flex;"><span><span style="color:#75715e">{{</span> now<span style="color:#f92672">()</span> <span style="color:#f92672">-</span> state_attr<span style="color:#f92672">(</span>sensor.bird_time_seen<span style="color:#f92672">,</span> <span style="color:#e6db74">&#39;last_triggered&#39;</span><span style="color:#f92672">)</span> <span style="color:#f92672">&gt;</span> timedelta<span style="color:#f92672">(</span>hours<span style="color:#f92672">=</span><span style="color:#ae81ff">24</span><span style="color:#f92672">)</span> <span style="color:#75715e">}}</span>
</span></span></code></pre></div><p>or:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jinja" data-lang="jinja"><span style="display:flex;"><span><span style="color:#75715e">{%</span> <span style="color:#66d9ef">set</span> bird <span style="color:#f92672">=</span> strptime<span style="color:#f92672">(</span>states<span style="color:#f92672">(</span><span style="color:#e6db74">&#39;sensor.bird_time_seen&#39;</span><span style="color:#f92672">),</span> <span style="color:#e6db74">&#34;%H:%M:%S&#34;</span><span style="color:#f92672">)</span> <span style="color:#75715e">%}</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">{{</span> relative_time<span style="color:#f92672">(</span>bird<span style="color:#f92672">)</span> <span style="color:#75715e">}}</span>
</span></span></code></pre></div><p>The thing is, HomeAssistant has already implemented this really neat feature for calculating time, especially from when
something was last updated. This function is called <code>relative_time</code>. Having something like this allows you set automations
to run after a specific amount of time has passed since the last time a sensor or entity was updated.</p>
<div class="box box-shortcode tip" >
<span class="icon-box baseline">
<svg><use href="#tip-box"></use></svg>
</span>
<p>An idea! 💡 For our specific use case, you could set up an automation that sends you a notification if no birds have been detected for
over 30 minutes. Of course, we&rsquo;ll set parameters like not to notify you at night or during the winter months.</p>
</div>
<p>The issue I faced with relative time has to do with the sensors I created from my AppDaemon script. Relative time expects
a date <em>and</em> time. I was only passing the time. In Home Assitant if you use the Developer Tools &gt; Template to test relative
time out on the <code>sensor.bird_time_seen</code> sensor, you&rsquo;ll get a result of 126 years&hellip; That&rsquo;s because without a date, Home
Assistant defaults the date to <code>1900-01-01</code>. The full relative_time return is <code>1900-01-01 15:15:15</code>.</p>
<p>We could go back and set the sensors to include both date and time, but I prefer them separate so that I can use them in
different places. For this dashboard, the day is always today, so having the date felt redundant. To create a new sensor
using the <code>relative_time</code> function, you&rsquo;ll need to edit your <code>configuration.yaml</code>.</p>
<p>Once you&rsquo;re editing your config file, add the following:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">template</span>:
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Bird Time Last Seen</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">sensor</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;Bird Last Seen&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">state</span>: &gt;<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> {% set birdseen = (states(&#39;sensor.bird_date_seen&#39;)+&#39; &#39;+states(&#39;sensor.bird_time_seen&#39;)) %}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> {% set bird = relative_time(strptime(birdseen, &#39;%Y-%m-%d %H:%M:%S&#39;)) %}
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> {{ bird }}</span>
</span></span></code></pre></div><p>What this does is uses Home Assistant&rsquo;s templating functionality and creates a new sensor called &ldquo;Bird Last Seen&rdquo;. The
default <code>sensor.</code> name will be <code>sensor.bird_last_seen</code>.</p>
<p>To configure the state of that sensor, we first set a variable called <code>birdseen</code>. To this variable we are assigning the
concatenated values of <code>bird_date_seen</code>, a single whitespace, and <code>bird_time_seen</code>. We&rsquo;re choosing this format because that
is the format that <code>relative_time</code> returned before when we tried using it without a date.</p>
<p>As a quick experiment, take the templating code under the <code>state: &gt;</code> parameter above and throw it into Developer Tools &gt;
Template. Do you get 126 years? Or something more realistic? If something more realistic, amazing!</p>
<p>We&rsquo;re almost there! Here&rsquo;s what you should see in HomeAssistant if the sensor was created correctly.
<img loading="lazy" src="../posts/img/birdnet-homeassistant-birdseen-sensor.png" alt="Bird Last Seen Entity" />
</p>
<div class="box box-shortcode info" >
<span class="icon-box baseline">
<svg><use href="#info-box"></use></svg>
</span>
<p>If you&rsquo;re new to templating for Home Assistant (or in general!) it would be helpful to read through a few of the docs that
HomeAssistant provides.</p>
<ul>
<li><a href="https://www.home-assistant.io/docs/configuration/templating/">HomeAssistant Templating Docs</a></li>
<li><a href="https://palletsprojects.com/p/jinja">Jinja2 Templating Engine Docs</a></li>
</ul>
<p><em>Note: Jinja2 is very popular and common. Learning it for home automation is worth it alone, but it may very well come in
handy in other places too!</em></p>
</div>
<details><summary>Data Card Yaml</summary>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">type</span>: <span style="color:#ae81ff">horizontal-stack</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">cards</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">sensor.bird_time_seen</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_state</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">icon</span>: <span style="color:#ae81ff">mdi:clock-outline</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">color</span>: <span style="color:#ae81ff">darkgrey</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">none</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">sensor.bird_confidence</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_state</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">icon</span>: <span style="color:#ae81ff">mdi:check-circle</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">none</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">icon</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">color</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> [[[
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> if (states[&#39;sensor.bird_confidence&#39;].state &gt; 80 )
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> return &#34;green&#34;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> return &#34;lightblue&#34;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ]]]</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">sensor.bird_last_seen</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_state</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">icon</span>: <span style="color:#ae81ff">mdi:timer-refresh-outline</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">none</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">icon</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">color</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> [[[
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> var y = states[&#39;sensor.bird_last_seen&#39;].state;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> let x = y.slice(0, 2);
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> var e = Number(x);
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> if (e &lt; 5) return &#39;#ff6969&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> if (e &lt; 10) return &#39;#ffdf87&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> if (e &lt; 15) return &#39;#d9d76f&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> if (e &lt; 20) return &#39;#fcc2ea&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> else return &#39;#ccccc8&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ]]]</span>
</span></span></code></pre></div>
</details>
<h2 id="weather-card">Weather Card</h2>
<p><img loading="lazy" src="../posts/img/birdnet-homeassistant-weather-card.png" alt="Weather Card" />
</p>
<p>This doesn&rsquo;t need a lot of explaining or instructions. It is just the standard weather card! Here&rsquo;s the YAML, none of the
less, so you know what I toggled on/off. I&rsquo;m using <a href="https://pirateweather.net/en/latest/">Pirate Weather Integration</a> as my
data source.</p>
<details><summary>Weather Card</summary>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:weather-card</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">entity</span>: <span style="color:#ae81ff">weather.pirateweather</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">forecast</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">hourly_forecast</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">name</span>: <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">details</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">current</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">number_of_forecasts</span>: <span style="color:#e6db74">&#39;5&#39;</span>
</span></span></code></pre></div>
</details>
<h2 id="description-card">Description Card</h2>
<p>Finally, we reach the bottom of the dashboard: the description card. This one is also really straightforward. We&rsquo;re just
using a <a href="https://www.home-assistant.io/dashboards/markdown/">standard markdown card</a> and taking the description sensor we
created using Wikipedia&rsquo;s API and making that the main content of the card.</p>
<p><img loading="lazy" src="../posts/img/birdnet-homeassistant-description-card.png" alt="Bird Description Card" />
</p>
<p>Other than setting the theme, the only other small changes are removing the border and increasing from the default font size.
We&rsquo;ll use <a href="https://github.com/thomasloven">Thomas Loven&rsquo;s</a> famous Card Mod for that.</p>
<details><summary>Description Card</summary>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">type</span>: <span style="color:#ae81ff">markdown</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">content</span>: <span style="color:#e6db74">&#39;{{ state_attr(&#39;&#39;sensor.birdnet_wiki&#39;&#39;,&#39;&#39;description&#39;&#39;)}}&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">theme</span>: <span style="color:#ae81ff">Catppuccin Mocha</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">card_mod</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">style</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ha-card.type-markdown {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> border: none;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ha-markdown {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> font-size: 16px;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> }</span>
</span></span></code></pre></div>
</details>
<h2 id="conclusion">Conclusion</h2>
<p>And that&rsquo;s all there is to it! I say that flippantly, but I know that it can seem like there&rsquo;s a lot of setup. Everything I
did here evolved out of other people&rsquo;s projects and dashboards on <a href="www.reddit.com/r/homeassistant">Reddit</a> or the invaluable
<a href="https://community.home-assistant.io/">HomeAssistant Community</a></p>
<p>Please feel free to reach out to me on <a href="www.fosstodon.org/@notnorm">Mastodon</a> if you have any questions or get stuck
anywhere!</p>
<h2 id="full-dashboard-yaml">Full Dashboard YAML</h2>
<details><summary>Full Dashboard YAML</summary>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#f92672">theme</span>: <span style="color:#ae81ff">Catppuccin Macchiato</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">title</span>: <span style="color:#ae81ff">BirdNet-Dashboard</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">path</span>: <span style="color:#ae81ff">birdnet-dashboard</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">icon</span>: <span style="color:#ae81ff">mdi:bird</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:vertical-layout</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">badges</span>: []
</span></span><span style="display:flex;"><span> <span style="color:#f92672">cards</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">horizontal-stack</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">cards</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">sensor.bird_common_name</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">triggers_update</span>: <span style="color:#ae81ff">all</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_state</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_label</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">none</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">width</span>: <span style="color:#ae81ff">215px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">height</span>: <span style="color:#ae81ff">175px</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">custom_fields</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">picture</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">camera.birdnet_flickr</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_entity_picture</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">height</span>: <span style="color:#ae81ff">100</span><span style="color:#ae81ff">%</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">width</span>: <span style="color:#ae81ff">100</span><span style="color:#ae81ff">%</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">padding</span>: <span style="color:#ae81ff">0px 15px 0px 15px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border-radius</span>: <span style="color:#ae81ff">3px 3px 15px 3px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">none</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">overflow</span>: <span style="color:#ae81ff">visible</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">img_cell</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">width</span>: <span style="color:#ae81ff">180px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">height</span>: <span style="color:#ae81ff">160px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border-radius</span>: <span style="color:#ae81ff">69</span><span style="color:#ae81ff">%</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">3px solid grey</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity_picture</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">width</span>: <span style="color:#ae81ff">215px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">height</span>: <span style="color:#ae81ff">100</span><span style="color:#ae81ff">%</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">vertical-stack</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">cards</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">sensor.bird_common_name</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_entity_picture</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_state</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">none</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">margin-top</span>: <span style="color:#ae81ff">35px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">font-size</span>: <span style="color:#ae81ff">25px</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">width</span>: <span style="color:#ae81ff">auto</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">sensor.bird_science_name</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_entity_picture</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_state</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">black</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">width</span>: <span style="color:#ae81ff">auto</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">horizontal-stack</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">cards</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">sensor.bird_time_seen</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_state</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">icon</span>: <span style="color:#ae81ff">mdi:clock-outline</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">color</span>: <span style="color:#ae81ff">darkgrey</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">none</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">sensor.bird_confidence</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_state</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">icon</span>: <span style="color:#ae81ff">mdi:check-circle</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">none</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">icon</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">color</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> [[[
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> if (states[&#39;sensor.bird_confidence&#39;].state &gt; 80 )
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> return &#34;green&#34;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> return &#34;lightblue&#34;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ]]]</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:button-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">sensor.bird_last_seen</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_state</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_icon</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">show_name</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">icon</span>: <span style="color:#ae81ff">mdi:timer-refresh-outline</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">styles</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">border</span>: <span style="color:#ae81ff">none</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">background</span>: <span style="color:#ae81ff">transparent</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">icon</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">color</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> [[[
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> var y = states[&#39;sensor.bird_last_seen&#39;].state;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> let x = y.slice(0, 2);
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> var e = Number(x);
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> if (e &lt; 5) return &#39;#ff6969&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> if (e &lt; 10) return &#39;#ffdf87&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> if (e &lt; 15) return &#39;#d9d76f&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> if (e &lt; 20) return &#39;#fcc2ea&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> else return &#39;#ccccc8&#39;;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ]]]</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">custom:weather-card</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">entity</span>: <span style="color:#ae81ff">weather.pirateweather</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">forecast</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">hourly_forecast</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">name</span>: <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">details</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">current</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">type</span>: <span style="color:#ae81ff">markdown</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">content</span>: <span style="color:#e6db74">&#39;{{ state_attr(&#39;&#39;sensor.birdnet_wiki&#39;&#39;,&#39;&#39;description&#39;&#39;)}}&#39;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">theme</span>: <span style="color:#ae81ff">Catppuccin Mocha</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">card_mod</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">style</span>: |<span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ha-card.type-markdown {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> border: none;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> }
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> ha-markdown {
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> font-size: 16px;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74"> }</span>
</span></span></code></pre></div>
</details>
<style>
.box-shortcode {
color: #e8e8e8;
border: none;
}
.post-content img {
margin: auto
}
</style>
]]></content:encoded>
</item>
<item>
<title>Creating a BirdNetPi Dashboard in HomeAssistant - Part 1</title>
<link>/posts/birdnet_homeassistant.html</link>
<pubDate>Sat, 30 Sep 2023 11:21:55 -0400</pubDate>
<guid>/posts/birdnet_homeassistant.html</guid>
<description>Learn how to take BirdNET-Pi Detections to create and display entities in HomeAssistant.</description>
<content:encoded><![CDATA[<p>This is Part One of a Two Part Series. You can find Part Two, <a href="/posts/birdnet_homeassistant_part2.html">here.</a></p>
<p><strong>Update: 10/11/2023. A huge thanks to Mastodon User <a href="https://mastodon.social/@e_mobil2014">e_mobile2014</a> who found a broken link in this guide and pointed out that I never explained how to get the mqtt sensors into HomeAssistant!</strong></p>
<h2 id="what-you-will-need">What you will need</h2>
<ul>
<li><a href="https://github.com/mcguirepr89/BirdNET-Pi">BirdNET-Pi</a></li>
<li><a href="https://www.home-assistant.io/">HomeAssistant</a></li>
<li><a href="https://appdaemon.readthedocs.io/en/latest/">AppDaemon</a></li>
<li>MQTT Broker (I use <a href="https://mosquitto.org/">Mosquitto</a>)</li>
</ul>
<h2 id="background">Background</h2>
<p>In early 2023, at the height of the <a href="https://www.tomshardware.com/news/raspberry-pi-availability-analysis">Raspberry Pi
shortage</a> I felt like a king with an extra Rpi laying
around, not being used. I&rsquo;m a big fan of any sort of passive intake of information and had been looking around for various
citizen science-style projects that can capture information from the world around me. Since I&rsquo;m already running an ADS-B
antenna with <a href="https://www.flightaware.com/">Flight Aware</a>, I figured this next project would deal with radio
waves/transmissions. Instead, to my amazement, I discovered <a href="https://github.com/mcguirepr89/BirdNET-Pi">BirdNET-Pi</a>!</p>
<h2 id="what-is-birdnet-pi">What is BirdNET-Pi?</h2>
<p>In case you didn&rsquo;t click the links above, <a href="https://github.com/mcguirepr89/BirdNET-Pi">BirdNET-Pi</a> is an app built specifically
made for Rapsberry Pi devices, that builds off the <a href="https://github.com/kahst/BirdNET-Analyzer">BirdNET Framework</a>. BirdNET is
one of the most advanced acoustic monitoring tools available for passively monitoring bird diversity populations.
Where BirdNET-Pi takes it to the next level is the ability to setup an SBC - hopefully enclosed in a waterproof space! - and
monitor birds in your local environment over time.</p>
<p>I think this project is beyond neat. It runs a bit slow on a Raspberry Pi 3, but overall it runs smoothly. I was even able to
<a href="https://github.com/mcguirepr89/BirdNET-Pi/pull/821">contribute a PR to the project</a> in April when I noticed a bug in the
platform after a hard reset of my Pi.</p>
<h2 id="birdnet-pi-notification-setup---mqtt">BirdNET-PI Notification Setup - MQTT</h2>
<p>Once you have BirdNET-Pi up and running, you&rsquo;ll need to head over to the Settings and setup the correct MQTT payloads. Here
are the possible variables you can pass in an MQTT payload:</p>
<ul>
<li><code>$sciname</code>: Scientific Name</li>
<li><code>$comname</code>: Common Name</li>
<li><code>$confidence</code>: Confidence Score</li>
<li><code>$confidencepct</code>: Confidence Score as a percentage (eg. 0.91 =&gt; 91)</li>
<li><code>$listenurl</code>: A link to the detection</li>
<li><code>$date</code>: Date</li>
<li><code>$time</code>: Time</li>
<li><code>$week</code>: Week</li>
<li><code>$latitude</code>: Latitude</li>
<li><code>$longitude</code>: Longitude</li>
<li><code>$cutoff</code>: Minimum Confidence set in &ldquo;Advanced Settings&rdquo;</li>
<li><code>$sens</code>: Sigmoid Sensitivity set in &ldquo;Advanced Settings&rdquo;</li>
<li><code>$overlap</code>: Overlap set in &ldquo;Advanced Settings&rdquo;</li>
<li><code>$flickrimage</code>: A preview image of the detected species from Flickr. Set your API key below.</li>
</ul>
<p>For our purposes, we will only be using <code>$comname, $sciname, $date, $time, $week,</code> and <code>$confidence</code>. However, this entire
process is extremely customizable, which you&rsquo;ll learn more about in the AppDaemon section. Please expand on it and include
information that is pertinent to your own uses.</p>
<p>Here is how I&rsquo;ve setup my MQTT payload from BirdNET-Pi Settings:</p>
<p><img loading="lazy" src="../posts/img/birdnet_mqtt_settings.png" alt="Notification Settings" />
</p>
<p>Here it is in text form:</p>
<pre tabindex="0"><code class="language-none" data-lang="none">Notification Title: $comname,
Notification Body: $sciname, $date, $time, $week, $confidence
[ ] Notify each new infrequent species detection (&lt; 5 visits per week)
[ ] Notify each new species first detection of the day
[X] Notify each new detection
[X] Send weekly report
Minimum time between notifications of the same species (sec): 5
</code></pre><p>To test my MQTT notifications, I use the iOS client &ldquo;MQTTool&rdquo;. After signing up, head to &ldquo;Subscribe&rdquo; and type <code>birdnet</code> as
the topic and then click Subscribe. If everything is setup correctly and there are birds being recorded by the BirdNET-Pi&rsquo;s
microphone, you should start seeing those detections in the MQTTool app. If so, fantastic news! Let&rsquo;s move onto AppDaemon.</p>
<h2 id="appdaemon-script">AppDaemon Script</h2>
<p>Now that we have the Pi communicating via MQTT, it&rsquo;s time to get that information into HomeAssistant. I&rsquo;ve shared <a href="/posts/birdnet_homeassistant.html#birdnet-appdaemon-script">the full
script</a> at the bottom of this page, but let&rsquo;s jump into each
section. This is not a full tutorial of how to use AppDaemon, but it may help fill in any knowledge gaps with the system.</p>
<h3 id="imports">Imports</h3>
<p>First, we&rsquo;re going to import <code>time</code> and <code>requests</code>. We&rsquo;re going to use time as a backup to the <code>$time</code> component in the
payload. This can be helpful to see if there delays, or if BirdNET-Pi stopped detecting. We&rsquo;re then going to use requests to
pull from Wikipedia&rsquo;s API and grab a description for our HomeAssistant Dashboard.</p>
<h3 id="class-definition">Class Definition</h3>
<p>To start any AppDaemon app, you need to include a Class that is defined in the <code>apps.yaml</code> file. This is also where we
initialize and define the various items that will be used in the remainder of the script.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">birdnet</span>(adbase<span style="color:#f92672">.</span>ADBase):
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">initialize</span>(self):
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>hassapi <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>get_plugin_api(<span style="color:#e6db74">&#34;HASS&#34;</span>)
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>adapi <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>get_ad_api()
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>mqttapi <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>get_plugin_api(<span style="color:#e6db74">&#34;MQTT&#34;</span>)
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>birdnet_mqtt <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;birdnet&#34;</span>
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>mqttapi<span style="color:#f92672">.</span>listen_event(
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>birdnet_message, <span style="color:#e6db74">&#34;MQTT_MESSAGE&#34;</span>, topic<span style="color:#f92672">=</span>self<span style="color:#f92672">.</span>birdnet_mqtt
</span></span><span style="display:flex;"><span> )
</span></span></code></pre></div><p>For this script, we need to use a lot of the AppDaemon APIs across more than just HomeAssistant, so we&rsquo;re going to be using
<code>ADBase</code>. By using that, we can initialize the various APIs, which we do in the next 3 lines. In these 3 lines we need to get
access to HomeAssistant&rsquo;s APIs, AppDaemon&rsquo;s APIs, and MQTT APIs - the first and third items are plugins of AppDaemon, and
AppDaemon is&hellip; well&hellip; AppDaemon! Here are a few reference docs:</p>
<ul>
<li><a href="https://appdaemon.readthedocs.io/en/latest/MQTT_API_REFERENCE.html">MQTT AppDaemon API</a></li>
<li><a href="https://appdaemon.readthedocs.io/en/latest/HASS_API_REFERENCE.html">HomeAssistant AppDaemon API</a></li>
<li><a href="https://appdaemon.readthedocs.io/en/latest/AD_API_REFERENCE.html">AppDaemon API</a></li>
</ul>
<p>These will indispensable to you as you leverage AppDaemon and expand this little script.</p>
<p>Once we have access to that, we need to setup the main topic for MQTT from BirdNET-Pi and finally, what event we are
listening for that will trigger the functions in the rest of the script. <code>self.birdnet_mqtt = &quot;birdnet&quot;</code> is the definition
for the MQTT topic. Let&rsquo;s breakdown the last line of the class.</p>
<p>Here&rsquo;s a breakdown of each of the items in that last line. You can find the official documentation <a href="https://appdaemon.readthedocs.io/en/latest/MQTT_API_REFERENCE.html#appdaemon.plugins.mqtt.mqttapi.Mqtt.listen_event">here</a>.</p>
<ul>
<li><code>self.mqttapi.listen_event</code> - this is what we use in AppDaemon to listen for an MQTT event in order to trigger a function.</li>
<li><code>self.birdnet_message</code> - the name of the function you&rsquo;d like to trigger</li>
<li><code>&quot;MQTT_MESSAGE&quot;</code> - The default event in AppDaemon&rsquo;s MQTT API plugin. This is used because MQTT doesn&rsquo;t keep a state in this
plugin.</li>
<li><code>topic=self.birdnet_mqtt</code> - The topic that will be received to trigger the function. Defined on the previous line.</li>
</ul>
<p>In other words, what we are telling AppDaemon is the following: &ldquo;When AppDaemon&rsquo;s MQTT API plugin receives a message with the
topic of &lsquo;birdnet&rsquo;, run the function <code>birdnet_message</code>.&rdquo;</p>
<h3 id="birdnet_message-function">birdnet_message Function</h3>
<h4 id="part-1-variables-management">Part 1: Variables Management</h4>
<p>Now we get into our first function of the script. The first portion of this script is splitting up the payload that we
defined from the BirdNET-Pi UI into individual variables that we can better manage later on. If you test this script out by
adding <code>print()</code> statements at various points, you&rsquo;ll notice that the payload is received with the following json formatting:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;payload&#34;</span>: {
</span></span><span style="display:flex;"><span> <span style="color:#f92672">&#34;data&#34;</span>: <span style="color:#e6db74">&#34;data&#34;</span>
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>As such, we need to look <em>inside</em> the payload to begin grabbing the data. The <code>pre_split</code> variable is now just looking at the
data inside the payload and the rest of the variables take all the date into the payload, split it by the comma, and then
grab the string by their index. If you remember what <a href="/posts/birdnet_homeassistant.html#birdnet-pi-notification-setup-mqtt">we did above</a> above, you&rsquo;ll see that we have the various BirdNET information at each of the indexes in the AppDaemon script - 0 through 5.</p>
<h4 id="part-2-re-publishing-mqtt-payloads">Part 2: Re-Publishing MQTT Payloads</h4>
<p>This next section is shooting all the variables we just defined back via MQTT. The reason why we do it this way is because we
need HomeAssistant to grab each of these variables as individual sensors. BirdNET doesn&rsquo;t give us that capability - it&rsquo;s a
single message with all the information in one. <a href="https://appdaemon.readthedocs.io/en/latest/MQTT_API_REFERENCE.html">Here is the documentation from AppDaemon</a> on <code>mqtt_publish</code>. Later on, I&rsquo;ll show you how to ensure that HomeAssistant takes those topic payloads and adds them as
entities in your HA setup.</p>
<h4 id="part-3-wikipedia-sensor">Part 3: Wikipedia Sensor</h4>
<p>The next eight lines are a fairly straightforward <a href="https://en.wikipedia.org/api/rest_v1/">API call to Wikipedia</a>. We start
out by passing the <code>science_name</code> into the URL. The rest of the flags that we are passing into the URL comes from Wikipedia&rsquo;s
Docs.
<code>url = f&quot;https://en.wikipedia.org/w/api.php?format=json&amp;action=query&amp;prop=extracts&amp;exintro&amp;explaintext&amp;redirects=1&amp;titles={science_name}&quot;</code></p>
<p>Once that&rsquo;s done we call it with <code>response.get(url)</code> and format it with <code>response.json()</code>. Wikipedia returns the json payload
with the top level of <code>query</code> (which was our action in the url ;) ), and we&rsquo;re looking for the value within that query.</p>
<p>All that&rsquo;s left is to take that query value and push it to HomeAssistant! We can do that with the <code>self.hassapi.set_state</code>
function. Within the parenthesis we define the name of the sensor (<code>sensor.birdnet_wiki</code>), what it&rsquo;s state should be (<code>on</code>),
and any attributes associated with the entity. Since we can&rsquo;t assign a long description to the basic status of the entity,
we&rsquo;re adding an attribute with the key of <code>description</code> and the value will be the wikipedia description garnered from the API
call.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span> url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://en.wikipedia.org/w/api.php?format=json&amp;action=query&amp;prop=extracts&amp;exintro&amp;explaintext&amp;redirects=1&amp;titles=</span><span style="color:#e6db74">{</span>science_name<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span> response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(url)
</span></span><span style="display:flex;"><span> response <span style="color:#f92672">=</span> response<span style="color:#f92672">.</span>json()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> value <span style="color:#f92672">in</span> response[<span style="color:#e6db74">&#39;query&#39;</span>][<span style="color:#e6db74">&#39;pages&#39;</span>]:
</span></span><span style="display:flex;"><span> wiki_desc <span style="color:#f92672">=</span> response[<span style="color:#e6db74">&#39;query&#39;</span>][<span style="color:#e6db74">&#39;pages&#39;</span>][value][<span style="color:#e6db74">&#39;extract&#39;</span>]
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>hassapi<span style="color:#f92672">.</span>set_state(<span style="color:#e6db74">&#34;sensor.birdnet_wiki&#34;</span>, state<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;on&#39;</span>,
</span></span><span style="display:flex;"><span> attributes <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;description&#34;</span>: wiki_desc})
</span></span></code></pre></div><h4 id="part-4-generate-picture-for-detection-optional">Part 4: Generate Picture for Detection (Optional)</h4>
<p>This part is optional but I noticed that BirdNET-Pi was already grabbing a Flickr Picture for it&rsquo;s front end, so I took the
code from the BirdNET code base and adjusted it a bit for my needs. This will work very similarly to the Wikipedia API call,
the main difference here being that you need an API key for Flickr. You can find more <a href="https://www.flickr.com/services/api/misc.api_keys.html">information here</a>.</p>
<p>Given Flickr&rsquo;s fairly robust API, by passing in the detected bird&rsquo;s common name, we get amazing results from the community of
various pictures of the same species of bird. Ever since I&rsquo;ve set this up, I&rsquo;ve not seen a mislabeled picture in my
dashboard!</p>
<p>The most confusion portion of this section is the <code>image_url</code> as you&rsquo;ll notice a bunch of <code>data[&quot;value&quot;]</code> strings at various
portions of the URL. The short answer to this is in the previous line with the <code>data</code> variable. A successful query has Flickr
returning a large payload of information. We&rsquo;re specifically using <a href="https://www.flickr.com/services/api/flickr.photos.search.html">this</a>
Flickr API endpoint. While you can pass a lot of variables for your needs, if you scroll down, you can see that the example
response contains multiple photos in a single response. We&rsquo;re passing <code>per_page=5</code> to limit some of those response items.
Left out of that response, though, is a one-stop-shop for a URL to that photo. Thankfully, Flickr can help us put together a
URL from the data in the response.</p>
<style type="text/css">
.box-shortcode {
padding: 1.6em;
padding-top: 1.4em;
line-height: 1.4em;
margin-top: 1em;
margin-bottom: 2em;
border-radius: 4px;
color: #444;
background: #f3ebe850;
}
.box-title {
margin: -18px -18px 12px;
padding: 4px 18px;
border-radius: 4px 4px 0 0;
font-weight: 700;
color: #fff;
background: #6ab0de;
}
.box-shortcode.warning .box-title {
background: #ff6b6b;
}
.box-shortcode.warning {
background: #ff6b6b4f;
}
.box-shortcode.info .box-title {
background: #0089e488;
}
.box-shortcode.info {
background: #0089e41c;
box-shadow: 3px 3px 5px #0089e410;
}
.box-shortcode.important .box-title {
background: #f7ec2c;
}
.box-shortcode.important {
background: #f7ec2c7d;
}
.box-shortcode.tip .box-title {
background: #a3ffa34d;
}
.box-shortcode.tip {
background: #a3ffa34d;
box-shadow: 3px 3px 5px #0089e410;
}
.icon-box {
display: inline-flex;
align-self: center;
margin-right: 8px;
}
.icon-box img,
.icon-box svg {
height: 1em;
width: 1em;
fill: currentColor;
}
.icon-box img,
.icon-box.baseline svg {
top: 0.125em;
position: relative;
}
.box-shortcode p {
margin-bottom: 0.6em;
}
.box-shortcode p:first-of-type {
display: inline;
}
.box-shortcode p:nth-of-type(2) {
margin-top: 0.6em;
}
.box-shortcode p:last-child {
margin-bottom: 0;
}
</style>
<svg width="0" height="0" display="none" xmlns="http://www.w3.org/2000/svg">
<symbol id="tip-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"/>
</symbol>
<symbol id="important-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</symbol>
<symbol id="warning-box" viewBox="0 0 576 512" preserveAspectRatio="xMidYMid meet">
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"/>
</symbol>
<symbol id="info-box" viewBox="0 0 512 512" preserveAspectRatio="xMidYMid meet">
<path
d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"/>
</symbol>
</svg><div class="box box-shortcode info" >
<span class="icon-box baseline">
<svg><use href="#info-box"></use></svg>
</span>
<p><em>Note: Full Transparency that I only learned about this after reading through BirdNET-Pi&rsquo;s code base. Full credit goes to
<a href="https://github.com/mcguirepr89">mcguirepr89</a>. For additional reference, here is Flickr&rsquo;s <a href="https://www.flickr.com/services/api/misc.urls.html">official page on construction
photo image URLS</a></em></p>
</div>
<p>With this response, we now have the variables we need to construct the URL to actually render the image. Those variables are:
Farm ID, Server ID, ID and Secret. I haven&rsquo;t yet looked into why we need &ldquo;farm&rdquo; when the official documentation doesn&rsquo;t state
anything about it.</p>
<p>Almost there! We now do the same as we did with the Wikipedia API response. We create a sensor in HomeAssistant! We&rsquo;re
calling this sensor <code>sensor.birdpic</code>, ensuring the <code>state=on</code>, and giving it the attributes of the <code>image_url</code> as garnered
from Flickr.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span> headers <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#39;User-Agent&#39;</span>: <span style="color:#e6db74">&#39;Python_Flickr/1.0&#39;</span>}
</span></span><span style="display:flex;"><span> flickr_api <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;enter_your_api_key&#34;</span>
</span></span><span style="display:flex;"><span> flickr_url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://www.flickr.com/services/rest/?method=flickr.photos.search&amp;api_key=</span><span style="color:#e6db74">{</span>flickr_api<span style="color:#e6db74">}</span><span style="color:#e6db74">&amp;text=</span><span style="color:#e6db74">{</span>common_name<span style="color:#e6db74">}</span><span style="color:#e6db74"> bird&amp;sort=relevance&amp;per_page=5&amp;media=photos&amp;format=json&amp;nojsoncallback=1&#34;</span>
</span></span><span style="display:flex;"><span> flickr_resp <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(url<span style="color:#f92672">=</span>flickr_url, headers<span style="color:#f92672">=</span>headers)
</span></span><span style="display:flex;"><span> data <span style="color:#f92672">=</span> flickr_resp<span style="color:#f92672">.</span>json()[<span style="color:#e6db74">&#34;photos&#34;</span>][<span style="color:#e6db74">&#34;photo&#34;</span>][<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> image_url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;https://farm&#39;</span><span style="color:#f92672">+</span>str(data[<span style="color:#e6db74">&#34;farm&#34;</span>])<span style="color:#f92672">+</span><span style="color:#e6db74">&#39;.static.flickr.com/&#39;</span><span style="color:#f92672">+</span>str(data[<span style="color:#e6db74">&#34;server&#34;</span>])<span style="color:#f92672">+</span><span style="color:#e6db74">&#39;/&#39;</span><span style="color:#f92672">+</span>str(data[<span style="color:#e6db74">&#34;id&#34;</span>])<span style="color:#f92672">+</span><span style="color:#e6db74">&#39;_&#39;</span><span style="color:#f92672">+</span>str(data[<span style="color:#e6db74">&#34;secret&#34;</span>])<span style="color:#f92672">+</span><span style="color:#e6db74">&#39;_n.jpg&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>hassapi<span style="color:#f92672">.</span>set_state(<span style="color:#e6db74">&#34;sensor.birdpic&#34;</span>, state<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;on&#39;</span>,
</span></span><span style="display:flex;"><span> attributes<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;image&#34;</span>: image_url})
</span></span></code></pre></div><h2 id="importing-mqtt-sensors-into-homeassistant">Importing MQTT Sensors into HomeAssistant</h2>
<p>Now that we have all the sensors defined and communicating via MQTT, we have one more step to import them into HomeAssistant.
<a href="https://www.home-assistant.io/integrations/mqtt/">This MQTT documentation</a> by HomeAssistant is good to read about if you
need a broker setup. I will not be going over the broker in this tutorial, but may add one in the future. I tend to like the
yaml configuration for HomeAssistant, so for the sake of this guide, I&rsquo;ll be referencing the <a href="https://www.home-assistant.io/integrations/mqtt/#manual-configured-mqtt-items">manual configuration of MQTT
items and sensors</a>.</p>
<p>To add the sensors from above, open up your <code>configuration.yaml</code> file in your favorite editor. You&rsquo;ll then want to add the
mqtt platform and domain:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">mqtt</span>:
</span></span><span style="display:flex;"><span> - { <span style="color:#f92672">domain }</span>:
</span></span></code></pre></div><p>For the BirdNet sensors, we will be using a single domain: <code>sensor</code>. Feel free to copy and paste my config from below, but
make sure the names of each entity align with your needs, syntax, and nomenclature/system.</p>
<p><strong>Full MQTT Sensors in Configuration.yml</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">mqtt</span>:
</span></span><span style="display:flex;"><span> <span style="color:#f92672">sensor</span>:
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;Bird Common Name&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">state_topic</span>: <span style="color:#e6db74">&#34;birdnet/sensors/common_name&#34;</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;Bird Science Name&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">state_topic</span>: <span style="color:#e6db74">&#34;birdnet/sensors/science_name&#34;</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;Bird Time Seen&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">state_topic</span>: <span style="color:#e6db74">&#34;birdnet/sensors/time_seen&#34;</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;Bird Date Seen&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">state_topic</span>: <span style="color:#e6db74">&#34;birdnet/sensors/date_seen&#34;</span>
</span></span><span style="display:flex;"><span> - <span style="color:#f92672">name</span>: <span style="color:#e6db74">&#34;Bird Confidence&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">state_topic</span>: <span style="color:#e6db74">&#34;birdnet/sensors/confidence&#34;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">value_template</span>: <span style="color:#e6db74">&#39;{{ (value|float(0) *100) | round(1) }}&#39;</span>
</span></span><span style="display:flex;"><span> <span style="color:#f92672">unit_of_measurement</span>: <span style="color:#e6db74">&#39;%&#39;</span>
</span></span></code></pre></div><p>You might be looking at the list above and wondering where the Flickr and Wikipedia Description entities are. They were
already created by the AppDaemon script! Specifically, <code>self.hassapi.set_state()</code> function will either update the state for
an exisiting entity or, if the entity doesn&rsquo;t exist, it will create a new one.</p>
<p>For the rest of the mqtt payloads, we need HomeAssistant to create them as they come in, which is why we add the above
code block to our HomeAssistant configuration file. To be clear, you <em>do not</em> need to add the Wikipedia and Flickr sensors to
HA&rsquo;s configuration file!</p>
<h2 id="adding-the-camera-entity">Adding the Camera entity</h2>
<p>Last but not least, we need to add a camera entity to ensure that the <code>sensor.birdpic</code> can actually be rendered visually.
It&rsquo;s really easy to add this sensor, so this should be quick. Here&rsquo;s how:</p>
<ol>
<li>In HomeAssistant, navigate to Settings &gt; Devices &amp; Services &gt; Integrations</li>
<li>Click &ldquo;+ Add Integration&rdquo; in the bottom right-hand corner. Alternatively, if you already have a camera integration
enabled, look for the &ldquo;Generic Camera&rdquo; card and click &ldquo;Add Entry&rdquo;</li>
<li>Name the sensor. In this case, I called it &ldquo;BirdPicturesfromFlickr&rdquo; and renamed the entity to <code>camera.birdnet_flickr</code>.</li>
<li>You should now see a &ldquo;Still Image URL&rdquo; as the first of a few options on the screen. Enter the following into the still
image field: <code>{{ state_attr('sensor.birdpic', 'image') }}</code> (This is the sensor we created in AppDaemon with the flickr url
as the attribute).</li>
<li>Stream Source and RTSP transport protocol can both be left blank.</li>
<li>Authentication - select &ldquo;digest&rdquo;.</li>
<li>Username and Password can be left blank.</li>
<li>Frame Rate - 2</li>
<li>Leave the rest of the check boxes unchecked and click Submit</li>
</ol>
<p>When you now click on the entity, you should see an image!</p>
<p><img loading="lazy" src="../posts/img/birdnet_camera_entity.png" alt="HomeAssistant BirdNET Camera Entity" />
</p>
<p>By this point, you should have successfully created 7 new sensors in HomeAssistant. In Part 2 of this article, we&rsquo;ll take a
look at Home Assistant, see what these sensors look like, and create a rudimentary dashboard.</p>
<h2 id="birdnet-appdaemon-script">Birdnet AppDaemon Script</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">import</span> time
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> requests
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">birdnet</span>(adbase<span style="color:#f92672">.</span>ADBase):
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">initialize</span>(self):
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>hassapi <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>get_plugin_api(<span style="color:#e6db74">&#34;HASS&#34;</span>)
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>adapi <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>get_ad_api()
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>mqttapi <span style="color:#f92672">=</span> self<span style="color:#f92672">.</span>get_plugin_api(<span style="color:#e6db74">&#34;MQTT&#34;</span>)
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>birdnet_mqtt <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;birdnet&#34;</span>
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>mqttapi<span style="color:#f92672">.</span>listen_event(
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>birdnet_message, <span style="color:#e6db74">&#34;MQTT_MESSAGE&#34;</span>, topic<span style="color:#f92672">=</span>self<span style="color:#f92672">.</span>birdnet_mqtt
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">birdnet_message</span>(self, event_name, data, kwargs):
</span></span><span style="display:flex;"><span> pre_split <span style="color:#f92672">=</span> data[<span style="color:#e6db74">&#34;payload&#34;</span>]
</span></span><span style="display:flex;"><span> common_name <span style="color:#f92672">=</span> pre_split<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#39;,&#39;</span>)[<span style="color:#ae81ff">0</span>]<span style="color:#f92672">.</span>strip()
</span></span><span style="display:flex;"><span> science_name <span style="color:#f92672">=</span> pre_split<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#39;,&#39;</span>)[<span style="color:#ae81ff">1</span>]<span style="color:#f92672">.</span>strip()
</span></span><span style="display:flex;"><span> date_seen <span style="color:#f92672">=</span> pre_split<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#39;,&#39;</span>)[<span style="color:#ae81ff">2</span>]<span style="color:#f92672">.</span>strip()
</span></span><span style="display:flex;"><span> time_seen <span style="color:#f92672">=</span> pre_split<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#39;,&#39;</span>)[<span style="color:#ae81ff">3</span>]<span style="color:#f92672">.</span>strip()
</span></span><span style="display:flex;"><span> week_seen <span style="color:#f92672">=</span> pre_split<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#39;,&#39;</span>)[<span style="color:#ae81ff">4</span>]<span style="color:#f92672">.</span>strip()
</span></span><span style="display:flex;"><span> confidence <span style="color:#f92672">=</span> pre_split<span style="color:#f92672">.</span>split(<span style="color:#e6db74">&#39;,&#39;</span>)[<span style="color:#ae81ff">5</span>]<span style="color:#f92672">.</span>strip()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># print(f&#34;A {common_name} was seen on {date_seen} at {time_seen}. Confidence is {confidence}.&#34;)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>mqttapi<span style="color:#f92672">.</span>mqtt_publish(<span style="color:#e6db74">&#34;birdnet/sensors/common_name&#34;</span>, common_name)
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>mqttapi<span style="color:#f92672">.</span>mqtt_publish(<span style="color:#e6db74">&#34;birdnet/sensors/science_name&#34;</span>, science_name)
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>mqttapi<span style="color:#f92672">.</span>mqtt_publish(<span style="color:#e6db74">&#34;birdnet/sensors/time_seen&#34;</span>, time_seen)
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>mqttapi<span style="color:#f92672">.</span>mqtt_publish(<span style="color:#e6db74">&#34;birdnet/sensors/date_seen&#34;</span>, date_seen)
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>mqttapi<span style="color:#f92672">.</span>mqtt_publish(<span style="color:#e6db74">&#34;birdnet/sensors/confidence&#34;</span>, confidence)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://en.wikipedia.org/w/api.php?format=json&amp;action=query&amp;prop=extracts&amp;exintro&amp;explaintext&amp;redirects=1&amp;titles=</span><span style="color:#e6db74">{</span>science_name<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span> response <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(url)
</span></span><span style="display:flex;"><span> response <span style="color:#f92672">=</span> response<span style="color:#f92672">.</span>json()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">for</span> value <span style="color:#f92672">in</span> response[<span style="color:#e6db74">&#39;query&#39;</span>][<span style="color:#e6db74">&#39;pages&#39;</span>]:
</span></span><span style="display:flex;"><span> wiki_desc <span style="color:#f92672">=</span> response[<span style="color:#e6db74">&#39;query&#39;</span>][<span style="color:#e6db74">&#39;pages&#39;</span>][value][<span style="color:#e6db74">&#39;extract&#39;</span>]
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>hassapi<span style="color:#f92672">.</span>set_state(<span style="color:#e6db74">&#34;sensor.birdnet_wiki&#34;</span>, state<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;on&#39;</span>,
</span></span><span style="display:flex;"><span> attributes <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#34;description&#34;</span>: wiki_desc})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> headers <span style="color:#f92672">=</span> {<span style="color:#e6db74">&#39;User-Agent&#39;</span>: <span style="color:#e6db74">&#39;Python_Flickr/1.0&#39;</span>}
</span></span><span style="display:flex;"><span> flickr_api <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;enter_your_api_key&#34;</span>
</span></span><span style="display:flex;"><span> flickr_url <span style="color:#f92672">=</span> <span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;https://www.flickr.com/services/rest/?method=flickr.photos.search&amp;api_key=</span><span style="color:#e6db74">{</span>flickr_api<span style="color:#e6db74">}</span><span style="color:#e6db74">&amp;text=</span><span style="color:#e6db74">{</span>common_name<span style="color:#e6db74">}</span><span style="color:#e6db74"> bird&amp;sort=relevance&amp;per_page=5&amp;media=photos&amp;format=json&amp;nojsoncallback=1&#34;</span>
</span></span><span style="display:flex;"><span> flickr_resp <span style="color:#f92672">=</span> requests<span style="color:#f92672">.</span>get(url<span style="color:#f92672">=</span>flickr_url, headers<span style="color:#f92672">=</span>headers)
</span></span><span style="display:flex;"><span> data <span style="color:#f92672">=</span> flickr_resp<span style="color:#f92672">.</span>json()[<span style="color:#e6db74">&#34;photos&#34;</span>][<span style="color:#e6db74">&#34;photo&#34;</span>][<span style="color:#ae81ff">0</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> image_url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;https://farm&#39;</span><span style="color:#f92672">+</span>str(data[<span style="color:#e6db74">&#34;farm&#34;</span>])<span style="color:#f92672">+</span><span style="color:#e6db74">&#39;.static.flickr.com/&#39;</span><span style="color:#f92672">+</span>str(data[<span style="color:#e6db74">&#34;server&#34;</span>])<span style="color:#f92672">+</span><span style="color:#e6db74">&#39;/&#39;</span><span style="color:#f92672">+</span>str(data[<span style="color:#e6db74">&#34;id&#34;</span>])<span style="color:#f92672">+</span><span style="color:#e6db74">&#39;_&#39;</span><span style="color:#f92672">+</span>str(data[<span style="color:#e6db74">&#34;secret&#34;</span>])<span style="color:#f92672">+</span><span style="color:#e6db74">&#39;_n.jpg&#39;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>hassapi<span style="color:#f92672">.</span>set_state(<span style="color:#e6db74">&#34;sensor.birdpic&#34;</span>, state<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;on&#39;</span>,
</span></span><span style="display:flex;"><span> attributes<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;image&#34;</span>: image_url})
</span></span></code></pre></div><style>
.box-shortcode {
color: #e8e8e8;
border: none;
}
.post-content img {
margin: auto
}
</style>
]]></content:encoded>
</item>
<item>
<title>Pushing a Single Local Git Repo to Multiple Remote Repos</title>
<link>/posts/multiple_git_remotes.html</link>
<pubDate>Fri, 22 Sep 2023 15:07:10 -0400</pubDate>
<guid>/posts/multiple_git_remotes.html</guid>
<description>Learn one way to push your git changes to multiple remote repositories.</description>
<content:encoded><![CDATA[<h2 id="why-push-to-multiple-repos">Why push to multiple repos?</h2>
<p>Do want to use both Github &amp; and a Self-hosted Git Repo? Here&rsquo;s how I&rsquo;ve been doing it!</p>
<p>I really enjoy self-hosting services that I use everyday. One of those includes a git-style version control software. In my
case, I&rsquo;ve been running <a href="https://gitea.com/">Gitea</a> for a few years now and have been really satisfied with everything (except
for that one time that an update broke all my templates).</p>
<p>At the same time, there&rsquo;s the entire social element that comes with Github along with having your public repositories
available in a place that other developers are already spending time on. Instead of adding, committing, commenting, and
pushing on two different repos, here&rsquo;s how I run all those commands just once and push it to both repos.</p>
<p><em>Note: An import git note to remember is that you can only <em>push</em> to multiple remote repositories. You&rsquo;ll have to select
which repo you want to be the main pull repository. Have this be <code>remote-url-one</code> in the below instructions.</em></p>
<h2 id="command-line-instructions">Command Line Instructions</h2>
<p>These instructions come after you initialize the repo in your directory. Make sure you have both of your remote git URLs
handy at this point!</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-zsh" data-lang="zsh"><span style="display:flex;"><span>git remote add <span style="color:#f92672">{{</span> remote-name <span style="color:#f92672">}}</span> <span style="color:#f92672">{{</span> remote-url-one <span style="color:#f92672">}}</span>
</span></span><span style="display:flex;"><span>git remote set-url --add --push <span style="color:#f92672">{{</span> remote-name <span style="color:#f92672">}}</span> <span style="color:#f92672">{{</span> remote-url-one <span style="color:#f92672">}}</span>
</span></span><span style="display:flex;"><span>git remote set-url --add --push <span style="color:#f92672">{{</span> remote-name <span style="color:#f92672">}}</span> <span style="color:#f92672">{{</span> remote-url-two <span style="color:#f92672">}}</span>
</span></span></code></pre></div><p>To confirm that everything worked as expected, run <code>git remote -v</code> to check your remote repos. You should see one repo in
there twice, once for <code>(push)</code> and once for <code>(fetch)</code>.</p>
<p>I use the remote name &ldquo;all&rdquo; for multiple repos, so here&rsquo;s what my <code>git remote -v</code> returns:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-zsh" data-lang="zsh"><span style="display:flex;"><span>&gt; git remote -v
</span></span><span style="display:flex;"><span>all https://git.rsmsn.co/Normanras/rsmsn_blog.git <span style="color:#f92672">(</span>fetch<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>all https://git.rsmsn.co/Normanras/rsmsn_blog.git <span style="color:#f92672">(</span>push<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>all https://github.com/Normanras/rsmsn_blog.git <span style="color:#f92672">(</span>push<span style="color:#f92672">)</span>
</span></span><span style="display:flex;"><span>all https://git.rsmsn.co/Normanras/rsmsn_ddblog.git <span style="color:#f92672">(</span>push<span style="color:#f92672">)</span>
</span></span></code></pre></div><p>To now push to your repositories, after adding and committing run <code>git push {{ remote-name }} --all</code>. My command is
<code>git push all --all</code> (see why I use all, now?)</p>
<p>Here&rsquo;s the man page description on the <code>--all</code> flag:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-zsh" data-lang="zsh"><span style="display:flex;"><span>--all
</span></span><span style="display:flex;"><span> Push all branches <span style="color:#f92672">(</span>i.e. refs under refs/heads/<span style="color:#f92672">)</span>; cannot be used
</span></span><span style="display:flex;"><span> Instead of naming each ref to push, specifies that all refs under
</span></span><span style="display:flex;"><span> end, locally updated refs will be force updated on the remote end,
</span></span><span style="display:flex;"><span> Do everything except actually send the updates.
</span></span><span style="display:flex;"><span> same as prefixing all refs with a colon.
</span></span></code></pre></div><p>And that&rsquo;s it! You should be able to push everything to both of your repos fairly easily now with this new set commands.</p>
]]></content:encoded>
</item>
<item>
<title>Trouble Hosting Hugo with Nginx</title>
<link>/posts/hosting_hugo_troubles.html</link>
<pubDate>Wed, 20 Sep 2023 11:33:22 -0400</pubDate>
<guid>/posts/hosting_hugo_troubles.html</guid>
<description>Learn one way to push your git changes to multiple remote repositories.</description>
<content:encoded><![CDATA[<h2 id="intro">Intro</h2>
<p>For the last 3 days, I have been spending a few hours after working trying to figure out why my brand new Hugo site was not
loading correctly on my sub-domain. For context, I use Nginx to host all my apps and servers, most of them using reverse
proxy protocols such as <code>$proxy_host</code>, <code>$forward_scheme</code>, and <code>$port</code>. There are a few more and I&rsquo;m happy to share some
reverse proxy nginx config files. See my post on <a href="/posts/npm_to_nginx_tutorial.html">moving from NPM to Nginx</a> for more information.</p>
<p>Once I got the basic idea of this blog up and running, I ran <code>hugo</code> and then used <code>scp</code> to send the files to my nginx host&rsquo;s
public folder. Despite <code>index.html</code> and all the CSS files being in the right spots, I kept getting a few repetitive errors
from nginx - either in my browser&rsquo;s console or in nginx&rsquo;s log files. I swore I read through every StackOverflow or personal
blog post I could find. In fact, the next day, I would sit back down after work to debug and I would continue to find the
same sites and posts I found the day before! It was getting frustrating.</p>
<p>Wouldn&rsquo;t you know&hellip; it was one of the simplest solutions that got it all working. Here&rsquo;s a breakdown of what I was seeing
and my hypothesis.</p>
<h2 id="errors">Errors</h2>
<p><em>Console Errors:</em></p>
<ul>
<li>Incorrect MIME type &ndash;&gt; <code>css</code> files being set as <code>text/html</code> type.</li>
<li>500/502 Errors when trying to load javascript files</li>
<li>500 Errors when trying to load child pages.</li>
</ul>
<p><em>Nginx Log Errors for this server:</em></p>
<ul>
<li><code>[error] 1147432#1147432: *84013 invalid URL prefix in &quot;://:/favicon-16x16.png&quot;</code></li>
<li><code>[warn] 1147432#1147432: *84013 using uninitialized &quot;port&quot; variable</code></li>
</ul>
<p><em>Nginx <code>error.log</code> errors:</em></p>
<ul>
<li><code>[error] 1118832#1118832: *77105 directory index of &quot;/var/www/html/&quot; is forbidden</code></li>
</ul>
<p>I thought I had tried everything, but it was a rip and replace from a single blog post that solved it for me. Some of the
things I tried include the following. _Note: when I mention &rsquo;nginx config file&rsquo; below, I am referring to the specific file
for this subdomain. I have a single global nginx.conf and then individual files for each subdomain/proxy host.</p>
<ul>
<li>Editing <code>index.html</code> to ensure that any referenced file had an explicit mime type associated with it.</li>
<li>Included <code>include { full path }/mime.types</code> in the specific nginx config file.</li>
<li>Included specific <code>location ~ \.(css|js)$ {</code> sections in my nginx config file.</li>
<li>Tried assigning the <code>$forward_scheme</code>, <code>$host</code>, and <code>$port</code> variables (similar to a reverse proxy host).</li>
<li>Removing any SSL references &lt;&ndash; This caused similar behavior but different errors! I thought I was making progress&hellip;🫥</li>
<li>Switched out the variables of <code>root</code> and <code>alias</code> between my <code>server</code> and <code>location</code> blocks.</li>
<li>Started with something super simple, such as the suggestions from <a href="https://gideonwolfe.com/posts/sysadmin/hugonginx/">Gideon Wolfe&rsquo;s Block</a>.</li>
</ul>
<p>If you clicked on the link I just shared, you&rsquo;ll see that Gideon&rsquo;s setup is quite similar to the one that I finally got to
work. My thought there is that my errors were less about the nginx config setup from within the file, and instead it&rsquo;s very
possible that I set incorrect directory permissions after transferring all the public files to the web server. Gideon&rsquo;s blog
was the very first I clicked on, so I owe them my thanks since their site was the entry point to figuring all this out!
You can find a list of all the blogs I stumbled upon in this weird and fluctuating journey of doing something as simple as
sharing my static files on an nginx web server.</p>
<h2 id="solution">Solution</h2>
<p>In the end, <a href="https://newbs.rocks/posts/hugo-setup/">newbs.rocks blog post</a> on setting up Hugo with nginx provided me a config
example that worked for my setup. I think part of what happened was that as I was cycling through all the blog posts and
StackOverflow posts, I would remove one or two lines (usually the one or two I changed from the previous post&rsquo;s
recommendations) but in doing that, was making more of a mess for myself, burying the error even more deeply. By replacing
everything, I&rsquo;ve brought it back to a manageable place.</p>
<p>You&rsquo;ll also notice in my <a href="/posts/hosting_hugo_troubles.html#nginx-config">final config file</a>, I was able to add back in the <a href="https://www.authelia.com/">Authelia</a> snippets, paths for the SSL certs, and a few other items that connect nginx to the rest of my infrastructure.</p>
<p>If you&rsquo;ve stumbled upon this, I hope it helps you figure out your Hugo/Nginx issues! I definitely saw a lot of people posting
in Hugo&rsquo;s Discourse asking about <a href="https://discourse.gohugo.io/search?expanded=true&amp;q=mime%20type">mime type errors</a>, so it is
very likely that whatever you&rsquo;re facing isn&rsquo;t isolated to just you.</p>
<p>Now that I have this up and running, I need to write (and post!) a script that will pull from my repo, change directory
ownership, and reload nginx.</p>
<h2 id="resources">Resources</h2>
<h3 id="nginx-config">Working Nginx Config File</h3>
<pre tabindex="0"><code class="language-config" data-lang="config"># ------------------------------------------------------------
# selfhosted.rsmsn.co
# ------------------------------------------------------------
server {
listen 80;
listen [::]:80;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name selfhosted.rsmsn.co;
root /var/www/html/public/;
index /index.html;
# Force SSL
include conf.d/include/force-ssl.conf;
# Custom SSL
ssl_certificate custom_ssl/npm-3/fullchain.pem;
ssl_certificate_key custom_ssl/npm-3/privkey.pem;
access_log /var/log/nginx/blog.log combined;
error_log /var/log/nginx/blog_error.log warn;
include snippets/authelia-location.conf;
location / {
try_files $uri $uri/ =404;
include snippets/authelia-authrequest.conf;
}
}
</code></pre><h3 id="blogs-and-sites">Blogs and Sites</h3>
<ul>
<li><a href="https://gideonwolfe.com/posts/sysadmin/hugonginx/">Gideon Wolfe - Deploying a static Hugo site with NGINX</a></li>
<li><a href="https://pvera.net/posts/create-site-nginx-hugo/">Pvera - Deploying a simple static website using Nginx and Hugo</a></li>
<li><a href="https://discourse.gohugo.io/t/help-deploying-with-nginx/12609">Hugo Support thread on Discourse</a></li>
<li><a href="https://bravoslab.com/post/static-website-wirh-hugo-io-and-ngnix/">BravosLab - Static website with Hugo and Nginx</a></li>
<li><a href="https://blog.cavelab.dev/2021/02/hugo-build-deploy-to-nginx/">Cavelab - Hugo Build deploy to Nginx</a></li>
<li><a href="https://river.me/blog/hugo-non-root-location/">River - Serving Hugo from a non-root location with Nginx</a></li>
</ul>
]]></content:encoded>
</item>
<item>
<title>Tutorial: Move from NginxProxyManager to Nginx</title>
<link>/posts/npm_to_nginx_tutorial.html</link>
<pubDate>Sat, 05 Aug 2023 15:23:51 -0500</pubDate>
<guid>/posts/npm_to_nginx_tutorial.html</guid>
<description>Learn one way to push your git changes to multiple remote repositories.</description>
<content:encoded><![CDATA[<h2 id="goal">Goal</h2>
<p>A Tutorial Repo for migrating your Nginx Proxy Manager proxy setup to Nginx. I wrote this originally for <a href="https://www.reddit.com/r/selfhosted/comments/15j4v80/minitutorial_migrating_from_nginx_proxy_manager/">this reddit
post</a> and to post this <a href="https://github.com/Normanras/Npm_to_Nginx">my Github profile</a>. Thought my website would also be a good place to share it for any passers-by.</p>
<p>To give clear instructions to help users migrate from using <a href="https://nginxproxymanager.com/">Nginx Proxy Manager</a> (NPM) to standard <a href="https://docs.nginx.com/">Nginx</a>. This tutorial is not exhaustive and there are many other implementations of this transition. I would recommend checking out the many Nginx <a href="http://nginx.org/en/docs/">Documentation Sites</a> and tutorials to learn more.</p>
<h2 id="introduction">Introduction</h2>
<p>If you&rsquo;re anything like me and you got into the self-hosted/homelab/diy game sometime within the last 5 years, you&rsquo;ve likely been recommended to use Nginx Proxy Manager as one of the choice Reverse Proxy services. If you&rsquo;ve also been paying attention to various self-hosted communities, you may have also come across Christian Lempa&rsquo;s Video on <a href="https://youtu.be/uaixCKTaqY0">trusting smaller self hosted projects and tools</a>.</p>
<p><em>Spoilers:</em> He roasts NPM in his video and towards the end says he won&rsquo;t be using NPM anymore. He also, perhaps purposely, doesn&rsquo;t share which tool he will be migrating to.</p>
<p>Whether you follow Christian away from NPM or not, it dawned on me that while NPM is using a very trusted web server and reverse proxy under the hood, I hadn&rsquo;t taken the time to understand how an Nginx Config actually worked. Since NPM was already creating most of the files for Nginx, I got to reading through all the files and reworking them so that I could begin using Nginx without the NPM gui.</p>
<p><em>Contributing: This is not all encompassing of Nginx possibilities. Including instructions for various installation methods, using OpenResty, and any other migrations or use cases would help the community. If you&rsquo;d like to add in additional information on how to migrate from NPM to Nginx, that is welcome. Simply submit a PR with your steps.</em></p>
<h2 id="tldr---quick-steps">TL;DR - Quick Steps</h2>
<ol>
<li>
<p>Copy the following contents (including sub-directories) from the NPM <code>/data/nginx</code> directory to the Nginx <code>/etc/nginx</code> folder:</p>
<ul>
<li><code>proxy_hosts</code> &gt; <code>sites-available</code></li>
<li><code>conf.d</code> &gt; <code>conf.d</code></li>
<li><code>snippets</code> &gt; <code>snippets</code></li>
<li><code>custom_ssl</code> &gt; <code>custom_ssl</code> (if applicable)</li>
</ul>
</li>
<li>
<p>Edit each file in your <code>sites-available</code> directory and update the paths. Most will change from <code>/data/nginx/</code> to <code>/etc/nginx</code>.</p>
</li>
<li>
<p>Edit your <code>nginx.conf</code> file and ensure the following two paths are there:</p>
<ul>
<li><code>include /etc/nginx/conf.d/*.conf;</code> and <code>include /etc/nginx/sites-enabled/*;</code></li>
</ul>
</li>
<li>
<p>Symlink the proxy host files in <code>sites-available</code> to <code>sites-enabled</code></p>
<ul>
<li><code>ln -s * ./sites-enabled</code></li>
</ul>
</li>
<li>
<p>Test your changes with <code>nginx -t</code>. Make appropriate changes if there are error messages.</p>
</li>
</ol>
<h2 id="pre-requisites--assumptions">Pre-requisites &amp; Assumptions</h2>
<p>I am using an Ubuntu VM with NPM and it&rsquo;s db as a Docker Container while Nginx is installed natively on the machine. You don&rsquo;t have to use this setup exactly, but I am making a few assumptions as to what you should have access to before you begin. I am also using custom SSL certs, but theoretically, the transition should be the same when using Lets Encrypt.</p>
<p>I&rsquo;ve added some example files to show before and after changes to this repo and outlined file trees below.</p>
<ul>
<li>You understand the basics of what a Reverse Proxy is doing and are sticking with some stock settings (like exposing port 80 an 443).</li>
<li>You&rsquo;ve installed <a href="https://nginxproxymanager.com/setup/">NPM</a> and <a href="https://www.nginx.com/resources/wiki/start/topics/tutorials/install/">Nginx</a> using your preferred method.</li>
<li>You have access to both NPMs file tree and Nginx&rsquo;s.</li>
<li>If using NPM in docker, make sure you&rsquo;ve mapped a local volume on the host to the container.</li>
<li>My setup using docker-compose is the following: <code>/user/nginx/data:/data</code>.</li>
<li>Know where your Nginx files are. If using docker, same as above, make sure your container directories are mapped to the host.</li>
<li>For a linux install, they should be accessible at <code>/etc/nginx</code>.</li>
<li>You know how to edit files at the command line using <code>nano</code>, <code>vi</code>, <code>vim</code>, <code>neovim</code>, <code>emacs</code>, or something else.</li>
</ul>
<h2 id="nginx-files">Nginx Files</h2>
<p>Nginx uses the <code>nginx.conf</code> file and within that file, it will include your proxy files. These exist under <code>./nginx/sites-enabled/</code>. In the main <code>nginx.conf</code> file, the line <code>include /etc/nginx/sites-enabled/*;</code> will bring in those files to the config file, making the proxies accessible.</p>
<h2 id="how-to-transition---detailed-version">How to Transition - Detailed Version</h2>
<ol>
<li>
<p>Since NPM uses Nginx under the hood, they are both, by default, going to try and use ports 80 and 443 to serve up your apps and content. Turn off both systems.</p>
<ul>
<li>Docker: <code>docker stop [app_container, db_container]</code></li>
<li>Systemd: <code>systemctl stop nginx</code></li>
</ul>
</li>
<li>
<p>Copy your <code>proxy_host</code> (NPM) files to the <code>sites-available</code> (Nginx) folder.
<code>cp -r /user/nginx/data/nginx/proxy_hosts/* /etc/nginx/sites-available/</code></p>
</li>
<li>
<p>Nginx doesn&rsquo;t really care what the files are called, but NPM numbers them based on the order in which you added them in the GUI. I find it better to rename them to what service they actually serve up for easier identification later.</p>
</li>
<li>
<p>Copy your <code>custom_ssl</code> folder from NPM to the <code>custom_ssl</code> folder in Nginx. See the following step for the various default paths in both systems.</p>
<ul>
<li><code>cp -r /user/nginx/data/custom_ssl/* /etc/nginx/custom_ssl/</code></li>
</ul>
</li>
<li>
<p>Copy the <code>conf.d</code> folder from NPM to the <code>conf.d</code> folder in Nginx. <em>Note: For some reason, not all of the files in
the proxy files were actually in my <code>conf.d</code> directory. If you&rsquo;re missing any files, please download and/or copy and
paste them from the <a href="https://github.com/NginxProxyManager/nginx-proxy-manager/tree/fa851b61da3fe3726d1a04c25e69d36e79edea2d/docker/rootfs/etc/nginx/conf.d/include">NPM Repo</a></em></p>
<ul>
<li><code>cp -r /user/nginx/data/nginx/conf.d /etc/nginx/</code></li>
</ul>
</li>
<li>
<p>If you had any additional files included in the Advanced section of an NPM Proxy Host, make sure you copy them over. For my setup and this tutorial, they were all located in the <code>snippets</code> directory.</p>
<ul>
<li><code>cp -r /user/nginx/data/nginx/snippets/* /etc/nginx/snippets/</code></li>
</ul>
</li>
<li>
<p>There are a number of lines that need to be updated in each proxy configuration file to make them work with Nginx. I&rsquo;ve placed additional comments in <a href="./proxy_host/npm_proxy.conf"><code>./proxy_host/npm_proxy.conf</code></a> file. The line changes are the following:</p>
<ol>
<li>
<p>Custom SSL path:</p>
<ul>
<li>NPM path: <code>/data/custom_ssl...</code></li>
<li>Nginx path: <code>/etc/nginx/custom_ssl...</code></li>
</ul>
</li>
<li>
<p>conf.d:</p>
<ul>
<li>The paths should remain the same. However, if you changed the path for <code>conf.d</code> in Nginx and differed in step 5, above, make sure you use the correct path.</li>
</ul>
</li>
<li>
<p>Access &amp; Error Logs</p>
<ul>
<li>NPM path: <code>/data/logs/...</code></li>
<li>Nginx path: <code>/var/log/nginx/...</code></li>
</ul>
</li>
</ol>
</li>
<li>
<p>Double Check all your paths! If this is your first time using Nginx, make sure every directory is correct! Save your work.</p>
</li>
<li>
<p>Navigate to the <code>nginx.conf</code> file which is located at <code>/etc/nginx/</code>. You can see the one I am using in this repo.</p>
</li>
<li>
<p>Make sure that you have the following two lines in the main conf file and that they are pointing to the appropriate directories in the nginx directory path.</p>
<ul>
<li><code>include /etc/nginx/conf.d/*.conf;</code> and <code>include /etc/nginx/sites-enabled/*;</code></li>
</ul>
</li>
<li>
<p>You&rsquo;ll notice that you ensured there was a <code>sites-enabled</code> directory in the configuration file, but you changed all your proxy host config files in <code>sites-available</code>! Good eye, all that&rsquo;s left is to symlink the files to <code>sites-enabled</code> so that nginx can start using them.</p>
</li>
<li>
<p>To symlink the available proxy files run the following command within the <code>sites-available</code> directory:</p>
<ul>
<li><code>ln -s * ./sites-enabled</code></li>
</ul>
</li>
<li>
<p>Once you&rsquo;re confident that you&rsquo;ve done all the above correctly, you can test your setup using nginx command and flags. While in the directory with your <code>nginx.conf</code> file - usually <code>/etc/nginx</code> - run the following command: <code>nginx -t</code>.</p>
</li>
<li>
<p>If all is working as expected you should see the below output. If it returns any errors, fix them appropriately. It will usually tell you what line is throwing the error. In this case, there&rsquo;s a high likelihood that it will be path error.</p>
</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
</span></span><span style="display:flex;"><span>nginx: configuration file /etc/nginx/nginx.conf test is successful
</span></span></code></pre></div><p>And that&rsquo;s it! You can now restart your nginx service on the host and access all your sites just as if you were using Nginx Proxy Manager! Make sure you take a look at your logs and system&rsquo;s status should nginx fail to start.</p>
<h2 id="additional-informationappendix">Additional Information/Appendix</h2>
<h3 id="file-trees-for-npm-in-container-and-nginx-on-host">File Trees for NPM (in container) and Nginx (on host)</h3>
<p><em>I did not expand every directory in these trees. Only the ones that are pertinent for reference in this tutorial.</em></p>
<h3 id="nginx">NGINX</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>├── conf.d
</span></span><span style="display:flex;"><span>│   └── include
</span></span><span style="display:flex;"><span>│   ├── assets.conf
</span></span><span style="display:flex;"><span>│   ├── block-exploits.conf
</span></span><span style="display:flex;"><span>│   ├── force-ssl.conf
</span></span><span style="display:flex;"><span>│   ├── ip_ranges.conf
</span></span><span style="display:flex;"><span>│   ├── proxy.conf
</span></span><span style="display:flex;"><span>│   └── resolvers.conf
</span></span><span style="display:flex;"><span>├── custom_ssl
</span></span><span style="display:flex;"><span>│   ├── npm-1
</span></span><span style="display:flex;"><span>│   │   ├── fullchain.pem
</span></span><span style="display:flex;"><span>│   │   └── privkey.pem
</span></span><span style="display:flex;"><span>│   ├── npm-2
</span></span><span style="display:flex;"><span>│   │   ├── fullchain.pem
</span></span><span style="display:flex;"><span>│   │   └── privkey.pem
</span></span><span style="display:flex;"><span>│   └── npm-3
</span></span><span style="display:flex;"><span>│   ├── fullchain.pem
</span></span><span style="display:flex;"><span>│   └── privkey.pem
</span></span><span style="display:flex;"><span>├── fastcgi.conf
</span></span><span style="display:flex;"><span>├── fastcgi_params
</span></span><span style="display:flex;"><span>├── koi-utf
</span></span><span style="display:flex;"><span>├── koi-win
</span></span><span style="display:flex;"><span>├── mime.types
</span></span><span style="display:flex;"><span>├── modules-available
</span></span><span style="display:flex;"><span>├── modules-enabled
</span></span><span style="display:flex;"><span>├── nginx.conf
</span></span><span style="display:flex;"><span>├── proxy_params
</span></span><span style="display:flex;"><span>├── scgi_params
</span></span><span style="display:flex;"><span>├── sites-available
</span></span><span style="display:flex;"><span>│   ├── auth.conf
</span></span><span style="display:flex;"><span>│   ├── bitwarden.conf
</span></span><span style="display:flex;"><span>│   ├── codehub.conf
</span></span><span style="display:flex;"><span>│   ├── default.backup
</span></span><span style="display:flex;"><span>│   ├── files.conf
</span></span><span style="display:flex;"><span>│   ├── notes.conf
</span></span><span style="display:flex;"><span>│   ├── photos.conf
</span></span><span style="display:flex;"><span>│   ├── rsmsn-root.conf
</span></span><span style="display:flex;"><span>│   ├── wordle.conf
</span></span><span style="display:flex;"><span>│   └── wordle-it.conf
</span></span><span style="display:flex;"><span>├── sites-enabled
</span></span><span style="display:flex;"><span>│   ├── auth.conf -&gt; /etc/nginx/sites-available/auth.conf
</span></span><span style="display:flex;"><span>│   ├── bitwarden.conf -&gt; /etc/nginx/sites-available/bitwarden.conf
</span></span><span style="display:flex;"><span>│   ├── codehub.conf -&gt; /etc/nginx/sites-available/codehub.conf
</span></span><span style="display:flex;"><span>│   ├── files.conf -&gt; /etc/nginx/sites-available/files.conf
</span></span><span style="display:flex;"><span>│   ├── notes.conf -&gt; /etc/nginx/sites-available/notes.conf
</span></span><span style="display:flex;"><span>│   ├── photos.conf -&gt; /etc/nginx/sites-available/photos.conf
</span></span><span style="display:flex;"><span>│   ├── wordle.conf -&gt; /etc/nginx/sites-available/wordle.conf
</span></span><span style="display:flex;"><span>│   └── wordle-it.conf -&gt; /etc/nginx/sites-available/wordle-it.conf
</span></span><span style="display:flex;"><span>├── snippets
</span></span><span style="display:flex;"><span>│   ├── authelia-authrequest-basic.conf
</span></span><span style="display:flex;"><span>│   ├── authelia-authrequest.conf
</span></span><span style="display:flex;"><span>│   ├── authelia-authrequest-detect.conf
</span></span><span style="display:flex;"><span>│   ├── authelia-location-basic.conf
</span></span><span style="display:flex;"><span>│   ├── authelia-location.conf
</span></span><span style="display:flex;"><span>│   ├── authelia-location-detect.conf
</span></span><span style="display:flex;"><span>│   ├── fastcgi-php.conf
</span></span><span style="display:flex;"><span>│   ├── proxy.conf
</span></span><span style="display:flex;"><span>│   └── snakeoil.conf
</span></span><span style="display:flex;"><span>├── uwsgi_params
</span></span><span style="display:flex;"><span>└── win-utf
</span></span></code></pre></div><h3 id="npm">NPM</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>├── data
</span></span><span style="display:flex;"><span>│   ├── access
</span></span><span style="display:flex;"><span>│   ├── custom_ssl
</span></span><span style="display:flex;"><span>│   │   ├── npm-1
</span></span><span style="display:flex;"><span>│   │   │   ├── fullchain.pem
</span></span><span style="display:flex;"><span>│   │   │   └── privkey.pem
</span></span><span style="display:flex;"><span>│   │   ├── npm-2
</span></span><span style="display:flex;"><span>│   │   │   ├── fullchain.pem
</span></span><span style="display:flex;"><span>│   │   │   └── privkey.pem
</span></span><span style="display:flex;"><span>│   │   └── npm-3
</span></span><span style="display:flex;"><span>│   │   ├── fullchain.pem
</span></span><span style="display:flex;"><span>│   │   └── privkey.pem
</span></span><span style="display:flex;"><span>│   ├── keys.json
</span></span><span style="display:flex;"><span>│   ├── letsencrypt-acme-challenge
</span></span><span style="display:flex;"><span>│   ├── logs
</span></span><span style="display:flex;"><span>│   ├── mysql
</span></span><span style="display:flex;"><span>│   └── nginx
</span></span><span style="display:flex;"><span>│   ├── custom
</span></span><span style="display:flex;"><span>│   ├── dead_host
</span></span><span style="display:flex;"><span>│   ├── default_host
</span></span><span style="display:flex;"><span>│   ├── default_www
</span></span><span style="display:flex;"><span>│   ├── dummycert.pem
</span></span><span style="display:flex;"><span>│   ├── dummykey.pem
</span></span><span style="display:flex;"><span>│   ├── proxy_host
</span></span><span style="display:flex;"><span>│   │   ├── 10.conf
</span></span><span style="display:flex;"><span>│   │   ├── 11.conf
</span></span><span style="display:flex;"><span>│   │   ├── 12.conf
</span></span><span style="display:flex;"><span>│   │   ├── 13.conf
</span></span><span style="display:flex;"><span>│   │   ├── 15.conf
</span></span><span style="display:flex;"><span>│   │   ├── 1.conf
</span></span><span style="display:flex;"><span>│   │   ├── 2.conf
</span></span><span style="display:flex;"><span>│   │   ├── 4.conf
</span></span><span style="display:flex;"><span>│   │   ├── 5.conf
</span></span><span style="display:flex;"><span>│   │   └── 6.conf
</span></span><span style="display:flex;"><span>│   ├── redirection_host
</span></span><span style="display:flex;"><span>│   ├── snippets
</span></span><span style="display:flex;"><span>│   │   ├── authelia-authrequest-basic.conf
</span></span><span style="display:flex;"><span>│   │   ├── authelia-authrequest.conf
</span></span><span style="display:flex;"><span>│   │   ├── authelia-authrequest-detect.conf
</span></span><span style="display:flex;"><span>│   │   ├── authelia-location-basic.conf
</span></span><span style="display:flex;"><span>│   │   ├── authelia-location.conf
</span></span><span style="display:flex;"><span>│   │   ├── authelia-location-detect.conf
</span></span><span style="display:flex;"><span>│   │   └── proxy.conf
</span></span><span style="display:flex;"><span>│   ├── stream
</span></span><span style="display:flex;"><span>│   └── temp
</span></span><span style="display:flex;"><span>├── docker-compose.yml
</span></span><span style="display:flex;"><span>└── letsencrypt
</span></span><span style="display:flex;"><span>└── renewal-hooks
</span></span></code></pre></div>]]></content:encoded>
</item>
<item>
<title>My First Merged PR!</title>
<link>/posts/whiptail-first-merged-pr.html</link>
<pubDate>Thu, 01 Sep 2022 13:25:02 -0400</pubDate>
<guid>/posts/whiptail-first-merged-pr.html</guid>
<description>Child like joy of having my first merged PR! I recently was using Whiptail library and fixed a bug.</description>
<content:encoded><![CDATA[<p>Admittedly, I feel a bit like a child sharing something like this, as there are so many devs that pull and merge requests
from contributors on a regular basis. However, while I&rsquo;ve contributed to documentation and/or tutorials and other non-coding
portions of repositories, I feel a tiny bit proud that this was the first instance where I was using a library, <a href="https://github.com/domdfcoding/whiptail/issues/41">found a bug</a>,
created an issue, cloned the repo to my local machine, found and fixed the code, and opened a pull request. It was a great
learning experience with git, github, contributing to projects, and more. <a href="https://github.com/domdfcoding/whiptail/pull/42">My first merged PR!</a></p>
<p>The project and library I was using was called <a href="https://github.com/domdfcoding/whiptail">Whiptail</a> which allows you to use
the terminal message boxes through a python script. <a href="https://whiptail.readthedocs.io/en/latest/">Here are their docs</a>. These message boxes are the same that you might see when you install a linux distro from a USB or first install of a server.</p>
<p>What I was using this for is to develop an visually appealing way to use the <a href="https://meshtastic.org/docs/software/python/cli">Meshtastic CLI</a>. There can be a ton of
settings and flags to add to your <a href="https://meshtastic.org/">Meshtastic</a> device and adding the flags one by one - or worse,
you have to go back and change a flag and you&rsquo;re not using <a href="https://github.com/jeffreytse/zsh-vi-mode">zsh-vi-mode</a> - can be
time consuming. This project would allow you to choose in a whiptail dialog from a list of flags and pass values to the
flags. By having them in a navigable list, you can always retract a flag you no longer need.</p>
<p>Then, once you&rsquo;ve added everything you need, you confirm, and the command runs and syncs up your Meshtastic device!</p>
<p>Maybe I&rsquo;ll post more about that project if I ever get around to finishing it. But since Meshtastic releases new updates on
such a regular basis, I need to ensure the project pulls from Meshtastic commands in a more dynamic way.</p>
<p>Anyway, in this letter to no one, just thought I&rsquo;d share my excitement.</p>
]]></content:encoded>
</item>
</channel>
</rss>