Jekyll2024-01-17T07:27:06-08:00https://aarongodfrey.dev/feed.xmlAutomate The ThingsSoftware engineer and home automation enthusiast.Aaron GodfreyMy Fantastic Fest 20232023-10-01T00:00:00-07:002023-10-01T00:00:00-07:00https://aarongodfrey.dev/movies/fantastic-fest-2023<p><a href="https://fantasticfest.com/">Fantastic Fest</a> is the largest genre film festival in the US showing horror, fantasy, sci-fi and action films. This year was my first time back
since 2019. Over the course of 4 days I screened a total of 23 films and TV shows. There were so many great films I wasn’t able to see but those that I did included:</p>
<ul>
<li><a href="https://www.imdb.com/title/tt9764386/">30 Coins (Season 2, Episodes 1 & 2)</a></li>
<li><a href="https://www.imdb.com/title/tt20413870/">In My Mother’s Skin</a></li>
<li><a href="https://www.imdb.com/title/tt17423262/">The Last Video Store</a></li>
<li><a href="https://www.imdb.com/title/tt23061176/">Property</a></li>
<li><a href="https://www.imdb.com/title/tt10042482/">Falling Stars</a></li>
<li>Mushrooms (2023/Poland)</li>
<li><a href="https://www.imdb.com/title/tt22023218/">You’ll Never Find Me</a></li>
<li>Infested (aka Vermin)</li>
<li><a href="https://m.imdb.com/title/tt15567174">The Fall of the House of Usher (Episodes 1 & 2)</a></li>
<li><a href="https://m.imdb.com/title/tt16300962/">When Evil Lurks</a></li>
<li><a href="https://m.imdb.com/title/tt28662887/">Wake Up</a></li>
<li>Short Fuse (Horror Shorts)
<ul>
<li>Butterscotch</li>
<li>Chomp</li>
<li>Fck’n Nuts</li>
<li>Gummy</li>
<li>Home</li>
<li>Reds</li>
<li>The Third Ear</li>
<li>Transition</li>
<li>Whodunit</li>
</ul>
</li>
<li><a href="https://m.imdb.com/title/tt7527682/">The Origin</a></li>
<li><a href="https://m.imdb.com/title/tt7178516/">Crumb Catcher</a></li>
<li><a href="https://m.imdb.com/title/tt11426232/">Totally Killer</a></li>
</ul>
<h2 id="my-personal-top-5">My Personal Top 5</h2>
<p>There really were not any bad films I watched, but my top 5 films I saw (in no particular order) were:</p>
<ul>
<li><a href="https://www.imdb.com/title/tt23061176/">Property</a>
<blockquote>
<p>A gang of disenfranchised farmhands traps a traumatized woman in her armored car in Daniel Bandeira’s Brazilian take on the home invasion.</p>
</blockquote>
</li>
</ul>
<p>This was a very dark film depicting the terrible class divide between the rich and the poor. Make sure you are prepared before viewing this one!</p>
<ul>
<li><a href="https://m.imdb.com/title/tt7178516/">Crumb Catcher</a>
<blockquote>
<p>An anxiety-inducing chamber piece that will make you fondly remember the worst high-pressure sales pitch you’ve ever delivered (or endured).</p>
</blockquote>
</li>
</ul>
<p>This film really took me by surprise. It was extremely cringey with moments of absurd humor and an overall horror theme.</p>
<ul>
<li><a href="https://m.imdb.com/title/tt11426232/">Totally Killer</a>
<blockquote>
<p>35 years after the shocking murder of three teens, the infamous “Sweet Sixteen Killer“ returns on Halloween night to claim a fourth victim.</p>
</blockquote>
</li>
</ul>
<p>This film was a great way to close out the festival. It was super fun and had me and the audience laughing out loud.</p>
<ul>
<li><a href="https://www.imdb.com/title/tt22023218/">You’ll Never Find Me</a>
<blockquote>
<p>A strange woman desperate for shelter from a harrowing storm picks the wrong trailer to seek refuge… or did she choose exactly right?</p>
</blockquote>
</li>
</ul>
<p>A great single location horror film dripping with tension. The sound design in this movie was excellent.</p>
<ul>
<li><a href="https://m.imdb.com/title/tt7527682/">The Origin</a>
<blockquote>
<p>A group fights for survival against an unknown adversary in this stone age thriller.</p>
</blockquote>
</li>
</ul>
<p>Beautifully filmed with a twist that I wasn’t expecting.</p>Aaron GodfreyFilms and TV shows I screened while at Fantastic Fest 2023 in Austin, TX.Google Maps SDK for Android: Authorization Failure2022-01-23T00:00:00-08:002022-01-23T00:00:00-08:00https://aarongodfrey.dev/programming/google_maps_sdk_authorization_failure<p>I’ve recently been working on rewriting an Android app in <a href="https://kotlinlang.org/">Kotlin</a>
using the new <a href="https://developer.android.com/jetpack/compose">Jetpack Compose UI framework</a>.
One of the screens in the app utilizes Google Maps.</p>
<p>I followed the <a href="https://developers.google.com/maps/documentation/android-sdk/start">documentation</a>
carefully and launched the app. The map loaded as a gray tile and in the logs I received
the following error:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2022-01-16 17:27:13.868 17817-18850/com.foobar.android E/Google Maps Android API: Authorization failure. Please see https://developers.google.com/maps/documentation/android-api/start for how to correctly set up the map.
2022-01-16 17:27:13.872 17817-18850/com.foobar.android E/Google Maps Android API: In the Google Developer Console (https://console.developers.google.com)
Ensure that the "Google Maps Android API v2" is enabled.
Ensure that the following Android Key exists:
API Key: "<my-api-key-recacted>"
Android Application (<cert_fingerprint>;<package_name>): <my-cert-fingerprint-redacted>;com.foobar.android
</code></pre></div></div>
<p>After trying many combinations of verifying the api key was valid and trying no restrictions
and restrictions recommended by the documentation, it still wouldn’t load. I scoured the
first few pages of search results in Google and nothing worked. I finally decided to
reach out to technical support for the Google Maps Platform as a last ditch effort.</p>
<p>I provided a self-contained sample demonstrating the problem, but to my surprise they
were not able to reproduce the issue with my sample code. In their response they asked
about my <code class="language-plaintext highlighter-rouge">local.properties</code> file that contained the api key since that sensitive file
was not provided in the sample code. In their response they mentioned:</p>
<blockquote>
<p>We have not seen how you added the API key in the local.properties file, but we would like you to note that you don’t need to include quotation marks in the API key.</p>
</blockquote>
<p>I went and double checked my <code class="language-plaintext highlighter-rouge">local.properties</code> and saw I had wrapped the api
key in quotes:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GOOGLE_MAPS_DEV_API_KEY="<my-api-key-redacted>"
</code></pre></div></div>
<p>I went ahead and removed the quotes and re-ran the app. Sure enough the map loaded with
no issue. So hopefully this helps someone else out there that makes this simple mistake,
as the error logged by the SDK didn’t give any hint that the quotes were the problem.</p>
<p><em>TLDR</em>: Don’t wrap your values in your <code class="language-plaintext highlighter-rouge">local.properties</code> in quotes.</p>
<p><strong>UPDATE 2022-02-15:</strong> It looks like I’m not the only one who has run into <a href="https://github.com/google/secrets-gradle-plugin/issues/46">this problem</a>.
Quotes will now automatically be removed in <a href="https://github.com/google/secrets-gradle-plugin/releases/tag/v2.0.1">version 2.0.1</a>.</p>Aaron GodfreyA quick post documenting my tiny mistake that took me way too much time to diagnose.Integrating a Gas Insert Fireplace Controlled by a Proflame 2 Transmitter with Home Assiststant2021-10-31T00:00:00-07:002021-10-31T00:00:00-07:00https://aarongodfrey.dev/home%20automation/automating_a_gas_insert_fireplace<h2 id="motivation">Motivation</h2>
<p>My initial motivation to automate the fireplace was to make sure it always turned off
when I went to bed. I primarily use the “smart temperature” mode where the RF remote
has a temperature sensor and will turn the fireplace on/off when it hits the programmed
target temperature.</p>
<p>This is great, but if you don’t remember to turn the remote off,
it will continue to turn the fireplace on/off all night. Because the flames might not
be on when I am heading to bed, I tend to forget the remote is still on and controlling
the fireplace.</p>
<p>So my main goal was to be able to write an automation to automatically turn the fireplace
off at a certain hour, if it was on.</p>
<h2 id="integrating-with-home-assistant">Integrating with Home Assistant</h2>
<h3 id="hardware">Hardware</h3>
<p>The gas insert fireplace uses the Proflame 2 system and is controlled by their
<a href="/assets/data/Proflame-II-Transmitter-Instructions.pdf">Proflame 2 Transmitter Remote</a>.
The remote operates over a Radio Frequency (RF). It can only be controlled via RF or a
battery powered emergency module when the power is out. There is no physical switch to
turn the fireplace on, and can only really be controlled via RF.</p>
<p>After doing some research, I came up short on a definitive answer on how to control it,
but it <em>seemed</em> like it was supported by the <a href="https://bondhome.io/product/bond-bridge/">Bond Bridge</a>.
I went ahead and ordered one and Home Assistant picked it up immediately after I programmed
the remote command with the Bond Bridge. On the Home Assistant side, it was discovered
in the integrations area without any need for manual yaml configuration.</p>
<p>The problem was that both Home Assistant and the Bond Home app wasn’t working, even though
the bridge was indicating it successfully registered the command. I reached out to support
and discovered it needed to be manually programmed. For reference the FCCID for this product
is: <code class="language-plaintext highlighter-rouge">T99058402300</code>. I could just barely make this out on the remote, it had been used for
a year and was nearly non-visible.</p>
<h3 id="programming-commands">Programming Commands</h3>
<p>With some manual steps you can turn the fireplace on/off and control the flames up/down.
One thing to note is that as of 10/31/21, the entity created by the <a href="https://www.home-assistant.io/integrations/bond/">bond integration</a>
will be represented by the <code class="language-plaintext highlighter-rouge">light</code> domain. Toggling the <code class="language-plaintext highlighter-rouge">light.fireplace</code> will turn it
on/off and the brightness slider will increase or decrease the flames. Below are the
steps required to configure these commands in the Bond Home app.</p>
<h4 id="power-on">Power On</h4>
<ol>
<li>Set the fireplace state to ON using the remote control.</li>
<li>In the BOND application select the + (plus) sign.</li>
<li>Select Remote Control then choose your bond</li>
<li>Select Fireplace and set the location and device name and click Continue.</li>
<li>In Select the function you want to program, choose the OFF command to record.</li>
<li>Select Advanced Settings</li>
<li>Select Signal type and click Radio Frequency (RF)</li>
<li>Tap on Frequency and Enter 315 into the frequency rectangle and select done. If there is existing data, please delete it first.</li>
<li>Turn OFF “Search Remote Database” and click save.</li>
<li>Select “Start” and continue with the pairing process by pressing and releasing the power button of the remote.</li>
<li>The bond center ring should turn Green once the command has been programmed</li>
<li>Test if the On command works in the Bond Home Application.</li>
<li>Select yes it works</li>
</ol>
<h4 id="power-off">Power Off</h4>
<ol>
<li>After configuring the above <a href="#power-on">Power On</a> command, when you are asked to select
or choose another command to program, this time, choose the Power ON command.</li>
<li>Make sure the actual state of the fireplace is turned off.</li>
<li>Select “Start” and continue with the pairing process by pressing and releasing the power button of the remote.</li>
<li>The bond center ring should turn Green once the command has been programmed</li>
<li>Test if the Off command works in the Bond Home Application.</li>
<li>Select yes it works</li>
</ol>
<h4 id="flame-up">Flame Up</h4>
<ol>
<li>When you are asked to select or choose another command to program, this time, choose the Flame UP command.</li>
<li>Select “Start” and continue with the pairing process by pressing and releasing the UP button on the remote. And Make sure the mode of your remote is set to control the flame.</li>
<li>Test if the command works in the app.</li>
</ol>
<h4 id="flame-down">Flame Down</h4>
<ol>
<li>After confirming Flame Up works, choose another command to record.</li>
<li>This time choose the Flame Down command</li>
<li>Select “Start” and continue with the pairing process by pressing and releasing the DOWN button on the remote. And Make sure the mode of your remote is set to control the flame.</li>
<li>Test if the command works.</li>
<li>Once you are done programming the commands, save the remote control.</li>
</ol>
<h2 id="automations">Automations</h2>
<p>As of writing, I have 3 primary automations for the fireplace. All of these were created
in Node-Red, but could also easily be done as a normal Home Assistant automation. Since
I already have 99% of my automations in Node-Red I added these there too.</p>
<h3 id="smart-temperature-mode">Smart temperature mode</h3>
<p>This automation replicates the functionality of the Proflame II Transmitter remote. A
target temperature is set and the fireplace will turn on until it reaches that temperature.
When the temperature in the room drops below the target it will turn on, otherwise it will turn off. I already
had a temperature sensor in the room with the fireplace, so I used that entity to probe
the temperature of the room.</p>
<p>I first created two helpers in Home Assistant.</p>
<ol>
<li>A toggle helper to enable / disable the fireplace smart temperature mode.</li>
<li>A number helper to configure the target fireplace temperature for the smart mode.</li>
</ol>
<p>From here the automation is pretty simple. The device I am using for getting the
temperature of the room reports updates approximately every 5 minutes. It takes the current
state and compares it against the configured number helper state and if it’s less it will
send it to the first output to turn on the fireplace. If it’s greater than the configured
value, it will send it to the second output to turn off the fireplace.</p>
<p><a href="/assets/images/0021_smart_temp.png"><img src="/assets/images/0021_smart_temp.png" alt="Smart temperature automation" /></a></p>
<p><a href="/assets/data/smart_temp.json">JSON Export of Flow</a></p>
<p>One interesting thing in this flow is the first node. A <a href="https://jsonata.org/">JSONata</a>
expression is used to extract the state of the number helper so we can compare the
current temperature to the specified target temperature.</p>
<h3 id="auto-off">Auto off</h3>
<p>This one is very staright forward, I just want to ensure that the fireplace gets turned
off at a specific time every night. This just uses an inject node to specify a time and
then runs the service call to switch off the fireplace.</p>
<p><a href="/assets/images/0021_auto_off.png"><img src="/assets/images/0021_auto_off.png" alt="Auto off automation" /></a></p>
<p><a href="/assets/data/fireplace_auto_off.json">JSON Export of Flow</a></p>
<h3 id="sleep-timer">Sleep timer</h3>
<p>The Proflame 2 transmitter remote does not expose a sleep timer function, so I built one
into Home Assistant. This automation uses 2 Home Assistant helpers.</p>
<ol>
<li>A number helper that allows the number of hours (in half hour increments) to be specified to set the
sleep timer.</li>
<li>A timer helper that will handle starting/restarting/finishing based on the number
configured above.</li>
</ol>
<p>The automation is in Node-Red, but it’s triggered via the lovelace dashboard. I’ll go
into the lovelace dashboard configuration in the <a href="#home-assistant-dashboard">Home Assistant Dashboard</a>
section below. The automation looks for timer started/restarted events to turn the
fireplace on. The timer finished event triggers turning the fireplace off.</p>
<p><a href="/assets/images/0021_sleep_timer.png"><img src="/assets/images/0021_sleep_timer.png" alt="Sleep timer automation" /></a></p>
<p><a href="/assets/data/fireplace_sleep_timer.json">JSON Export of Flow</a></p>
<h2 id="home-assistant-dashboard">Home Assistant Dashboard</h2>
<p><a href="/assets/images/0021_dashboard.png"><img src="/assets/images/0021_dashboard.png" alt="Fireplace lovelace dashboard" /></a></p>
<p>The fireplace dashboard in lovelace consists of a single card. The top section displays:</p>
<ul>
<li>The current temperature of the room the fireplace resides.</li>
<li>A number input to configure the target temperature for the “Smart Temperature Mode”.</li>
<li>A toggle to turn on/off the fireplace “Smart Temperature Mode”.</li>
<li>The timer entity which displays the remaining time left on the timer (if activated).</li>
</ul>
<p>The bottom section allows for one to set the number of hours (in half hour increments)
until the fireplace should turn off. Specifying a value and clicking the <code class="language-plaintext highlighter-rouge">RUN</code> button
will start the Home Assistant timer which Node-Red observes.</p>
<p>Below is the yaml for the fireplace dashboard.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">Fireplace</span>
<span class="na">path</span><span class="pi">:</span> <span class="s">fireplace</span>
<span class="na">icon</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mdi:fireplace"</span>
<span class="na">cards</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s2">"</span><span class="s">custom:card-templater"</span>
<span class="na">entities</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">input_number.fireplace_sleep_timer</span>
<span class="na">card</span><span class="pi">:</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">entities</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">Fireplace</span>
<span class="na">entities</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">sensor.living_room_temperature</span>
<span class="pi">-</span> <span class="s">input_number.target_fireplace_temperature</span>
<span class="pi">-</span> <span class="na">entity</span><span class="pi">:</span> <span class="s">input_boolean.enable_fireplace</span>
<span class="na">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Fireplace</span><span class="nv"> </span><span class="s">"Smart</span><span class="nv"> </span><span class="s">Temperature</span><span class="nv"> </span><span class="s">Mode"'</span>
<span class="pi">-</span> <span class="s">timer.fireplace_sleep_timer</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">section</span>
<span class="na">label</span><span class="pi">:</span> <span class="s">Set Sleep Timer</span>
<span class="pi">-</span> <span class="s">input_number.fireplace_sleep_timer</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">call-service</span>
<span class="na">service</span><span class="pi">:</span> <span class="s">timer.start</span>
<span class="na">service_data</span><span class="pi">:</span>
<span class="na">entity_id</span><span class="pi">:</span> <span class="s">timer.fireplace_sleep_timer</span>
<span class="na">duration_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">0:00:00"</span>
<span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="nv"> </span><span class="s">"</span>
<span class="na">icon</span><span class="pi">:</span> <span class="s2">"</span><span class="nv"> </span><span class="s">"</span>
</code></pre></div></div>
<p>The above yaml requires one custom card, the <a href="https://github.com/gadgetchnnel/lovelace-card-templater">lovelace-card-templater</a>.</p>Aaron GodfreyIn this post I document how I integrated my gas insert fireplace controlled by a Proflame 2 Transmitter with Home Assistant. I'll go over the instructions to get it integrated and the automations created to solve the problems I had.10 Favorite Video Games from the Last Year and a Half2021-07-13T00:00:00-07:002021-07-13T00:00:00-07:00https://aarongodfrey.dev/video_games/10-favorite-games-past-year<p>The last year and a half has been quite a ride with the global pandemic. With so much
time being spent at home, I was able to try out a lot of new games on Steam and the
Nintendo Switch. Below is a list of my 10 favorites. With each I’ll include a link to
the platform I personally played it on and a brief description on why I enjoyed it.</p>
<h2 id="-10---song-of-horror"># 10 - Song of Horror</h2>
<p><a href="/assets/images/0020_song_of_horror.jpg"><img src="/assets/images/0020_song_of_horror.jpg" alt="Song of Horror" /></a></p>
<p><a href="https://store.steampowered.com/app/1096570/SONG_OF_HORROR_COMPLETE_EDITION/">Steam</a></p>
<p>This was quite an interesting game, it’s essentially a puzzle game with some horror elements
in an episodic format. The puzzles are very reminiscent of those in the early Resident
Evil games. Instead of using weapons to attack the evil, a mini game will begin that typically
involves some button mashing or timed presses. The game felt very rewarding when figuring
out a puzzle. That being said, some of them were extremely obtuse and near impossible to figure out
even with the available clues and required looking up hints online.</p>
<h2 id="-9---alwas-legacy"># 9 - Alwa’s Legacy</h2>
<p><a href="/assets/images/0020_alwas_legacy.jpg"><img src="/assets/images/0020_alwas_legacy.jpg" alt="Alwa's Legacy" /></a></p>
<p><a href="https://www.nintendo.com/games/detail/alwas-legacy-switch/">Nintendo Switch</a></p>
<p>I had played the original <a href="https://store.steampowered.com/app/549260/Alwas_Awakening/">Alwa’s Awakening</a> before,
so I knew what to expect when picking up the sequel. It’s very Zelda like with some
metroidvania characteristics thrown in. This sequel is quite polished and fixed most of
the issues I had with the original game.</p>
<h2 id="-8---bloodstained-ritual-of-the-night"># 8 - Bloodstained: Ritual of the Night</h2>
<p><a href="/assets/images/0020_bloodstained.jpg"><img src="/assets/images/0020_bloodstained.jpg" alt="Bloodstained: Ritual of the Night" /></a></p>
<p><a href="https://store.steampowered.com/app/692850/Bloodstained_Ritual_of_the_Night/">Steam</a></p>
<p>I ended up playing this on Steam after hearing about performance issues with the Nintendo
Switch version. This “Castlevania” game (all but the name) was developed by Koji Igarashi,
a long time developer of some of the original games. This RPG heavy action platformer
was exactly what I was looking for to scratch that itch for a new Castlevania title.</p>
<h2 id="-7-kaze-and-the-wild-masks"># 7 Kaze and the Wild Masks</h2>
<p><a href="/assets/images/0020_kaze.jpg"><img src="/assets/images/0020_kaze.jpg" alt="Kaze and the Wild Masks" /></a></p>
<p><a href="https://www.nintendo.com/games/detail/kaze-and-the-wild-masks-switch/">Nintendo Switch</a></p>
<p>This was a surprise title for me. If you’ve ever played any of the Donky Kong Country
games, then you know what to expect with this title. The level design is very well done
and the platforming can be challenging at times. There are collectibles and other challenges
that enhance the replayability and kept me engaged for about 12 hours.</p>
<h2 id="-6---ori-and-the-will-of-the-wisps"># 6 - Ori and the Will of the Wisps</h2>
<p><a href="/assets/images/0020_ori.jpg"><img src="/assets/images/0020_ori.jpg" alt="Ori and the Will of the Wisps" /></a></p>
<p><a href="https://store.steampowered.com/app/1057090/Ori_and_the_Will_of_the_Wisps/">Steam</a></p>
<p>This action / platformer / metroidvania is the sequel to the excellent Ori and the Blind
Forest. It fixes most issues with the original game (not that there were many) and adds
a few new mechanics. I really enjoyed this game, but one thing I disliked is the difficulty
was dialed down a notch compared to the original.</p>
<h2 id="-5---control-ultimate-edition"># 5 - Control Ultimate Edition</h2>
<p><a href="/assets/images/0020_control.jpg"><img src="/assets/images/0020_control.jpg" alt="Control Ultimate Edition" /></a></p>
<p><a href="https://store.steampowered.com/app/870780/Control_Ultimate_Edition/">Steam</a></p>
<p>Control is a really unique experience, from the minds of the developers who made Alan Wake,
comes a 3D action game with some metroidvania characteristics. The story is bizarre as
are some of the enemies you’ll encounter. The combat is incredibly fun using your powers
to control and throw objects. This game has a lot of content too and kept me busy for many
hours.</p>
<h2 id="-4---salt-and-sanctuary"># 4 - Salt and Sanctuary</h2>
<p><a href="/assets/images/0020_salt_and_sanctuary.jpg"><img src="/assets/images/0020_salt_and_sanctuary.jpg" alt="Salt and Sanctuary" /></a></p>
<p><a href="https://www.nintendo.com/games/detail/salt-and-sanctuary-switch/">Nintendo Switch</a></p>
<p>I might have bumped this up the list if it wasn’t for the large learning curve and lack
of instructions on how to play the game. I completed the game not ever using many items
that I collected. The creed mechanic isn’t explained very well either so I stuck with the
one I chose at the beginning. Another negative is not having a map. There are some large
areas, but over time you get the hang of it.</p>
<p>All of those negatives aside, this was a super fun metroidvania containing that reminded
me a lot of Hollow Knight and Blasphemous. The boss encounters were fun and the level design
and artwork was fantastic.</p>
<h2 id="-3---spiritfarer"># 3 - Spiritfarer</h2>
<p><a href="/assets/images/0020_spiritfarer.jpg"><img src="/assets/images/0020_spiritfarer.jpg" alt="Spiritfarer" /></a></p>
<p><a href="https://www.nintendo.com/games/detail/spiritfarer-switch/">Nintendo Switch</a></p>
<p>Of this list, this was the biggest surprise for me. When I read the description of the game
I really didn’t think it was for me. In the game you explore the sea, visit towns, harvest
items that you can use to upgrade your ship and take care of spirits you encounter.</p>
<p>The artwork is gorgeous and it is super fun to control the main character. The stories you
encounter with each spirit really gave me the feels. Combined with the amazing soundtrack
this game invokes a lot of emotion and I highly recommend it.</p>
<h2 id="-2---blasphemous"># 2 - Blasphemous</h2>
<p><a href="/assets/images/0020_blasphemous.jpg"><img src="/assets/images/0020_blasphemous.jpg" alt="Blasphemous" /></a></p>
<p><a href="https://www.nintendo.com/games/detail/blasphemous-switch/">Nintendo Switch</a></p>
<p>This was by far my favorite metroidvania I played in the last year and a half. The combat
is excellent and the story is incredibly dark. It has some souls-like mechanics mixed in
and has a pretty high level of difficulty.</p>
<p>I think this is the first game that I finished
and immediately started it again. The 2nd play through allows you to select a penance
that massively changes how you play the game. I chose the “Penitence of the True Guilt”
which meant the bile flasks no longer refilled my health but instead my Fervour. This meant
I used Fervour much more on my 2nd play through where I rarely used it the first time through.</p>
<h2 id="-1---phoenotopia-awakening"># 1 - Phoenotopia: Awakening</h2>
<p><a href="/assets/images/0020_phoenotopia.jpg"><img src="/assets/images/0020_phoenotopia.jpg" alt="Phoenotopia: Awakening" /></a></p>
<p><a href="https://www.nintendo.com/games/detail/phoenotopia-awakening-switch/">Nintendo Switch</a></p>
<p>My top spot goes to Pheonotopia: Awakening. I really wish more people knew about this
game and gave it a shot. I was drawn to it by some comparisons to Zelda II on the NES.
The overworld looks very similar to Zelda II where enemies spawn on the map and when
you run into them you jump into a 2-d side scroller to combat them. There are many
towns, NPCs, and secrets to find.</p>
<p>I enjoyed this game so much that even after finishing the game and beating the secret
boss, I kept playing to find every single secret. Fortunately there is an NPC that will
give you hints to aid you in finding the secrets which is more fun then just spilling
the beans. I dumped 70+ hours into this title and it is absolutely worth the price of
admission.</p>Aaron GodfreyI compiled a quick list of my 10 favorite video games I've played over the last year and a half.Use CoordinatorEntity when using the DataUpdateCoordinator2021-04-19T00:00:00-07:002021-04-19T00:00:00-07:00https://aarongodfrey.dev/home%20automation/use-coordinatorentity-with-the-dataupdatecoordinator<p>I recently had a user point out that one of my custom components that monitors a user’s
<a href="https://github.com/custom-components/sensor.nintendo_wishlist">Nintendo Wish List</a> was
using excessive data. He pointed out the drop in traffic to the domain that is used
to search for games on sale in Europe when he disabled the integration as you can see
below and in the header image.</p>
<p><a href="/assets/images/0019_header.jpg"><img src="/assets/images/0019_header.jpg" alt="Data Usage" /></a></p>
<p>I was a bit perplexed as the custom integration utilized Home Assistant’s
<a href="https://developers.home-assistant.io/docs/integration_fetching_data/#coordinated-single-api-poll-for-data-for-all-entities">DataUpdateCoordinator</a> which drastically reduces network
calls by fetching all of the data needed by the entities just once. The entities then
use the data stored by the coordinator to update their state. The other way to do this
is to have each entity (think 10 games on your wish list) and each one individually
hits the api to see if it’s on sale. Since all the data comes from the same endpoint we
only need to make that call once and the <code class="language-plaintext highlighter-rouge">DataUpdateCoordinator</code> helps us manage that.</p>
<p>One of the arguments that is passed to the <code class="language-plaintext highlighter-rouge">DataUpdateCoordinator</code> is a <code class="language-plaintext highlighter-rouge">datetime.timedelta</code> that specified how
often it should update. I have this value configurable through the integration and
defaulted to once an hour. After doing some debugging I saw thousands of updates were
being made in the first few hours.</p>
<p>The TLDR is if you are using the <code class="language-plaintext highlighter-rouge">DataUpdateCoordinator</code>, your entities need to make
sure that they return <code class="language-plaintext highlighter-rouge">False</code> for the <code class="language-plaintext highlighter-rouge">should_poll</code> property. By default this value is
<code class="language-plaintext highlighter-rouge">True</code> when you sub-class the <code class="language-plaintext highlighter-rouge">Entity</code> or <code class="language-plaintext highlighter-rouge">BinarySensorEntity</code>. Without this, data will
be fetched about every 30 seconds.</p>
<p>While I was investigating this issue I stumbled upon the <a href="https://github.com/home-assistant/core/blob/dev/homeassistant/helpers/update_coordinator.py#L291">CoordinatorEntity</a>
which was added at some point in the Fall of 2020. This class basically just takes care
of defining a few common methods you would normally add to all of your <code class="language-plaintext highlighter-rouge">Entity</code> classes
when you are using the <code class="language-plaintext highlighter-rouge">DataUpdateCoordinator</code>. It also explicitly sets the <code class="language-plaintext highlighter-rouge">should_poll</code>
property to <code class="language-plaintext highlighter-rouge">False</code> which was the hint I needed to figure out why my integration was
making so many network calls.</p>
<p>If you are interested in the details, the commit to switch to sub-classing the <code class="language-plaintext highlighter-rouge">CoordinatorEntity</code> can
be found <a href="https://github.com/custom-components/sensor.nintendo_wishlist/commit/6709a5c1b6e323494e7449fa1ac24e61100fc302">here</a>.
Hopefully this helps someone else out if they run into this issue with their custom
component.</p>Aaron GodfreyA quick tip on using the CoordinatorEntity class for you entities when using the DataUpdateCoordinator in Home Assistant.Fixing the Roborock Vacuum Error 102021-03-11T00:00:00-08:002021-03-11T00:00:00-08:00https://aarongodfrey.dev/troubleshooting/roborock-s4-error-10<h2 id="the-problem">The Problem</h2>
<p>Recently every time my <a href="https://us.roborock.com/pages/roborock-s4">Roborock S4 vacuum</a>
ran, it would announce something like “Error 10 - Check the filter, if it’s wet dry it.”
The official roborock <a href="https://support.roborock.com/hc/en-us/articles/360035731571-What-should-I-do-when-error-10-occurs-">support article</a> simply states that the filter
should be cleaned and dried thoroughly.</p>
<p>The problem is, even after a thorough cleaning and drying the filter for 48 hours it would
still report the same error every time it ran.</p>
<h2 id="the-solution">The Solution</h2>
<p>Since the official support didn’t help, I searched the Internet and came across a
<a href="https://www.reddit.com/r/Roborock/comments/fr8ilg/roborock_s6_error_10_filter_clogged_or_wet_but/">reddit post</a> where the original poster had the
same issue. Many commented that the actual fix is to use compressed air to clean the
rear vent.</p>
<p><a href="/assets/images/0018_roborock_vent.png"><img src="/assets/images/0018_roborock_vent.png" alt="Roborock Vacuum vent" /></a></p>
<p>Sure enough, after using compressed air to clean the vent, the vacuum has now been running
great for over a week with no errors. Hopefully this helps someone else who runs into
this issue.</p>Aaron GodfreyA short post on how to fix your vacuum when it complains about Error 10.Integrating the Dyson TP04 Air Purifier in Home Assistant2021-02-12T00:00:00-08:002021-02-12T00:00:00-08:00https://aarongodfrey.dev/home%20automation/dyson-tp04-integration<h2 id="why-the-dyson-tp04">Why the Dyson TP04?</h2>
<p>I currently reside in the Pacific North West and wild fires occur in the region seasonally.
With the rise of climate change they are getting worse, so after last year’s several week
stint of hazardous air quality I wanted to pick up an air purifier to help indoors.</p>
<p>Naturally, I was inclined to find an air purifier that I could control with Home Assistant
and would ideally operate locally without the need for the cloud. During my research the
first model that stood out was the <a href="https://www.mi.com/global/mi-air-purifier-3H">Xioami Mi Air Purifer 3H</a>.
Unfortunately this was not available at US retailors and was even difficult to find
on sites like GearBest and AliExpress.</p>
<p>I decided to scrap the idea of the Xioami Mi Air Purifier and searched for other options
in the <a href="https://community.home-assistant.io/">Home Assistant community forums</a>. I found
several posts discussing the <a href="https://www.dyson.com/air-treatment/purifiers">Dyson air purifiers</a>.
Of the models listed, Home Assistant currently supports the <code class="language-plaintext highlighter-rouge">DP04</code>, <code class="language-plaintext highlighter-rouge">TP04</code> and <code class="language-plaintext highlighter-rouge">PH01</code> models.
After looking at the 3 models I immediately ruled out the <a href="https://www.dyson.com/air-treatment/purifiers/dyson-pure-cool/dyson-pure-cool-desk-white-silver">DP04</a>
as it’s not longer available. The <a href="https://www.dyson.com/air-treatment/purifier-humidifiers/dyson-pure-humidify-cool/dyson-humidify-cool-white-silver">PH01</a> was also ruled out based on
price and that it was also a humidifier which I didn’t need. The remaining option was the
<a href="https://www.dyson.com/air-treatment/purifiers/dyson-pure-cool/dyson-pure-cool-tower-white-silver">TP04</a>.</p>
<p>When I started looking into the <a href="https://www.dyson.com/air-treatment/purifiers/dyson-pure-cool/dyson-pure-cool-tower-white-silver">TP04</a>
I was initially put off by the price. It’s not a cheap device by any means, but after
reading some reviews I ended up justifying the cost based on the following reasons:</p>
<ul>
<li>It’s an air purifier - Yes, this was the original goal and even those on the cheaper side
start at around $200.</li>
<li>It has a lot of sensors - I was pretty impressed by the advertised sensors I’d be able
to use in home assistant including:
<ul>
<li>Temperature</li>
<li>Humidity</li>
<li>AQI</li>
<li>Particulate Matter 2.5</li>
<li>Particulate Matter 10</li>
<li>NO2</li>
<li>Volatile Organic Compounds</li>
</ul>
</li>
<li>A stand alone indoor air sensor from <a href="https://www2.purpleair.com/collections/air-quality-sensors/products/purpleair-pa-i-indoor">purple air</a> already runs at about $200. So
combining that with the air purifier would put it at about $400 without any “smart features”.</li>
<li>It has really good smart features - The Auto mode is a “set it and forget it” mode that will
automatically purify the air. When you pair that with schedules and night mode, which will
make it operate at quietly as possible, you have some really useful features.</li>
<li>Local control via MQTT - While a network request is used to fetch registered devices once
during the integration setup, the rest of the sensor data and control of the devices is
done via MQTT locally.</li>
<li>It doubles as a fan - This is super minor and probably contributes least to why I
decided to go with this model.</li>
</ul>
<h2 id="integration">Integration</h2>
<div class="notice--warning">
<strong>Update 20201-04-19:</strong>
<p>
A few months ago, the official integration broke due to an external dependency that is not
being maintained. Dyson made some updates and added 2FA which broke the external dependency.
</p>
<p>
I now recommend that you use the <a href="https://github.com/shenxn/ha-dyson">ha-dyson</a> custom
integration that can be installed via HACS. It not only works, but also can be used 100%
locally after the initial setup to gather authentication data for you devices from the cloud.
</p>
<p>
I'm leaving the rest of the original instructions this section here for posterity.
</p>
</div>
<p>After installing the Dyson Link app and connecting the air purifier, I added the necessary
<a href="https://www.home-assistant.io/integrations/dyson/">configuration</a> to my <code class="language-plaintext highlighter-rouge">configuration.yaml</code>.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Enables Dyson integration (for our air purifier (Dyson pure cool TP04)</span>
<span class="c1"># https://www.home-assistant.io/integrations/dyson/</span>
<span class="na">dyson</span><span class="pi">:</span>
<span class="na">username</span><span class="pi">:</span> <span class="kt">!secret</span> <span class="s">dyson_username</span>
<span class="na">password</span><span class="pi">:</span> <span class="kt">!secret</span> <span class="s">dyson_password</span>
<span class="na">language</span><span class="pi">:</span> <span class="s">US</span>
<span class="na">devices</span><span class="pi">:</span>
<span class="na">device_id</span><span class="pi">:</span> <span class="kt">!secret</span> <span class="s">dyson_id</span>
<span class="na">device_ip</span><span class="pi">:</span> <span class="kt">!secret</span> <span class="s">dyson_ip</span>
</code></pre></div></div>
<p>After a restart I had the following new entities:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">air_quality.bedroom</code></li>
<li><code class="language-plaintext highlighter-rouge">fan.bedroom</code></li>
<li><code class="language-plaintext highlighter-rouge">sensor.bedroom_humidity</code></li>
<li><code class="language-plaintext highlighter-rouge">sensor.bedroom_temperature</code></li>
<li><code class="language-plaintext highlighter-rouge">sensor.bedroom_hepa_filter_remaining_life</code></li>
<li><code class="language-plaintext highlighter-rouge">sensor.bedroom_carbon_filter_remaining_life</code></li>
</ul>
<p>This integration has all the normal <code class="language-plaintext highlighter-rouge">fan</code> services and a few that are unique to the device.
As of writing the following additional services are available for this model:</p>
<ul>
<li><a href="https://www.home-assistant.io/integrations/dyson/#service-dysonset_speed">dyson.set_speed</a> - Set the exact speed (1-10) of the fan.</li>
<li><a href="https://www.home-assistant.io/integrations/dyson/#service-dysonset_auto_mode">dyson.set_auto_mode</a> - Toggle the fan’s auto mode.</li>
<li><a href="https://www.home-assistant.io/integrations/dyson/#service-dysonset_night_mode">dyson.set_night_mode</a> - Toggle the fan’s night mode.</li>
<li><a href="https://www.home-assistant.io/integrations/dyson/#service-dysonset_angle-only-for-dp04-and-tp04">dyson.set_angle</a> - Set the oscillation angle of the fan.</li>
<li><a href="https://www.home-assistant.io/integrations/dyson/#service-dysonset_flow_direction_front-only-for-dp04-and-tp04">dyson.set_flow_direction_front</a> - Set the flow direction of the fan.</li>
<li><a href="https://www.home-assistant.io/integrations/dyson/#service-dysonset_timer-only-for-dp04-and-tp04">dyson.set_timer</a> - Set a sleep timer.</li>
</ul>
<h2 id="template-sensors">Template Sensors</h2>
<div class="notice--warning">
<strong>Update 20201-04-19:</strong>
<p>
These template sensors are no longer needed if you are using the <a href="https://github.com/shenxn/ha-dyson">ha-dyson</a> custom
integration that can be installed via HACS.
</p>
</div>
<p>While there are attributes on the fan entity that is created for the various sensors,
I wanted to pull each of them out into their own sensor. My primary reason for this was
so that data could be fed to <a href="https://www.influxdata.com/products/influxdb/">influxdb</a> and I could visualize it using <a href="https://grafana.com/">grafana</a>. I ended up creating separate sensors
for Air Quality Index, Volatile Organic Compounds, NO2, Particulate Matter 2.5, and
Particulate Matter 10. Below are the template sensors in my <code class="language-plaintext highlighter-rouge">configration.yaml</code>.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">sensor</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">platform</span><span class="pi">:</span> <span class="s">template</span>
<span class="na">sensors</span><span class="pi">:</span>
<span class="na">dyson_aqi</span><span class="pi">:</span>
<span class="na">friendly_name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Dyson</span><span class="nv"> </span><span class="s">TP-04</span><span class="nv"> </span><span class="s">Air</span><span class="nv"> </span><span class="s">Quality</span><span class="nv"> </span><span class="s">Index"</span>
<span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">state_attr('air_quality.bedroom',</span><span class="nv"> </span><span class="s">'air_quality_index')</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s">AQI</span>
<span class="na">unique_id</span><span class="pi">:</span> <span class="s">dyson_tp_04_aqi</span>
<span class="na">dyson_particulate_matter_2_5</span><span class="pi">:</span>
<span class="na">friendly_name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Dyson</span><span class="nv"> </span><span class="s">TP-04</span><span class="nv"> </span><span class="s">PM2.5"</span>
<span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">state_attr('air_quality.bedroom',</span><span class="nv"> </span><span class="s">'particulate_matter_2_5')</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">µg/m³"</span>
<span class="na">unique_id</span><span class="pi">:</span> <span class="s">dyson_tp_04_pm2_5</span>
<span class="na">dyson_particulate_matter_10</span><span class="pi">:</span>
<span class="na">friendly_name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Dyson</span><span class="nv"> </span><span class="s">TP-04</span><span class="nv"> </span><span class="s">PM10"</span>
<span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">state_attr('air_quality.bedroom',</span><span class="nv"> </span><span class="s">'particulate_matter_10')</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">µg/m³"</span>
<span class="na">unique_id</span><span class="pi">:</span> <span class="s">dyson_tp_04_pm_10</span>
<span class="na">dyson_volatile_organic_compounds</span><span class="pi">:</span>
<span class="na">friendly_name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Dyson</span><span class="nv"> </span><span class="s">TP-04</span><span class="nv"> </span><span class="s">Volatile</span><span class="nv"> </span><span class="s">Organic</span><span class="nv"> </span><span class="s">Compounds"</span>
<span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">state_attr('air_quality.bedroom',</span><span class="nv"> </span><span class="s">'volatile_organic_compounds')</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">VOC</span><span class="nv"> </span><span class="s">Scale"</span>
<span class="na">unique_id</span><span class="pi">:</span> <span class="s">dyson_tp_04_voc</span>
<span class="na">dyson_nitrogen_dioxide</span><span class="pi">:</span>
<span class="na">friendly_name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Dyson</span><span class="nv"> </span><span class="s">TP-04</span><span class="nv"> </span><span class="s">Nitrogen</span><span class="nv"> </span><span class="s">Dioxide"</span>
<span class="na">value_template</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">state_attr('air_quality.bedroom',</span><span class="nv"> </span><span class="s">'nitrogen_dioxide')</span><span class="nv"> </span><span class="s">}}"</span>
<span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s">NO2 Scale</span>
<span class="na">unique_id</span><span class="pi">:</span> <span class="s">dyson_tp_04_no2</span>
</code></pre></div></div>
<h2 id="calculating-aqi-based-on-pm25">Calculating AQI (based on PM2.5)</h2>
<p>One thing I noticed about the AQI sensor, is that the integration simply returns the maximum
number from the PM2.5, PM10, NO2 and VOC sensors as it’s value. That particular value isn’t
all that useful since each of those sensors have their own unit of measurement. It’s not clear in
the Dyson Link app how that value is calculated and it isn’t exposed in the MQTT data.</p>
<p>What I wanted was a number like I use for my <a href="https://www2.purpleair.com/collections/air-quality-sensors/products/purpleair-pa-i-indoor">PurpleAir PA-I-Indoor sensor</a>. Fortunately the formula
was pulled from the javascript code in the purple air site <a href="https://community.home-assistant.io/t/purpleair-air-quality-sensor/146588">in this community post</a>. Using that formula I created
my own calculated AQI based on PM2.5 as a template sensor (this can be added with the other
template sensors created above):</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">platform</span><span class="pi">:</span> <span class="s">template</span>
<span class="na">sensors</span><span class="pi">:</span>
<span class="na">dyson_calc_aqi</span><span class="pi">:</span>
<span class="na">friendly_name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Dyson</span><span class="nv"> </span><span class="s">Calculated</span><span class="nv"> </span><span class="s">PM2.5</span><span class="nv"> </span><span class="s">AQI"</span>
<span class="na">unit_of_measurement</span><span class="pi">:</span> <span class="s">AQI</span>
<span class="na">unique_id</span><span class="pi">:</span> <span class="s">dypson_tp_04_calc_aqi</span>
<span class="c1"># https://community.home-assistant.io/t/purpleair-air-quality-sensor/146588</span>
<span class="na">value_template</span><span class="pi">:</span> <span class="pi">></span>
<span class="s">{% macro calcAQI(Cp, Ih, Il, BPh, BPl) -%}</span>
<span class="s">{{ (((Ih - Il)/(BPh - BPl)) * (Cp - BPl) + Il)|round }}</span>
<span class="s">{%- endmacro %}</span>
<span class="s">{% if (state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float) > 1000 %}</span>
<span class="s">invalid</span>
<span class="s">{% elif (state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float) > 350.5 %}</span>
<span class="s">{{ calcAQI((state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float), 500.0, 401.0, 500.0, 350.5) }}</span>
<span class="s">{% elif (state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float) > 250.5 %}</span>
<span class="s">{% elif (state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float) > 250.5 %}</span>
<span class="s">{{ calcAQI((state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float), 400.0, 301.0, 350.4, 250.5) }}</span>
<span class="s">{% elif (state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float) > 150.5 %}</span>
<span class="s">{{ calcAQI((state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float), 300.0, 201.0, 250.4, 150.5) }}</span>
<span class="s">{% elif (state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float) > 55.5 %}</span>
<span class="s">{{ calcAQI((state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float), 200.0, 151.0, 150.4, 55.5) }}</span>
<span class="s">{% elif (state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float) > 35.5 %}</span>
<span class="s">{{ calcAQI((state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float), 150.0, 101.0, 55.4, 35.5) }}</span>
<span class="s">{% elif (state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float) > 12.1 %}</span>
<span class="s">{{ calcAQI((state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float), 100.0, 51.0, 35.4, 12.1) }}</span>
<span class="s">{% elif (state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float) >= 0.0 %}</span>
<span class="s">{{ calcAQI((state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float), 50.0, 0.0, 12.0, 0.0) }}</span>
<span class="s">{% else %}</span>
<span class="s">invalid</span>
<span class="s">{% endif %}</span>
</code></pre></div></div>
<p>To verify that this was calculating the value correctly, I moved my PurpleAir indoor
sensor next to the Dyson air purifier for 6 hours and plotted the 2 values on a graph in
grafana.</p>
<p><a href="/assets/images/0017_header.png"><img src="/assets/images/0017_header.png" alt="Grafana Graph" /></a></p>
<p>After examining the graph it was pretty clear that the calculation was matching that used
by the PurpleAir indoor sensor.</p>
<h2 id="custom-card">Custom Card</h2>
<p>After I created all of the template sensors I wanted, I created a custom card to be able
to display all of the sensor data and to control the air purifier. To do this I used the
extremely versatile <a href="https://www.home-assistant.io/lovelace/picture-elements/">picture-elements lovelace card</a>. Before I show you the code, here is the finished card in my dashboard:</p>
<p><a href="/assets/images/0017_dyson_card.png"><img src="/assets/images/0017_dyson_card.png" alt="Dyson Air Purifier Custom Card" /></a></p>
<p>On the left of the card are the sensors for calculated AQI, PM2.5, PM10, NO2 and VOC.
The background color of these sensors will change based on the level reported. So they
will be green when in healthy levels than transition to orange, red, purple and burgundy
when hazordous. In the middle of the card are circular cards representing the % remaining of the HEPA
and carbon filters. On the right of the card are the current humidity and temperature
readings. Along the bottom are buttons to control the purifier. They will be green when
enabled and the default color when not enabled. From left to right they represent
On/Off, Oscillation, Air flow direction, Auto Mode and Night Mode.</p>
<p>In order to use this card as-is you will need to install 2 custom cards from <a href="https://hacs.xyz/">HACS</a>:</p>
<ul>
<li><a href="https://github.com/gadgetchnnel/lovelace-card-templater">lovelace-card-templater</a></li>
<li><a href="https://github.com/custom-cards/circle-sensor-card">circle-sensor-card</a></li>
</ul>
<p>You will also need to create the <a href="#template-sensors">template sensors above</a> or modify
the yaml below to reference attributes of your <code class="language-plaintext highlighter-rouge">fan</code> entity. Obviously you will need to
make sure to update entity names referenced in the yaml to match your names. The last
thing you will need is the background image which can be <a href="/assets/images/0017_card_background.png">downloaded here</a>.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s2">"</span><span class="s">custom:card-templater"</span>
<span class="na">entities</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">fan.master_bedroom</span>
<span class="pi">-</span> <span class="s">sensor.dyson_calc_aqi</span>
<span class="pi">-</span> <span class="s">sensor.dyson_volatile_organic_compounds</span>
<span class="pi">-</span> <span class="s">sensor.dyson_nitrogen_dioxide</span>
<span class="pi">-</span> <span class="s">sensor.dyson_particulate_matter_2_5</span>
<span class="pi">-</span> <span class="s">sensor.dyson_particulate_matter_10</span>
<span class="na">card</span><span class="pi">:</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">picture-elements</span>
<span class="na">image</span><span class="pi">:</span> <span class="s2">"</span><span class="s">/local/tp-04.png"</span>
<span class="na">elements</span><span class="pi">:</span>
<span class="c1"># Buttons</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-icon</span>
<span class="na">icon</span><span class="pi">:</span> <span class="s">mdi:power</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">fan.master_bedroom</span>
<span class="na">tap_action</span><span class="pi">:</span>
<span class="na">action</span><span class="pi">:</span> <span class="s">call-service</span>
<span class="na">service</span><span class="pi">:</span> <span class="s">fan.toggle</span>
<span class="na">service_data</span><span class="pi">:</span>
<span class="na">entity_id</span><span class="pi">:</span> <span class="s">fan.master_bedroom</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">left</span><span class="pi">:</span> <span class="m">0</span>
<span class="na">right</span><span class="pi">:</span> <span class="m">0</span>
<span class="na">bottom</span><span class="pi">:</span> <span class="m">0</span>
<span class="na">border</span><span class="pi">:</span> <span class="s">1px solid rgba(0, 0, 0, 0.4)</span>
<span class="na">background-color</span><span class="pi">:</span> <span class="s2">"</span><span class="s">rgba(0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0.8)"</span>
<span class="na">padding</span><span class="pi">:</span> <span class="s">10px</span>
<span class="na">font-size</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">line-height</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="s">--paper-item-icon-active-color</span><span class="pi">:</span> <span class="s">green</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-icon</span>
<span class="na">state_color</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">icon</span><span class="pi">:</span> <span class="s">mdi:sleep</span>
<span class="na">title_template</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Night</span><span class="nv"> </span><span class="s">mode</span><span class="nv"> </span><span class="s">{%</span><span class="nv"> </span><span class="s">if</span><span class="nv"> </span><span class="s">states("switch.master_bedroom_night_mode")</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">"on"</span><span class="nv"> </span><span class="s">%}(click</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">disable){%</span><span class="nv"> </span><span class="s">else</span><span class="nv"> </span><span class="s">%}(click</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">enable){%</span><span class="nv"> </span><span class="s">endif</span><span class="nv"> </span><span class="s">%}'</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">fan.master_bedroom</span>
<span class="na">tap_action</span><span class="pi">:</span>
<span class="na">action</span><span class="pi">:</span> <span class="s">call-service</span>
<span class="na">service</span><span class="pi">:</span> <span class="s">switch.toggle</span>
<span class="na">service_data</span><span class="pi">:</span>
<span class="na">entity_id</span><span class="pi">:</span> <span class="s">switch.master_bedroom_night_mode</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">right</span><span class="pi">:</span> <span class="m">0</span>
<span class="na">bottom</span><span class="pi">:</span> <span class="m">0</span>
<span class="na">padding</span><span class="pi">:</span> <span class="s">10px</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="na">font-size</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">line-height</span><span class="pi">:</span> <span class="s">16px</span>
<span class="s">--paper-item-icon-color_template</span><span class="pi">:</span> <span class="s1">'</span><span class="s">{%</span><span class="nv"> </span><span class="s">if</span><span class="nv"> </span><span class="s">states("switch.master_bedroom_night_mode")</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">"on"</span><span class="nv"> </span><span class="s">%}green{%</span><span class="nv"> </span><span class="s">else</span><span class="nv"> </span><span class="s">%}rgb(68,</span><span class="nv"> </span><span class="s">115,</span><span class="nv"> </span><span class="s">158){%</span><span class="nv"> </span><span class="s">endif</span><span class="nv"> </span><span class="s">%}'</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-icon</span>
<span class="na">state_color</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">icon</span><span class="pi">:</span> <span class="s">mdi:alpha-a-circle</span>
<span class="na">title_template</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Auto</span><span class="nv"> </span><span class="s">mode</span><span class="nv"> </span><span class="s">{%</span><span class="nv"> </span><span class="s">if</span><span class="nv"> </span><span class="s">states("switch.master_bedroom_auto_mode")</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">"on"</span><span class="nv"> </span><span class="s">%}(click</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">disable){%</span><span class="nv"> </span><span class="s">else</span><span class="nv"> </span><span class="s">%}(click</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">enable){%</span><span class="nv"> </span><span class="s">endif</span><span class="nv"> </span><span class="s">%}'</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">fan.master_bedroom</span>
<span class="na">tap_action</span><span class="pi">:</span>
<span class="na">action</span><span class="pi">:</span> <span class="s">call-service</span>
<span class="na">service</span><span class="pi">:</span> <span class="s">switch.toggle</span>
<span class="na">service_data</span><span class="pi">:</span>
<span class="na">entity_id</span><span class="pi">:</span> <span class="s">switch.master_bedroom_auto_mode</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">right</span><span class="pi">:</span> <span class="s">61px</span>
<span class="na">bottom</span><span class="pi">:</span> <span class="m">0</span>
<span class="na">padding</span><span class="pi">:</span> <span class="s">10px</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="na">font-size</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">line-height</span><span class="pi">:</span> <span class="s">16px</span>
<span class="s">--paper-item-icon-color_template</span><span class="pi">:</span> <span class="s1">'</span><span class="s">{%</span><span class="nv"> </span><span class="s">if</span><span class="nv"> </span><span class="s">states("switch.master_bedroom_auto_mode")</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">"on"</span><span class="nv"> </span><span class="s">%}green{%</span><span class="nv"> </span><span class="s">else</span><span class="nv"> </span><span class="s">%}rgb(68,</span><span class="nv"> </span><span class="s">115,</span><span class="nv"> </span><span class="s">158){%</span><span class="nv"> </span><span class="s">endif</span><span class="nv"> </span><span class="s">%}'</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-icon</span>
<span class="na">state_color</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">icon</span><span class="pi">:</span> <span class="s">mdi:air-purifier</span>
<span class="na">title_template</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Airflow</span><span class="nv"> </span><span class="s">direction</span><span class="nv"> </span><span class="s">{%</span><span class="nv"> </span><span class="s">if</span><span class="nv"> </span><span class="s">state_attr("fan.master_bedroom",</span><span class="nv"> </span><span class="s">"flow_direction_front")</span><span class="nv"> </span><span class="s">%}(click</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">set</span><span class="nv"> </span><span class="s">flow</span><span class="nv"> </span><span class="s">direction</span><span class="nv"> </span><span class="s">behind){%</span><span class="nv"> </span><span class="s">else</span><span class="nv"> </span><span class="s">%}(click</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">set</span><span class="nv"> </span><span class="s">flow</span><span class="nv"> </span><span class="s">direction</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">front){%</span><span class="nv"> </span><span class="s">endif</span><span class="nv"> </span><span class="s">%}'</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">fan.master_bedroom</span>
<span class="na">tap_action</span><span class="pi">:</span>
<span class="na">action</span><span class="pi">:</span> <span class="s">call-service</span>
<span class="na">service</span><span class="pi">:</span> <span class="s">dyson.set_flow_direction_front</span>
<span class="na">service_data</span><span class="pi">:</span>
<span class="na">entity_id</span><span class="pi">:</span> <span class="s">fan.master_bedroom</span>
<span class="na">flow_direction_front_template</span><span class="pi">:</span> <span class="pi">>-</span>
<span class="s">{% if state_attr("fan.master_bedroom", "flow_direction_front") %}false{% else %}true{% endif %}</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">right</span><span class="pi">:</span> <span class="s">122px</span>
<span class="na">bottom</span><span class="pi">:</span> <span class="m">0</span>
<span class="na">padding</span><span class="pi">:</span> <span class="s">10px</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="na">font-size</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">line-height</span><span class="pi">:</span> <span class="s">16px</span>
<span class="s">--paper-item-icon-color_template</span><span class="pi">:</span> <span class="s1">'</span><span class="s">{%</span><span class="nv"> </span><span class="s">if</span><span class="nv"> </span><span class="s">state_attr("fan.master_bedroom",</span><span class="nv"> </span><span class="s">"flow_direction_front")</span><span class="nv"> </span><span class="s">%}green{%</span><span class="nv"> </span><span class="s">else</span><span class="nv"> </span><span class="s">%}rgb(68,</span><span class="nv"> </span><span class="s">115,</span><span class="nv"> </span><span class="s">158){%</span><span class="nv"> </span><span class="s">endif</span><span class="nv"> </span><span class="s">%}'</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-icon</span>
<span class="na">state_color</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">icon</span><span class="pi">:</span> <span class="s">mdi:arrow-split-vertical</span>
<span class="na">title_template</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Oscillation</span><span class="nv"> </span><span class="s">{%</span><span class="nv"> </span><span class="s">if</span><span class="nv"> </span><span class="s">state_attr("fan.master_bedroom",</span><span class="nv"> </span><span class="s">"oscillating")</span><span class="nv"> </span><span class="s">%}(click</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">turn</span><span class="nv"> </span><span class="s">off){%</span><span class="nv"> </span><span class="s">else</span><span class="nv"> </span><span class="s">%}(click</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">turn</span><span class="nv"> </span><span class="s">on){%</span><span class="nv"> </span><span class="s">endif</span><span class="nv"> </span><span class="s">%}'</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">fan.master_bedroom</span>
<span class="na">tap_action</span><span class="pi">:</span>
<span class="na">action</span><span class="pi">:</span> <span class="s">call-service</span>
<span class="na">service</span><span class="pi">:</span> <span class="s">fan.oscillate</span>
<span class="na">service_data</span><span class="pi">:</span>
<span class="na">entity_id</span><span class="pi">:</span> <span class="s">fan.master_bedroom</span>
<span class="na">oscillating_template</span><span class="pi">:</span> <span class="pi">>-</span>
<span class="s">{% if state_attr("fan.master_bedroom", "oscillating") %}false{% else %}true{% endif %}</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">right</span><span class="pi">:</span> <span class="s">183px</span>
<span class="na">bottom</span><span class="pi">:</span> <span class="m">0</span>
<span class="na">padding</span><span class="pi">:</span> <span class="s">10px</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="na">font-size</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">line-height</span><span class="pi">:</span> <span class="s">16px</span>
<span class="s">--paper-item-icon-color_template</span><span class="pi">:</span> <span class="s1">'</span><span class="s">{%</span><span class="nv"> </span><span class="s">if</span><span class="nv"> </span><span class="s">state_attr("fan.master_bedroom",</span><span class="nv"> </span><span class="s">"oscillating")</span><span class="nv"> </span><span class="s">%}green{%</span><span class="nv"> </span><span class="s">else</span><span class="nv"> </span><span class="s">%}rgb(68,</span><span class="nv"> </span><span class="s">115,</span><span class="nv"> </span><span class="s">158){%</span><span class="nv"> </span><span class="s">endif</span><span class="nv"> </span><span class="s">%}'</span>
<span class="c1"># Sensors</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-label</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">sensor.dyson_calc_aqi</span>
<span class="na">suffix</span><span class="pi">:</span> <span class="s2">"</span><span class="nv"> </span><span class="s">(PM2.5)"</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">background-color_template</span><span class="pi">:</span> <span class="pi">>-</span>
<span class="s">{% if states("sensor.dyson_calc_aqi")|float > 300 %}</span>
<span class="s">#B71C1C</span>
<span class="s">{% elif states("sensor.dyson_calc_aqi")|float > 200 %}</span>
<span class="s">#9C27B0</span>
<span class="s">{% elif states("sensor.dyson_calc_aqi")|float > 150 %}</span>
<span class="s">#E53935</span>
<span class="s">{% elif states("sensor.dyson_calc_aqi")|float > 100 %}</span>
<span class="s">#FB8C00</span>
<span class="s">{% elif states("sensor.dyson_calc_aqi")|float > 50 %}</span>
<span class="s">#FFC107</span>
<span class="s">{% else %}</span>
<span class="s">#4CAF50</span>
<span class="s">{% endif %}</span>
<span class="na">width</span><span class="pi">:</span> <span class="s">25%</span>
<span class="na">top</span><span class="pi">:</span> <span class="s">3%</span>
<span class="na">left</span><span class="pi">:</span> <span class="s">0%</span>
<span class="na">border-top-right-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="na">border-bottom-right-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="na">border</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2px</span><span class="nv"> </span><span class="s">solid</span><span class="nv"> </span><span class="s">rgba(0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0.2)"</span>
<span class="na">padding</span><span class="pi">:</span> <span class="s">5px</span>
<span class="na">font-size</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">line-height</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">color</span><span class="pi">:</span> <span class="s">white</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-label</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">air_quality.master_bedroom</span>
<span class="na">attribute</span><span class="pi">:</span> <span class="s">particulate_matter_2_5</span>
<span class="na">suffix</span><span class="pi">:</span> <span class="s2">"</span><span class="nv"> </span><span class="s">PM2.5"</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">background-color_template</span><span class="pi">:</span> <span class="pi">>-</span>
<span class="s">{% if state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float > 250 %}</span>
<span class="s">#B71C1C</span>
<span class="s">{% elif state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float > 150 %}</span>
<span class="s">#9C27B0</span>
<span class="s">{% elif state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float > 70 %}</span>
<span class="s">#E53935</span>
<span class="s">{% elif state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float > 53 %}</span>
<span class="s">#FB8C00</span>
<span class="s">{% elif state_attr("air_quality.master_bedroom", "particulate_matter_2_5")|float > 35 %}</span>
<span class="s">#FFC107</span>
<span class="s">{% else %}</span>
<span class="s">#4CAF50</span>
<span class="s">{% endif %}</span>
<span class="na">width</span><span class="pi">:</span> <span class="s">25%</span>
<span class="na">top</span><span class="pi">:</span> <span class="s">17%</span>
<span class="na">left</span><span class="pi">:</span> <span class="s">0%</span>
<span class="na">border-top-right-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="na">border-bottom-right-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="na">border</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2px</span><span class="nv"> </span><span class="s">solid</span><span class="nv"> </span><span class="s">rgba(0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0.2)"</span>
<span class="na">padding</span><span class="pi">:</span> <span class="s">5px</span>
<span class="na">font-size</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">line-height</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">color</span><span class="pi">:</span> <span class="s">white</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-label</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">air_quality.master_bedroom</span>
<span class="na">attribute</span><span class="pi">:</span> <span class="s">particulate_matter_10</span>
<span class="na">suffix</span><span class="pi">:</span> <span class="s2">"</span><span class="nv"> </span><span class="s">PM10"</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">background-color_template</span><span class="pi">:</span> <span class="pi">>-</span>
<span class="s">{% if state_attr("air_quality.master_bedroom", "particulate_matter_10")|float > 420 %}</span>
<span class="s">#B71C1C</span>
<span class="s">{% elif state_attr("air_quality.master_bedroom", "particulate_matter_10")|float > 350 %}</span>
<span class="s">#9C27B0</span>
<span class="s">{% elif state_attr("air_quality.master_bedroom", "particulate_matter_10")|float > 100 %}</span>
<span class="s">#E53935</span>
<span class="s">{% elif state_attr("air_quality.master_bedroom", "particulate_matter_10")|float > 75 %}</span>
<span class="s">#FB8C00</span>
<span class="s">{% elif state_attr("air_quality.master_bedroom", "particulate_matter_10")|float > 50 %}</span>
<span class="s">#FFC107</span>
<span class="s">{% else %}</span>
<span class="s">#4CAF50</span>
<span class="s">{% endif %}</span>
<span class="na">width</span><span class="pi">:</span> <span class="s">25%</span>
<span class="na">top</span><span class="pi">:</span> <span class="s">31%</span>
<span class="na">left</span><span class="pi">:</span> <span class="s">0%</span>
<span class="na">border-top-right-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="na">border-bottom-right-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="na">border</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2px</span><span class="nv"> </span><span class="s">solid</span><span class="nv"> </span><span class="s">rgba(0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0.2)"</span>
<span class="na">padding</span><span class="pi">:</span> <span class="s">5px</span>
<span class="na">font-size</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">line-height</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">color</span><span class="pi">:</span> <span class="s">white</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-label</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">air_quality.master_bedroom</span>
<span class="na">attribute</span><span class="pi">:</span> <span class="s">nitrogen_dioxide</span>
<span class="na">suffix</span><span class="pi">:</span> <span class="s2">"</span><span class="nv"> </span><span class="s">NO2"</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">background-color_template</span><span class="pi">:</span> <span class="pi">>-</span>
<span class="s">{% if state_attr("air_quality.master_bedroom", "nitrogen_dioxide")|float > 8 %}</span>
<span class="s">#E53935</span>
<span class="s">{% elif state_attr("air_quality.master_bedroom", "nitrogen_dioxide")|float > 6 %}</span>
<span class="s">#FB8C00</span>
<span class="s">{% elif state_attr("air_quality.master_bedroom", "nitrogen_dioxide")|float > 3 %}</span>
<span class="s">#FFC107</span>
<span class="s">{% else %}</span>
<span class="s">#4CAF50</span>
<span class="s">{% endif %}</span>
<span class="na">width</span><span class="pi">:</span> <span class="s">25%</span>
<span class="na">top</span><span class="pi">:</span> <span class="s">45%</span>
<span class="na">left</span><span class="pi">:</span> <span class="s">0%</span>
<span class="na">border-top-right-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="na">border-bottom-right-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="na">border</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2px</span><span class="nv"> </span><span class="s">solid</span><span class="nv"> </span><span class="s">rgba(0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0.2)"</span>
<span class="na">padding</span><span class="pi">:</span> <span class="s">5px</span>
<span class="na">font-size</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">line-height</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">color</span><span class="pi">:</span> <span class="s">white</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-label</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">air_quality.master_bedroom</span>
<span class="na">attribute</span><span class="pi">:</span> <span class="s">volatile_organic_compounds</span>
<span class="na">suffix</span><span class="pi">:</span> <span class="s2">"</span><span class="nv"> </span><span class="s">VOC"</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">background-color_template</span><span class="pi">:</span> <span class="pi">>-</span>
<span class="s">{% if state_attr("air_quality.master_bedroom", "volatile_organic_compounds")|float > 8 %}</span>
<span class="s">#E53935</span>
<span class="s">{% elif state_attr("air_quality.master_bedroom", "volatile_organic_compounds")|float > 6 %}</span>
<span class="s">#FB8C00</span>
<span class="s">{% elif state_attr("air_quality.master_bedroom", "volatile_organic_compounds")|float > 3 %}</span>
<span class="s">#FFC107</span>
<span class="s">{% else %}</span>
<span class="s">#4CAF50</span>
<span class="s">{% endif %}</span>
<span class="na">width</span><span class="pi">:</span> <span class="s">25%</span>
<span class="na">border</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2px</span><span class="nv"> </span><span class="s">solid</span><span class="nv"> </span><span class="s">rgba(0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0.2)"</span>
<span class="na">top</span><span class="pi">:</span> <span class="s">59%</span>
<span class="na">left</span><span class="pi">:</span> <span class="s">0%</span>
<span class="na">border-top-right-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="na">border-bottom-right-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="na">padding</span><span class="pi">:</span> <span class="s">5px</span>
<span class="na">font-size</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">line-height</span><span class="pi">:</span> <span class="s">16px</span>
<span class="na">color</span><span class="pi">:</span> <span class="s">white</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-icon</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">sensor.master_bedroom_humidity_dyson</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">top</span><span class="pi">:</span> <span class="s">2%</span>
<span class="na">right</span><span class="pi">:</span> <span class="s">0px</span>
<span class="na">background-color</span><span class="pi">:</span> <span class="s2">"</span><span class="s">rgba(0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0.5)"</span>
<span class="na">border</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2px</span><span class="nv"> </span><span class="s">solid</span><span class="nv"> </span><span class="s">rgba(0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0.2)"</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="na">width</span><span class="pi">:</span> <span class="s">20%</span>
<span class="na">border-top-left-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="na">border-bottom-left-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-label</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">sensor.master_bedroom_humidity_dyson</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">top</span><span class="pi">:</span> <span class="s">2%</span>
<span class="na">right</span><span class="pi">:</span> <span class="s">5%</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="na">line-height</span><span class="pi">:</span> <span class="s">25px</span>
<span class="c1"># Temperature and Humidity</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-icon</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">sensor.master_bedroom_temperature_dyson</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">top</span><span class="pi">:</span> <span class="s">16%</span>
<span class="na">right</span><span class="pi">:</span> <span class="s">0px</span>
<span class="na">background-color</span><span class="pi">:</span> <span class="s2">"</span><span class="s">rgba(0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0.5)"</span>
<span class="na">border</span><span class="pi">:</span> <span class="s2">"</span><span class="s">2px</span><span class="nv"> </span><span class="s">solid</span><span class="nv"> </span><span class="s">rgba(0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0,</span><span class="nv"> </span><span class="s">0.2)"</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="na">width</span><span class="pi">:</span> <span class="s">20%</span>
<span class="na">border-top-left-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="na">border-bottom-left-radius</span><span class="pi">:</span> <span class="s">7px</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">state-label</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">sensor.master_bedroom_temperature_dyson</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">top</span><span class="pi">:</span> <span class="s">16%</span>
<span class="na">right</span><span class="pi">:</span> <span class="s">3%</span>
<span class="na">transform</span><span class="pi">:</span> <span class="s">translate(0%,0%)</span>
<span class="na">line-height</span><span class="pi">:</span> <span class="s">25px</span>
<span class="c1"># Filters</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">custom:circle-sensor-card</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">sensor.master_bedroom_carbon_filter_life</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">Carbon Filter</span>
<span class="na">fill</span><span class="pi">:</span> <span class="s">rgba(0, 0, 0, 0.7)</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">top</span><span class="pi">:</span> <span class="s">25%</span>
<span class="na">left</span><span class="pi">:</span> <span class="s">50%</span>
<span class="na">width</span><span class="pi">:</span> <span class="s">20%</span>
<span class="pi">-</span> <span class="na">type</span><span class="pi">:</span> <span class="s">custom:circle-sensor-card</span>
<span class="na">entity</span><span class="pi">:</span> <span class="s">sensor.dyson_hepa_filter</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">HEPA Filter</span>
<span class="na">fill</span><span class="pi">:</span> <span class="s">rgba(0, 0, 0, 0.7)</span>
<span class="na">style</span><span class="pi">:</span>
<span class="na">top</span><span class="pi">:</span> <span class="s">60%</span>
<span class="na">left</span><span class="pi">:</span> <span class="s">50%</span>
<span class="na">width</span><span class="pi">:</span> <span class="s">20%</span>
</code></pre></div></div>Aaron GodfreyIn this post I integrate my Dyson TP04 Air Purifier in Home Assistant. I'll get it configured, add some custom sensors and create a card to display in the dashboard.Building a Home Assistant Custom Component Part 5: Debugging2020-12-28T00:00:00-08:002020-12-28T00:00:00-08:00https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_5<div class="notice--info">
<p>This is the fifth and final part of a multi-part tutorial to create a Home Assistant custom component.</p>
<ul>
<li><a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_1/">Part 1 - Project Structure and Basics</a></li>
<li><a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_2/">Part 2 - Unit Testing and Continuous Integration</a></li>
<li><a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_3/">Part 3 - Config Flow</a></li>
<li><a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_4/">Part 4 - Options Flow</a></li>
<li>Part 5 - Debugging (Reading Now!)</li>
</ul>
</div>
<h2 id="introduction">Introduction</h2>
<p>This is the final part of the tutorial for creating a Home Assistant custom component.
This post will cover how to debug your custom component to ensure it
works as expected and to figure out why some thing might not be working like you expect.</p>
<p>I must admit, I only recently learned about the devcontainer offered by Home Assistant for
local development. Prior to learning about it, I would modify files locally, scp them to
the the correct folder on my Home Assistant instance then restart my instance. Not only
was this slow, but it also meant my Home Assistant instance powering my house would have
to go down constantly as I tried out changes when debugging a problem. As it turns out,
Home Assistant had already developed a solution for local development that doesn’t require
taking down your “production” instance. Enter the devcontainer.</p>
<h2 id="visual-studio-code--devcontainer">Visual Studio Code + devcontainer</h2>
<p>To utilize the devcontainer, you will first need to install
<a href="https://code.visualstudio.com/">Visual Studio Code</a>. Visual Studio Code is a free IDE
that is extremely popular and has plenty of extensions for speeding up and improving
development. After installing there are a few other requirements that must be installed, check out the <a href="https://developers.home-assistant.io/docs/development_environment/#developing-with-visual-studio-code--devcontainer">official documentation</a>
for more details.</p>
<p>After you have installed the prerequisites and cloned the <a href="https://github.com/home-assistant/core">home-assistant/core</a> repository, you can start Visual Studio Code and open the cloned
directory. When opening the folder for the first time, Visual Studio Code will detect
the devcontainer and ask if you would like to open the editor in the container, select
yes. This first open will build the container which may take a minute or two. Subsequent
opens will be much quicker as it will reuse the built container.</p>
<h2 id="configuring-the-devcontainer">Configuring the devcontainer</h2>
<p>Before we proceed further we will need to copy our custom component into the <code class="language-plaintext highlighter-rouge">config</code>
directory in the root of the cloned home-assistant/core repository. Note that you may
need to elevate your permissions as docker will create files owned by root in the <code class="language-plaintext highlighter-rouge">config</code>
directory. Alternatively you can use the built-in terminal in the IDE which gives you a
root prompt with the correct permissions.</p>
<p>First navigate to <code class="language-plaintext highlighter-rouge">/path/to/cloned/home-assistant/config</code> and create a <code class="language-plaintext highlighter-rouge">custom_components</code>
directory. From there copy your custom component directly into this new folder. For our
tutourial project we’d copy the entire <code class="language-plaintext highlighter-rouge">github_custom</code> directory.</p>
<p>Next add any necessary configuration to the <code class="language-plaintext highlighter-rouge">configuration.yaml</code> file. Since our tutorial
custom component uses the config flow, we don’t need to add anything as we can add the
integration and set it up in the configuration UI.</p>
<h2 id="run-home-assistant">Run Home Assistant</h2>
<p>Now that we have our files copied and configuration updated, return to Visual Studio Code
and click on the Run tab (Ctrl+Shift+D) in the left panel. You will see a mostly empty
panel with a dropdown at the top that contains debug configurations that can be run.
At the time of writing there are 2 options, <code class="language-plaintext highlighter-rouge">Home Assistant</code> which runs a local instance and
<code class="language-plaintext highlighter-rouge">Preview (nodejs)</code> which runs a local instance of the documentation site.</p>
<figure style="width: 290px" class="align-left">
<img src="/assets/images/0016_run_tab.png" alt="VSCode Run Panel" />
<figcaption>Visual Studio Code Run panel.</figcaption>
</figure>
<p>Select <code class="language-plaintext highlighter-rouge">Home Assistant</code> from the dropdown and click the green triangle to start the
debugger. This will also open the <code class="language-plaintext highlighter-rouge">Terminal</code> panel at the bottom where you can
see the Home Assistant logs. You can now navigate to <a href="http://localhost:8123">http://localhost:8123</a>
in your browser and you will be guided through the initial setup of Home Assistant (creating
your user, etc.). In Visual Studio Code you will also see a debug toolbar pop up near the
top center of the IDE.</p>
<figure style="width: 218px" class="align-right">
<img src="/assets/images/0016_debug_toolbar.png" alt="VSCode Debug Toolbar" />
<figcaption>Visual Studio Code debug toolbar.</figcaption>
</figure>
<p>The debug toolbar contains controls for the following operations in the order the icons
appear to the right:</p>
<ul>
<li>Pause/Resume</li>
<li>Step over</li>
<li>Step into</li>
<li>Step Out</li>
<li>Restart</li>
<li>Stop</li>
</ul>
<p>Check out the <a href="https://code.visualstudio.com/Docs/editor/debugging">Visual Studio Code documentation</a>
for more details on what each operation does. The most used buttons will be to resume
the program after hitting a breakpoint and restarting Home Assistant after making python
code changes.</p>
<h2 id="breakpoints">Breakpoints</h2>
<p>Breakpoints are extremely useful for being able to stop program execution and inspect
variables at a particular spot in your code. To set a breakpoint find the line where
you want to pause the program flow and inspect the variables and click to the left of
the line number. This will add a red dot which indicates a break point. When you hit that
code while navigating Home Assistant in your browser, it will automatically pause the
program and allow you to inspect values in the run panel.</p>
<p><a href="/assets/images/0016_breakpoint.png"><img src="/assets/images/0016_breakpoint.png" alt="Breakpoint" /></a></p>
<p>In the screenshot above you can see the local and global variables along with their values.
After you are done inspecting the values you can click the resume button in the debug
toolbar to continue program execution until it hits another breakpoint.</p>
<h2 id="wrap-up">Wrap Up</h2>
<p>As you can see the devcontainer inside Visual Studio Code makes debugging your custom
component much simpler and faster.</p>
<p>I sincererly hope that these posts have helped you understand how you can develop your
own custom component (and possibly even add it to Home Assistant at some point in the future).
Many of the concepts documented in these posts also apply to the official Home Assistant
code base, so I highly encourage you to contribute or become a code owner of an
existing integration.</p>Aaron GodfreyPart 5 of building a custom component in Home Assistant. This is the final post in the tutorial series and it focuses on how to debug your custom component (or any Home Assistant component) in a devcontainer using Visual Studio Code.Building a Home Assistant Custom Component Part 4: Options Flow2020-12-27T00:00:00-08:002020-12-27T00:00:00-08:00https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_4<div class="notice--info">
<p>This is the fourth part of a multi-part tutorial to create a Home Assistant custom component.</p>
<ul>
<li><a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_1/">Part 1 - Project Structure and Basics</a></li>
<li><a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_2/">Part 2 - Unit Testing and Continuous Integration</a></li>
<li><a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_3/">Part 3 - Config Flow</a></li>
<li>Part 4 - Options Flow (Reading Now!)</li>
<li><a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_5/">Part 5 - Debugging</a></li>
</ul>
</div>
<h2 id="introduction">Introduction</h2>
<p>In this post we will be adding an <a href="https://developers.home-assistant.io/docs/config_entries_options_flow_handler/">Options flow</a>
to our custom component. We are still using the same example project, <a href="https://github.com/boralyl/github-custom-component-tutorial">github-custom-component-tutorial</a>.
You can find the diff for this post on the <a href="https://github.com/boralyl/github-custom-component-tutorial/compare/feature/part3...feature/part4">feature/part4</a> branch.</p>
<p>The options flow allows a user to configure additional options for the component at any
time by navigating to the integrations page and clicking the <code class="language-plaintext highlighter-rouge">Options</code> button on the
card for your component. Generally speaking these configuration values are optional, whereas
values in the config flow are required to make the component function.</p>
<p>I highly suggest reading over the <a href="https://developers.home-assistant.io/docs/config_entries_options_flow_handler/">official documentation</a>
prior to continuing along with the tutorial.</p>
<h2 id="enable-options-support">Enable Options Support</h2>
<p>Per the <a href="https://developers.home-assistant.io/docs/config_entries_options_flow_handler/#options-support">documentation</a>,
the first step is to define a method on your config flow class that lets it know that the
component supports options. In our case we will add this to our <a href="https://github.com/boralyl/github-custom-component-tutorial/blob/master/custom_components/github_custom/config_flow.py#L120-L124">GithubCustomConfigFlow</a> class.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="nb">staticmethod</span>
<span class="o">@</span><span class="n">callback</span>
<span class="k">def</span> <span class="nf">async_get_options_flow</span><span class="p">(</span><span class="n">config_entry</span><span class="p">):</span>
<span class="s">"""Get the options flow for this handler."""</span>
<span class="k">return</span> <span class="n">OptionsFlowHandler</span><span class="p">(</span><span class="n">config_entry</span><span class="p">)</span>
</code></pre></div></div>
<p>One slight modification from the official documentation is that our <code class="language-plaintext highlighter-rouge">OptionsFlowHandler</code>
class will require the instance of the config entry when initializing. This will be required
for nearly every component you may write as we will use the <code class="language-plaintext highlighter-rouge">options</code> property of the
<code class="language-plaintext highlighter-rouge">config_entry</code> to populate default values for our options flow form.</p>
<h2 id="configure-fields-and-errors-in-stringsjson">Configure Fields and Errors in strings.json</h2>
<p>Just like our config flow, we need to name our data fields and error messages in the
<code class="language-plaintext highlighter-rouge">strings.json</code>. These will be nested under an <code class="language-plaintext highlighter-rouge">options</code> key. You will need to add these
for each language you choose to support in the <code class="language-plaintext highlighter-rouge">translations</code> directory.</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gi">+ },
+ "options": {
+ "error": {
+ "invalid_path": "The path provided is not valid. Should be in the format `user/repo-name` and should be a valid github repository."
+ },
+ "step": {
+ "init": {
+ "title": "Manage Repos",
+ "data": {
+ "repos": "Existing Repos: Uncheck any repos you want to remove.",
+ "path": "New Repo: Path to the repository e.g. home-assistant-core",
+ "name": "New Repo: Name of the sensor."
+ },
+ "description": "Remove existing repos or add a new repo."
+ }
+ }
</span></code></pre></div></div>
<h2 id="define-an-optionsflow-handler">Define an OptionsFlow Handler</h2>
<p>The next step is to write our class to handle the options flow. This will look very similar
to the class we defined for our config flow so it should be familiar. For brevity I’m
going to omit much of the logic in the class to try to simplify it to show the important
parts. I’ll give a general overview of how it works then I’ll dive into the specific
logic I <a href="#options-flow-in-the-github-custom-component">added for our tutorial component</a>.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">OptionsFlowHandler</span><span class="p">(</span><span class="n">config_entries</span><span class="p">.</span><span class="n">OptionsFlow</span><span class="p">):</span>
<span class="s">"""Handles options flow for the component."""</span>
<span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">config_entry</span><span class="p">:</span> <span class="n">config_entries</span><span class="p">.</span><span class="n">ConfigEntry</span><span class="p">)</span> <span class="o">-></span> <span class="bp">None</span><span class="p">:</span>
<span class="bp">self</span><span class="p">.</span><span class="n">config_entry</span> <span class="o">=</span> <span class="n">config_entry</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">async_step_init</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span> <span class="n">user_input</span><span class="p">:</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>
<span class="p">)</span> <span class="o">-></span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]:</span>
<span class="s">"""Manage the options for the custom component."""</span>
<span class="n">errors</span><span class="p">:</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="p">{}</span>
<span class="c1"># Grab all configured repos from the entity registry so we can populate the
</span> <span class="c1"># multi-select dropdown that will allow a user to remove a repo.
</span> <span class="n">entity_registry</span> <span class="o">=</span> <span class="k">await</span> <span class="n">async_get_registry</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">hass</span><span class="p">)</span>
<span class="n">entries</span> <span class="o">=</span> <span class="n">async_entries_for_config_entry</span><span class="p">(</span>
<span class="n">entity_registry</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">config_entry</span><span class="p">.</span><span class="n">entry_id</span>
<span class="p">)</span>
<span class="c1"># Default value for our multi-select.
</span> <span class="n">all_repos</span> <span class="o">=</span> <span class="p">{</span><span class="n">e</span><span class="p">.</span><span class="n">entity_id</span><span class="p">:</span> <span class="n">e</span><span class="p">.</span><span class="n">original_name</span> <span class="k">for</span> <span class="n">e</span> <span class="ow">in</span> <span class="n">entries</span><span class="p">}</span>
<span class="n">repo_map</span> <span class="o">=</span> <span class="p">{</span><span class="n">e</span><span class="p">.</span><span class="n">entity_id</span><span class="p">:</span> <span class="n">e</span> <span class="k">for</span> <span class="n">e</span> <span class="ow">in</span> <span class="n">entries</span><span class="p">}</span>
<span class="k">if</span> <span class="n">user_input</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
<span class="c1"># Validation and additional processing logic omitted for brevity.
</span> <span class="c1"># ...
</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">errors</span><span class="p">:</span>
<span class="c1"># Value of data will be set on the options property of our config_entry
</span> <span class="c1"># instance.
</span> <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">async_create_entry</span><span class="p">(</span>
<span class="n">title</span><span class="o">=</span><span class="s">""</span><span class="p">,</span>
<span class="n">data</span><span class="o">=</span><span class="p">{</span><span class="n">CONF_REPOS</span><span class="p">:</span> <span class="n">updated_repos</span><span class="p">},</span>
<span class="p">)</span>
<span class="n">options_schema</span> <span class="o">=</span> <span class="n">vol</span><span class="p">.</span><span class="n">Schema</span><span class="p">(</span>
<span class="p">{</span>
<span class="n">vol</span><span class="p">.</span><span class="n">Optional</span><span class="p">(</span><span class="s">"repos"</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="nb">list</span><span class="p">(</span><span class="n">all_repos</span><span class="p">.</span><span class="n">keys</span><span class="p">())):</span> <span class="n">cv</span><span class="p">.</span><span class="n">multi_select</span><span class="p">(</span>
<span class="n">all_repos</span>
<span class="p">),</span>
<span class="n">vol</span><span class="p">.</span><span class="n">Optional</span><span class="p">(</span><span class="n">CONF_PATH</span><span class="p">):</span> <span class="n">cv</span><span class="p">.</span><span class="n">string</span><span class="p">,</span>
<span class="n">vol</span><span class="p">.</span><span class="n">Optional</span><span class="p">(</span><span class="n">CONF_NAME</span><span class="p">):</span> <span class="n">cv</span><span class="p">.</span><span class="n">string</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">async_show_form</span><span class="p">(</span>
<span class="n">step_id</span><span class="o">=</span><span class="s">"init"</span><span class="p">,</span> <span class="n">data_schema</span><span class="o">=</span><span class="n">options_schema</span><span class="p">,</span> <span class="n">errors</span><span class="o">=</span><span class="n">errors</span>
<span class="p">)</span>
</code></pre></div></div>
<h3 id="override-__init__">Override __init__</h3>
<p>We must override <code class="language-plaintext highlighter-rouge">__init__</code> so that it can accept a <code class="language-plaintext highlighter-rouge">config_entry</code> instance which we
set as an attribute on the class. As mentioned above this is so we can access it’s
<code class="language-plaintext highlighter-rouge">options</code> property to pre-populate data in our options flow form.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">config_entry</span><span class="p">:</span> <span class="n">config_entries</span><span class="p">.</span><span class="n">ConfigEntry</span><span class="p">)</span> <span class="o">-></span> <span class="bp">None</span><span class="p">:</span>
<span class="bp">self</span><span class="p">.</span><span class="n">config_entry</span> <span class="o">=</span> <span class="n">config_entry</span>
</code></pre></div></div>
<h3 id="define-the-options-data-schema">Define the Options Data Schema</h3>
<p>Next up we define our options data schema. This is identical to how we define the schema
for our config flow. We are defining the schema in the method itself so that we can supply
a default value for the repos key which is dynamically evalulated in this method. If you
do not need any dynamic values you can define it as a constant above just like we did with
the schema for the config flow.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">options_schema</span> <span class="o">=</span> <span class="n">vol</span><span class="p">.</span><span class="n">Schema</span><span class="p">(</span>
<span class="p">{</span>
<span class="n">vol</span><span class="p">.</span><span class="n">Optional</span><span class="p">(</span><span class="s">"repos"</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="nb">list</span><span class="p">(</span><span class="n">all_repos</span><span class="p">.</span><span class="n">keys</span><span class="p">())):</span> <span class="n">cv</span><span class="p">.</span><span class="n">multi_select</span><span class="p">(</span>
<span class="n">all_repos</span>
<span class="p">),</span>
<span class="n">vol</span><span class="p">.</span><span class="n">Optional</span><span class="p">(</span><span class="n">CONF_PATH</span><span class="p">):</span> <span class="n">cv</span><span class="p">.</span><span class="n">string</span><span class="p">,</span>
<span class="n">vol</span><span class="p">.</span><span class="n">Optional</span><span class="p">(</span><span class="n">CONF_NAME</span><span class="p">):</span> <span class="n">cv</span><span class="p">.</span><span class="n">string</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>
<p>While I am not using default values for the other keys in the schema in my component, this is where you
would generally look up existing options values from the config entry instance to set
default values for your form. For example:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">vol</span><span class="p">.</span><span class="n">Optional</span><span class="p">(</span><span class="n">CONF_PATH</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">config_entry</span><span class="p">.</span><span class="n">options</span><span class="p">[</span><span class="n">CONF_PATH</span><span class="p">])</span>
</code></pre></div></div>
<h3 id="display-the-options-form">Display the Options Form</h3>
<p>There isn’t anything new here that we haven’t seen in the config flow. One thing to note
that is different from the config flow, is that the options flow only ever has a single
step named <code class="language-plaintext highlighter-rouge">init</code>.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">async_show_form</span><span class="p">(</span>
<span class="n">step_id</span><span class="o">=</span><span class="s">"init"</span><span class="p">,</span> <span class="n">data_schema</span><span class="o">=</span><span class="n">options_schema</span><span class="p">,</span> <span class="n">errors</span><span class="o">=</span><span class="n">errors</span>
<span class="p">)</span>
</code></pre></div></div>
<h3 id="save-options-data">Save Options Data</h3>
<p>When a user has submitted <code class="language-plaintext highlighter-rouge">user_input</code> that validates we can then format and save our
options data by returning the <code class="language-plaintext highlighter-rouge">asnyc_create_entry</code> method.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Value of data will be set on the options property of our config_entry
# instance.
</span><span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">async_create_entry</span><span class="p">(</span>
<span class="n">title</span><span class="o">=</span><span class="s">""</span><span class="p">,</span>
<span class="n">data</span><span class="o">=</span><span class="p">{</span><span class="n">some_option</span><span class="p">:</span> <span class="n">user_input</span><span class="p">[</span><span class="s">"some_option"</span><span class="p">]},</span>
<span class="p">)</span>
</code></pre></div></div>
<h2 id="register-options-update-listener">Register Options Update Listener</h2>
<p>In order for our component to know that options have changed and to be able to act on them,
we must register and update a listener when initially setting up our config entry. In our
<code class="language-plaintext highlighter-rouge">__init__.py</code> file we will define our update listener function and register it with the
config entry.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">options_update_listener</span><span class="p">(</span>
<span class="n">hass</span><span class="p">:</span> <span class="n">core</span><span class="p">.</span><span class="n">HomeAssistant</span><span class="p">,</span> <span class="n">config_entry</span><span class="p">:</span> <span class="n">config_entries</span><span class="p">.</span><span class="n">ConfigEntry</span>
<span class="p">):</span>
<span class="s">"""Handle options update."""</span>
<span class="k">await</span> <span class="n">hass</span><span class="p">.</span><span class="n">config_entries</span><span class="p">.</span><span class="n">async_reload</span><span class="p">(</span><span class="n">config_entry</span><span class="p">.</span><span class="n">entry_id</span><span class="p">)</span>
</code></pre></div></div>
<p>As you can see above the logic of the listener is very simple. It just reloads the config
entry so that it can act on the new options data that was saved. We must then register
the listener in our <code class="language-plaintext highlighter-rouge">async_setup_entry</code> function.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">hass_data</span> <span class="o">=</span> <span class="nb">dict</span><span class="p">(</span><span class="n">entry</span><span class="p">.</span><span class="n">data</span><span class="p">)</span>
<span class="c1"># Registers update listener to update config entry when options are updated.
</span><span class="n">unsub_options_update_listener</span> <span class="o">=</span> <span class="n">entry</span><span class="p">.</span><span class="n">add_update_listener</span><span class="p">(</span><span class="n">options_update_listener</span><span class="p">)</span>
<span class="c1"># Store a reference to the unsubscribe function to cleanup if an entry is unloaded.
</span><span class="n">hass_data</span><span class="p">[</span><span class="s">"unsub_options_update_listener"</span><span class="p">]</span> <span class="o">=</span> <span class="n">unsub_options_update_listener</span>
<span class="n">hass</span><span class="p">.</span><span class="n">data</span><span class="p">[</span><span class="n">DOMAIN</span><span class="p">][</span><span class="n">entry</span><span class="p">.</span><span class="n">entry_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">hass_data</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">add_updated_listener</code> method returns an unsubscribe function that we will store for
later so that we can clean up the listener if the config entry is removed by the user.</p>
<p>One thing to note is that the update listener function will only get called if the data
passed to <code class="language-plaintext highlighter-rouge">self.async_create_entry</code> in our Options Flow class is different then it
previously was. If nothing changed, the options update listener will not get called and
your config entry will not be reloaded.</p>
<h2 id="use-options-values-during-setup">Use Options Values During Setup</h2>
<p>Now that we’ve setup our options flow, the user can enter values and they will be saved
on the config entry instance. The last step is to use those values while setting up our
platforms. In our <code class="language-plaintext highlighter-rouge">sensor.py</code> we could then use the options values to change how our
sensors get setup. An example might look something like the following:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">async_setup_entry</span><span class="p">(</span>
<span class="n">hass</span><span class="p">:</span> <span class="n">core</span><span class="p">.</span><span class="n">HomeAssistant</span><span class="p">,</span>
<span class="n">config_entry</span><span class="p">:</span> <span class="n">config_entries</span><span class="p">.</span><span class="n">ConfigEntry</span><span class="p">,</span>
<span class="n">async_add_entities</span><span class="p">,</span>
<span class="p">):</span>
<span class="s">"""Setup sensors from a config entry created in the integrations UI."""</span>
<span class="n">config</span> <span class="o">=</span> <span class="n">hass</span><span class="p">.</span><span class="n">data</span><span class="p">[</span><span class="n">DOMAIN</span><span class="p">][</span><span class="n">config_entry</span><span class="p">.</span><span class="n">entry_id</span><span class="p">]</span>
<span class="n">some_option</span> <span class="o">=</span> <span class="n">config_entry</span><span class="p">.</span><span class="n">options</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"some_option"</span><span class="p">)</span>
<span class="n">session</span> <span class="o">=</span> <span class="n">async_get_clientsession</span><span class="p">(</span><span class="n">hass</span><span class="p">)</span>
<span class="n">github</span> <span class="o">=</span> <span class="n">GitHubAPI</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="s">"requester"</span><span class="p">,</span> <span class="n">oauth_token</span><span class="o">=</span><span class="n">config</span><span class="p">[</span><span class="n">CONF_ACCESS_TOKEN</span><span class="p">])</span>
<span class="n">sensors</span> <span class="o">=</span> <span class="p">[</span><span class="n">GitHubRepoSensor</span><span class="p">(</span><span class="n">github</span><span class="p">,</span> <span class="n">repo</span><span class="p">,</span> <span class="n">some_option</span><span class="p">)</span> <span class="k">for</span> <span class="n">repo</span> <span class="ow">in</span> <span class="n">config</span><span class="p">[</span><span class="n">CONF_REPOS</span><span class="p">]]</span>
<span class="n">async_add_entities</span><span class="p">(</span><span class="n">sensors</span><span class="p">,</span> <span class="n">update_before_add</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>
<h2 id="options-flow-in-the-github-custom-component">Options Flow in the Github Custom Component</h2>
<p>Now that I went over the general information on using an options flow, I wanted to return
to the custom component we’ve been building in this tutorial. The options flow I added
performs actions that I haven’t seen in any other options flows. Mainly it allows for
removing repos that have been added as well as adding new repos via the options flow form.
Below you can see a screenshot of what it looks like.</p>
<p><a href="/assets/images/0015_options_flow.png"><img src="/assets/images/0015_options_flow.png" alt="Options Flow" /></a></p>
<p>The multi-select allows a user to uncheck repos that they want to remove. The other two
inputs allow a user to add a new repo and give it an optional name. Clicking <code class="language-plaintext highlighter-rouge">SUBMIT</code>
will remove un-checked repos and add any new repo if one was specified.</p>
<h3 id="removing-a-repo">Removing a Repo</h3>
<p>The logic for removing repos looks like the following in our options flow class:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">updated_repos</span> <span class="o">=</span> <span class="n">deepcopy</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">config_entry</span><span class="p">.</span><span class="n">data</span><span class="p">[</span><span class="n">CONF_REPOS</span><span class="p">])</span>
<span class="c1"># Remove any unchecked repos.
</span><span class="n">removed_entities</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">entity_id</span>
<span class="k">for</span> <span class="n">entity_id</span> <span class="ow">in</span> <span class="n">repo_map</span><span class="p">.</span><span class="n">keys</span><span class="p">()</span>
<span class="k">if</span> <span class="n">entity_id</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">user_input</span><span class="p">[</span><span class="s">"repos"</span><span class="p">]</span>
<span class="p">]</span>
<span class="k">for</span> <span class="n">entity_id</span> <span class="ow">in</span> <span class="n">removed_entities</span><span class="p">:</span>
<span class="c1"># Unregister from HA
</span> <span class="n">entity_registry</span><span class="p">.</span><span class="n">async_remove</span><span class="p">(</span><span class="n">entity_id</span><span class="p">)</span>
<span class="c1"># Remove from our configured repos.
</span> <span class="n">entry</span> <span class="o">=</span> <span class="n">repo_map</span><span class="p">[</span><span class="n">entity_id</span><span class="p">]</span>
<span class="n">entry_path</span> <span class="o">=</span> <span class="n">entry</span><span class="p">.</span><span class="n">unique_id</span>
<span class="n">updated_repos</span> <span class="o">=</span> <span class="p">[</span><span class="n">e</span> <span class="k">for</span> <span class="n">e</span> <span class="ow">in</span> <span class="n">updated_repos</span> <span class="k">if</span> <span class="n">e</span><span class="p">[</span><span class="s">"path"</span><span class="p">]</span> <span class="o">!=</span> <span class="n">entry_path</span><span class="p">]</span>
</code></pre></div></div>
<p>We first determine which repos were unchecked by comparing the selected repos to the repos
that were originally configured in our <code class="language-plaintext highlighter-rouge">config_entry</code>. Then we iterate through each <code class="language-plaintext highlighter-rouge">entity_id</code>
and remove it from the entity registry first, then from our list of repos initially
configured.</p>
<h3 id="adding-a-repo">Adding a Repo</h3>
<p>If the user enters a valid value for the <code class="language-plaintext highlighter-rouge">path</code> input, we will then add a new repo. That logic
is shown below:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">user_input</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">CONF_PATH</span><span class="p">):</span>
<span class="c1"># Validate the path.
</span> <span class="n">access_token</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">hass</span><span class="p">.</span><span class="n">data</span><span class="p">[</span><span class="n">DOMAIN</span><span class="p">][</span><span class="bp">self</span><span class="p">.</span><span class="n">config_entry</span><span class="p">.</span><span class="n">entry_id</span><span class="p">][</span>
<span class="n">CONF_ACCESS_TOKEN</span>
<span class="p">]</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">await</span> <span class="n">validate_path</span><span class="p">(</span><span class="n">user_input</span><span class="p">[</span><span class="n">CONF_PATH</span><span class="p">],</span> <span class="n">access_token</span><span class="p">,</span> <span class="bp">self</span><span class="p">.</span><span class="n">hass</span><span class="p">)</span>
<span class="k">except</span> <span class="nb">ValueError</span><span class="p">:</span>
<span class="n">errors</span><span class="p">[</span><span class="s">"base"</span><span class="p">]</span> <span class="o">=</span> <span class="s">"invalid_path"</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">errors</span><span class="p">:</span>
<span class="c1"># Add the new repo.
</span> <span class="n">updated_repos</span><span class="p">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">{</span>
<span class="s">"path"</span><span class="p">:</span> <span class="n">user_input</span><span class="p">[</span><span class="n">CONF_PATH</span><span class="p">],</span>
<span class="s">"name"</span><span class="p">:</span> <span class="n">user_input</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">CONF_NAME</span><span class="p">,</span> <span class="n">user_input</span><span class="p">[</span><span class="n">CONF_PATH</span><span class="p">]),</span>
<span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>
<p>If a value was provided we first validate it to ensure it’s a real GitHub repo. If it is
not we populate the <code class="language-plaintext highlighter-rouge">errors</code> dict with the error key defined in our <code class="language-plaintext highlighter-rouge">strings.json</code>. If
there are no errors we simply append the new repository to the existing list.</p>
<h3 id="updating-the-sensors">Updating the Sensors</h3>
<p>When we succuessfully return from our options flow handler it will pass the list of
updated repos as the data keyword argument. This <code class="language-plaintext highlighter-rouge">dict</code> will get set in the <code class="language-plaintext highlighter-rouge">options</code>
property of our <code class="language-plaintext highlighter-rouge">config_entry</code> instance.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">async_create_entry</span><span class="p">(</span>
<span class="n">title</span><span class="o">=</span><span class="s">""</span><span class="p">,</span>
<span class="n">data</span><span class="o">=</span><span class="p">{</span><span class="n">CONF_REPOS</span><span class="p">:</span> <span class="n">updated_repos</span><span class="p">},</span>
<span class="p">)</span>
</code></pre></div></div>
<p>We will access that data when setting up our sensors in <code class="language-plaintext highlighter-rouge">sensor.py</code>. Before creating
our sensors we augment the initial configuration data with the updated repos which may
have had repos removed or added since the initial configuration in our config flow.</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/custom_components/github_custom/sensor.py b/custom_components/github_custom/sensor.py
index 9a62f8a..c893fa2 100644
</span><span class="gd">--- a/custom_components/github_custom/sensor.py
</span><span class="gi">+++ b/custom_components/github_custom/sensor.py
</span><span class="p">@@ -70,6 +70,9 @@</span> async def async_setup_entry(
):
"""Setup sensors from a config entry created in the integrations UI."""
config = hass.data[DOMAIN][config_entry.entry_id]
<span class="gi">+ # Update our config to include new repos and remove those that have been removed.
+ if config_entry.options:
+ config.update(config_entry.options)
</span> session = async_get_clientsession(hass)
github = GitHubAPI(session, "requester", oauth_token=config[CONF_ACCESS_TOKEN])
sensors = [GitHubRepoSensor(github, repo) for repo in config[CONF_REPOS]]
</code></pre></div></div>
<h2 id="unit-tests">Unit Tests</h2>
<p>Unit testing the options flow isn’t terribly different than testing the config flow,
but it does require a few extra steps. The test below tests the case where the user
unchecks an existing repo from the config entry.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">patch</span><span class="p">(</span><span class="s">"custom_components.github_custom.sensor.GitHubAPI"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">test_options_flow_remove_repo</span><span class="p">(</span><span class="n">m_github</span><span class="p">,</span> <span class="n">hass</span><span class="p">):</span>
<span class="s">"""Test config flow options."""</span>
<span class="n">m_instance</span> <span class="o">=</span> <span class="n">AsyncMock</span><span class="p">()</span>
<span class="n">m_instance</span><span class="p">.</span><span class="n">getitem</span> <span class="o">=</span> <span class="n">AsyncMock</span><span class="p">()</span>
<span class="n">m_github</span><span class="p">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="n">m_instance</span>
<span class="n">config_entry</span> <span class="o">=</span> <span class="n">MockConfigEntry</span><span class="p">(</span>
<span class="n">domain</span><span class="o">=</span><span class="n">DOMAIN</span><span class="p">,</span>
<span class="n">unique_id</span><span class="o">=</span><span class="s">"kodi_recently_added_media"</span><span class="p">,</span>
<span class="n">data</span><span class="o">=</span><span class="p">{</span>
<span class="n">CONF_ACCESS_TOKEN</span><span class="p">:</span> <span class="s">"access-token"</span><span class="p">,</span>
<span class="n">CONF_REPOS</span><span class="p">:</span> <span class="p">[{</span><span class="s">"path"</span><span class="p">:</span> <span class="s">"home-assistant/core"</span><span class="p">,</span> <span class="s">"name"</span><span class="p">:</span> <span class="s">"HA Core"</span><span class="p">}],</span>
<span class="p">},</span>
<span class="p">)</span>
<span class="n">config_entry</span><span class="p">.</span><span class="n">add_to_hass</span><span class="p">(</span><span class="n">hass</span><span class="p">)</span>
<span class="k">assert</span> <span class="k">await</span> <span class="n">hass</span><span class="p">.</span><span class="n">config_entries</span><span class="p">.</span><span class="n">async_setup</span><span class="p">(</span><span class="n">config_entry</span><span class="p">.</span><span class="n">entry_id</span><span class="p">)</span>
<span class="k">await</span> <span class="n">hass</span><span class="p">.</span><span class="n">async_block_till_done</span><span class="p">()</span>
<span class="c1"># show initial form
</span> <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">hass</span><span class="p">.</span><span class="n">config_entries</span><span class="p">.</span><span class="n">options</span><span class="p">.</span><span class="n">async_init</span><span class="p">(</span><span class="n">config_entry</span><span class="p">.</span><span class="n">entry_id</span><span class="p">)</span>
<span class="c1"># submit form with options
</span> <span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">hass</span><span class="p">.</span><span class="n">config_entries</span><span class="p">.</span><span class="n">options</span><span class="p">.</span><span class="n">async_configure</span><span class="p">(</span>
<span class="n">result</span><span class="p">[</span><span class="s">"flow_id"</span><span class="p">],</span> <span class="n">user_input</span><span class="o">=</span><span class="p">{</span><span class="s">"repos"</span><span class="p">:</span> <span class="p">[]}</span>
<span class="p">)</span>
<span class="k">assert</span> <span class="s">"create_entry"</span> <span class="o">==</span> <span class="n">result</span><span class="p">[</span><span class="s">"type"</span><span class="p">]</span>
<span class="k">assert</span> <span class="s">""</span> <span class="o">==</span> <span class="n">result</span><span class="p">[</span><span class="s">"title"</span><span class="p">]</span>
<span class="k">assert</span> <span class="n">result</span><span class="p">[</span><span class="s">"result"</span><span class="p">]</span> <span class="ow">is</span> <span class="bp">True</span>
<span class="k">assert</span> <span class="p">{</span><span class="n">CONF_REPOS</span><span class="p">:</span> <span class="p">[]}</span> <span class="o">==</span> <span class="n">result</span><span class="p">[</span><span class="s">"data"</span><span class="p">]</span>
</code></pre></div></div>
<p>We first need to create a mock config entry and add it to Home Assistant. Next we generate
the initial options flow and capture the flow id. The flow id is used when we call
<code class="language-plaintext highlighter-rouge">hass.config_entries.options.async_configure</code> and pass in our <code class="language-plaintext highlighter-rouge">user_input</code> data. In this
case we are simulating unchecking the only repo that was configured.</p>
<p>Check out the <a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_2/">post on unit testing</a> for more details on the fixtures and helpers used
here.</p>
<h2 id="next-steps">Next Steps</h2>
<p>At this point we now have a fully functional custom component that can be configured via
the configuration UI or a <code class="language-plaintext highlighter-rouge">configuration.yaml</code> file. In the last post in this series I
will briefly cover testing and debugging your component locally using the
<a href="https://code.visualstudio.com/">Visual Studio Code</a> devcontainer provided by
<a href="https://developers.home-assistant.io/docs/development_environment#developing-with-visual-studio-code--devcontainer">Home Assistant</a>.</p>Aaron GodfreyPart 4 of building a custom component in Home Assistant. In this post we'll examine how to add an options flow so that your component can have additional options configured through the configuration UI in Home Assistant.Building a Home Assistant Custom Component Part 3: Config Flow2020-11-23T00:00:00-08:002020-11-23T00:00:00-08:00https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_3<div class="notice--info">
<p>This is the third part of a multi-part tutorial to create a Home Assistant custom component.</p>
<ul>
<li><a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_1/">Part 1 - Project Structure and Basics</a></li>
<li><a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_2/">Part 2 - Unit Testing and Continuous Integration</a></li>
<li>Part 3 - Config Flow (Reading Now!)</li>
<li><a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_4/">Part 4 - Options Flow</a></li>
<li><a href="https://aarongodfrey.dev/home%20automation/building_a_home_assistant_custom_component_part_5/">Part 5 - Debugging</a></li>
</ul>
</div>
<h2 id="introduction">Introduction</h2>
<p>In this post we will be updating the custom component to be able to be configurable
via the UI, by adding a <a href="https://developers.home-assistant.io/docs/config_entries_config_flow_handler">config flow</a>.
We are still using the same example project, <a href="https://github.com/boralyl/github-custom-component-tutorial">github-custom-component</a>.
You can find the diff for this post on the <a href="https://github.com/boralyl/github-custom-component-tutorial/compare/feature/part2...feature/part3">feature/part3</a> branch.</p>
<h2 id="updating-manifestjson">Updating manifest.json</h2>
<p>The first step is updating our <a href="https://github.com/boralyl/github-custom-component-tutorial/blob/master/custom_components/github_custom/manifest.json">manifest.json</a>.
We set the <code class="language-plaintext highlighter-rouge">config_flow</code> key to <code class="language-plaintext highlighter-rouge">true</code>, this will let Home Assistant know that this
component can be added via the configuration UI.</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code> {
"codeowners": ["@boralyl"],
<span class="gd">- "config_flow": false,
</span><span class="gi">+ "config_flow": true,
</span> "dependencies": [],
"documentation": "https://github.com/boralyl/github-custom-component-tutorial",
"domain": "github_custom",
</code></pre></div></div>
<h2 id="adding-the-config-flow">Adding the Config Flow</h2>
<p>Next up we will create our <a href="https://github.com/boralyl/github-custom-component-tutorial/blob/master/custom_components/github_custom/config_flow.py">config_flow.py</a> file.
Within this file we will extend the <code class="language-plaintext highlighter-rouge">ConfigFlow</code> class and define the different steps
that should show up in the UI when a user is setting up the component for the first time.</p>
<p>As of writing, having a component that requires an unknown sized list of configuration
values isn’t the easiest thing to do via config flow. To try to get around this limitation,
I decided to design the config flow to have 2 steps. The first step asks for the user’s
GitHub access token and optional enterprise server URL. After submitting that information
the user precedes to the second step which allows them to enter a repository and optional
name for it. To allow the user to add an additional repository I added a checkbox that if
checked will repeat the second step. The user can do this as many times as they want until
they have added all of the repositories that they want sensors created for.</p>
<h3 id="user-step">User Step</h3>
<p>The <a href="https://github.com/boralyl/github-custom-component-tutorial/blob/master/custom_components/github_custom/config_flow.py#L55">async_step_user</a> method of our config flow class is
invoked when a user clicks the add button and chooses the GitHub Custom integration.</p>
<p><a href="/assets/images/0014_init_flow.gif"><img src="/assets/images/0014_init_flow.gif" alt="Initializing the config flow" /></a></p>
<p>Let’s walk through what this method does.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">async_step_user</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_input</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">):</span>
<span class="s">"""Invoked when a user initiates a flow via the user interface."""</span>
<span class="n">errors</span><span class="p">:</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">if</span> <span class="n">user_input</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">await</span> <span class="n">validate_auth</span><span class="p">(</span><span class="n">user_input</span><span class="p">[</span><span class="n">CONF_ACCESS_TOKEN</span><span class="p">],</span> <span class="bp">self</span><span class="p">.</span><span class="n">hass</span><span class="p">)</span>
<span class="k">except</span> <span class="nb">ValueError</span><span class="p">:</span>
<span class="n">errors</span><span class="p">[</span><span class="s">"base"</span><span class="p">]</span> <span class="o">=</span> <span class="s">"auth"</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">errors</span><span class="p">:</span>
<span class="c1"># Input is valid, set data.
</span> <span class="bp">self</span><span class="p">.</span><span class="n">data</span> <span class="o">=</span> <span class="n">user_input</span>
<span class="bp">self</span><span class="p">.</span><span class="n">data</span><span class="p">[</span><span class="n">CONF_REPOS</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="c1"># Return the form of the next step.
</span> <span class="k">return</span> <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">async_step_repo</span><span class="p">()</span>
<span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">async_show_form</span><span class="p">(</span>
<span class="n">step_id</span><span class="o">=</span><span class="s">"user"</span><span class="p">,</span> <span class="n">data_schema</span><span class="o">=</span><span class="n">AUTH_SCHEMA</span><span class="p">,</span> <span class="n">errors</span><span class="o">=</span><span class="n">errors</span>
<span class="p">)</span>
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">user_input</code> variable defaults to <code class="language-plaintext highlighter-rouge">None</code> when this step is first called. When the
user clicks the submit button the variable will be populated with a dict containing the
data they entered. Home Assistant will do some basic validation on your behalf based on
the data schema that you defined. I added some additional validation that will use the
provided access token to ensure it’s validity. If it fails we set the base error to
<code class="language-plaintext highlighter-rouge">auth</code>. This value corresponds with the errors object in the <a href="https://github.com/boralyl/github-custom-component-tutorial/blob/master/custom_components/github_custom/strings.json#L4">strings.json</a> and will display the description defined there.</p>
<p>If there are no errors, the data is stored in the <code class="language-plaintext highlighter-rouge">self.data</code> attribute of the class. In
addition to storing the entered data I also initialize an empty list for the repositories
that will be added in the next step. Finally, we call the next step’s method <code class="language-plaintext highlighter-rouge">asyn_step_repo</code>
to advance the user to the second form where they can enter all of the GitHub repositories
that they want to monitor.</p>
<h3 id="repo-step">Repo Step</h3>
<p>The <a href="https://github.com/boralyl/github-custom-component-tutorial/blob/master/custom_components/github_custom/config_flow.py#L74">async_step_repo</a> method is invoked after the
user successfully completes the initial step. This step is responsible for showing a form
to enter repository information. If the user ticks the <code class="language-plaintext highlighter-rouge">Add another repo</code> checkbox then
we save the entered data and reset the form on submit.</p>
<p><a href="/assets/images/0014_repo_flow.gif"><img src="/assets/images/0014_repo_flow.gif" alt="Repo flow step" /></a></p>
<p>The logic in this method is very similar to the first step.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">async_step_repo</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_input</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">):</span>
<span class="s">"""Second step in config flow to add a repo to watch."""</span>
<span class="n">errors</span><span class="p">:</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">if</span> <span class="n">user_input</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
<span class="c1"># Validate the path.
</span> <span class="k">try</span><span class="p">:</span>
<span class="n">validate_path</span><span class="p">(</span><span class="n">user_input</span><span class="p">[</span><span class="n">CONF_PATH</span><span class="p">])</span>
<span class="k">except</span> <span class="nb">ValueError</span><span class="p">:</span>
<span class="n">errors</span><span class="p">[</span><span class="s">"base"</span><span class="p">]</span> <span class="o">=</span> <span class="s">"invalid_path"</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">errors</span><span class="p">:</span>
<span class="c1"># Input is valid, set data.
</span> <span class="bp">self</span><span class="p">.</span><span class="n">data</span><span class="p">[</span><span class="n">CONF_REPOS</span><span class="p">].</span><span class="n">append</span><span class="p">(</span>
<span class="p">{</span>
<span class="s">"path"</span><span class="p">:</span> <span class="n">user_input</span><span class="p">[</span><span class="n">CONF_PATH</span><span class="p">],</span>
<span class="s">"name"</span><span class="p">:</span> <span class="n">user_input</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">CONF_NAME</span><span class="p">,</span> <span class="n">user_input</span><span class="p">[</span><span class="n">CONF_PATH</span><span class="p">]),</span>
<span class="p">}</span>
<span class="p">)</span>
<span class="c1"># If user ticked the box show this form again so they can add an
</span> <span class="c1"># additional repo.
</span> <span class="k">if</span> <span class="n">user_input</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"add_another"</span><span class="p">,</span> <span class="bp">False</span><span class="p">):</span>
<span class="k">return</span> <span class="k">await</span> <span class="bp">self</span><span class="p">.</span><span class="n">async_step_repo</span><span class="p">()</span>
<span class="c1"># User is done adding repos, create the config entry.
</span> <span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">async_create_entry</span><span class="p">(</span><span class="n">title</span><span class="o">=</span><span class="s">"GitHub Custom"</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">data</span><span class="p">)</span>
<span class="k">return</span> <span class="bp">self</span><span class="p">.</span><span class="n">async_show_form</span><span class="p">(</span>
<span class="n">step_id</span><span class="o">=</span><span class="s">"repo"</span><span class="p">,</span> <span class="n">data_schema</span><span class="o">=</span><span class="n">REPO_SCHEMA</span><span class="p">,</span> <span class="n">errors</span><span class="o">=</span><span class="n">errors</span>
<span class="p">)</span>
</code></pre></div></div>
<p>One key difference is that we continue to return the current step if the <code class="language-plaintext highlighter-rouge">add_another</code>
checkbox is checked. When the user is done the final step is to call the
<code class="language-plaintext highlighter-rouge">async_create_entry</code> method which will create our config entry and register it with
Home Assistant.</p>
<h2 id="setting-up-the-config-entry">Setting Up the Config Entry</h2>
<p>The next thing that we need to do is set up our sensors from the config entry that was
created. In the <code class="language-plaintext highlighter-rouge">__init__.py</code> file we define an <code class="language-plaintext highlighter-rouge">async_setup_entry</code> function that will
forward the task to the sensor platform. For more details on how this works I encourage
you to checkout out the excellent <a href="https://developers.home-assistant.io/docs/config_entries_index/">documentation on the subject</a>.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">async_setup_entry</span><span class="p">(</span>
<span class="n">hass</span><span class="p">:</span> <span class="n">core</span><span class="p">.</span><span class="n">HomeAssistant</span><span class="p">,</span> <span class="n">entry</span><span class="p">:</span> <span class="n">config_entries</span><span class="p">.</span><span class="n">ConfigEntry</span>
<span class="p">)</span> <span class="o">-></span> <span class="nb">bool</span><span class="p">:</span>
<span class="s">"""Set up platform from a ConfigEntry."""</span>
<span class="n">hass</span><span class="p">.</span><span class="n">data</span><span class="p">.</span><span class="n">setdefault</span><span class="p">(</span><span class="n">DOMAIN</span><span class="p">,</span> <span class="p">{})</span>
<span class="n">hass</span><span class="p">.</span><span class="n">data</span><span class="p">[</span><span class="n">DOMAIN</span><span class="p">][</span><span class="n">entry</span><span class="p">.</span><span class="n">entry_id</span><span class="p">]</span> <span class="o">=</span> <span class="n">entry</span><span class="p">.</span><span class="n">data</span>
<span class="c1"># Forward the setup to the sensor platform.
</span> <span class="n">hass</span><span class="p">.</span><span class="n">async_create_task</span><span class="p">(</span>
<span class="n">hass</span><span class="p">.</span><span class="n">config_entries</span><span class="p">.</span><span class="n">async_forward_entry_setup</span><span class="p">(</span><span class="n">entry</span><span class="p">,</span> <span class="s">"sensor"</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">return</span> <span class="bp">True</span>
</code></pre></div></div>
<p>In the function above we are storing the data for the config entry in hass under our
<code class="language-plaintext highlighter-rouge">DOMAIN</code> key. This will allow us to store multiple config entries in the event the user
wants to setup the integration multiple times. Perhaps they have an enterprise server
account for work and a regular personal account. They can set up 2 different entries,
corresponding to each of those cases.</p>
<p>We then forward the setup to the <code class="language-plaintext highlighter-rouge">sensor</code> platform. In <a href="https://github.com/boralyl/github-custom-component-tutorial/blob/master/custom_components/github_custom/sensor.py">sensor.py</a>
we add an <code class="language-plaintext highlighter-rouge">async_setup_entry</code> function which will accept a config entry instance and create
the sensors for the component. You will notice this function looks nearly identical to the
<code class="language-plaintext highlighter-rouge">async_setup_platform</code> function below it which is used for setting up the sensors from
<code class="language-plaintext highlighter-rouge">configuration.yaml</code>. The only difference is we retrieve the config data from the config
entry instance.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">async_setup_entry</span><span class="p">(</span>
<span class="n">hass</span><span class="p">:</span> <span class="n">core</span><span class="p">.</span><span class="n">HomeAssistant</span><span class="p">,</span>
<span class="n">config_entry</span><span class="p">:</span> <span class="n">config_entries</span><span class="p">.</span><span class="n">ConfigEntry</span><span class="p">,</span>
<span class="n">async_add_entities</span><span class="p">,</span>
<span class="p">):</span>
<span class="s">"""Setup sensors from a config entry created in the integrations UI."""</span>
<span class="n">config</span> <span class="o">=</span> <span class="n">hass</span><span class="p">.</span><span class="n">data</span><span class="p">[</span><span class="n">DOMAIN</span><span class="p">][</span><span class="n">config_entry</span><span class="p">.</span><span class="n">entry_id</span><span class="p">]</span>
<span class="n">session</span> <span class="o">=</span> <span class="n">async_get_clientsession</span><span class="p">(</span><span class="n">hass</span><span class="p">)</span>
<span class="n">github</span> <span class="o">=</span> <span class="n">GitHubAPI</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="s">"requester"</span><span class="p">,</span> <span class="n">oauth_token</span><span class="o">=</span><span class="n">config</span><span class="p">[</span><span class="n">CONF_ACCESS_TOKEN</span><span class="p">])</span>
<span class="n">sensors</span> <span class="o">=</span> <span class="p">[</span><span class="n">GitHubRepoSensor</span><span class="p">(</span><span class="n">github</span><span class="p">,</span> <span class="n">repo</span><span class="p">)</span> <span class="k">for</span> <span class="n">repo</span> <span class="ow">in</span> <span class="n">config</span><span class="p">[</span><span class="n">CONF_REPOS</span><span class="p">]]</span>
<span class="n">async_add_entities</span><span class="p">(</span><span class="n">sensors</span><span class="p">,</span> <span class="n">update_before_add</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>
<h2 id="translations">Translations</h2>
<p>I briefly touched on <a href="https://github.com/boralyl/github-custom-component-tutorial/blob/master/custom_components/github_custom/strings.json">strings.json</a> when explaining how errors are defined. This file
contains strings used in the config flow process. I copied <code class="language-plaintext highlighter-rouge">strings.json</code> into the <code class="language-plaintext highlighter-rouge">translations</code>
folder and renamed it <code class="language-plaintext highlighter-rouge">en.json</code> for the English translation. You can add as many translation files as you would like, they
should be named using the 2 letter ISO 639-2 language code. All the keys should be the same as
the <code class="language-plaintext highlighter-rouge">strings.json</code> and the values should be the translated string. For example, this is
the Norwegian translation file for another one of my custom components: <a href="https://github.com/boralyl/steam-wishlist/blob/master/custom_components/steam_wishlist/translations/nb.json">nb.json</a>.</p>
<p>For more information on translations in custom components check out the <a href="https://developers.home-assistant.io/docs/internationalization/custom_integration/#translation-strings">official documentation</a>.</p>
<h2 id="unit-tests">Unit Tests</h2>
<p>I wanted to briefly touch on how to unit test the config flow. If you install and use
<a href="https://github.com/MatthewFlamm/pytest-homeassistant-custom-component">pytest-home-assistant-custom-component</a> you can make use of some pytest fixtures that make
testing much simpler.</p>
<p>Let’s take a look at a test to verify that we display an error if the GitHub access token
is invalid.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">patch</span><span class="p">(</span><span class="s">"custom_components.github_custom.config_flow.validate_auth"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">test_flow_user_init_invalid_auth_token</span><span class="p">(</span><span class="n">m_validate_auth</span><span class="p">,</span> <span class="n">hass</span><span class="p">):</span>
<span class="s">"""Test errors populated when auth token is invalid."""</span>
<span class="n">m_validate_auth</span><span class="p">.</span><span class="n">side_effect</span> <span class="o">=</span> <span class="nb">ValueError</span>
<span class="n">_result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">hass</span><span class="p">.</span><span class="n">config_entries</span><span class="p">.</span><span class="n">flow</span><span class="p">.</span><span class="n">async_init</span><span class="p">(</span>
<span class="n">config_flow</span><span class="p">.</span><span class="n">DOMAIN</span><span class="p">,</span> <span class="n">context</span><span class="o">=</span><span class="p">{</span><span class="s">"source"</span><span class="p">:</span> <span class="s">"user"</span><span class="p">}</span>
<span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">hass</span><span class="p">.</span><span class="n">config_entries</span><span class="p">.</span><span class="n">flow</span><span class="p">.</span><span class="n">async_configure</span><span class="p">(</span>
<span class="n">_result</span><span class="p">[</span><span class="s">"flow_id"</span><span class="p">],</span> <span class="n">user_input</span><span class="o">=</span><span class="p">{</span><span class="n">CONF_ACCESS_TOKEN</span><span class="p">:</span> <span class="s">"bad"</span><span class="p">}</span>
<span class="p">)</span>
<span class="k">assert</span> <span class="p">{</span><span class="s">"base"</span><span class="p">:</span> <span class="s">"auth"</span><span class="p">}</span> <span class="o">==</span> <span class="n">result</span><span class="p">[</span><span class="s">"errors"</span><span class="p">]</span>
</code></pre></div></div>
<p>In this test we mock the <code class="language-plaintext highlighter-rouge">validate_auth</code> function and cause it to raise a <code class="language-plaintext highlighter-rouge">ValueError</code>.
The <code class="language-plaintext highlighter-rouge">hass</code> parameter passed to our test comes from a pytest fixture installed by
<code class="language-plaintext highlighter-rouge">pytest-home-assistant-custom-component</code>. First we initialize the flow by specifying
our domain and which step, in this case <code class="language-plaintext highlighter-rouge">user</code>. We then run that step in the flow and
pass in our user input. The result contains an <code class="language-plaintext highlighter-rouge">errors</code> key that we assert matches our
expectation.</p>
<h2 id="next-steps">Next Steps</h2>
<p>With this code in place we can now configure and add repos via the UI instead of the
<code class="language-plaintext highlighter-rouge">configuration.yaml</code> file. When you are developing a new config flow, make sure to
do a hard referesh in your browser when you’ve modified files and restarted Home Assistant.
I’ve noticed the browser can cache some of this information causing you to see outdated
data.</p>
<p>The one glaring issue with our implementation is that there is no way to remove or add
new repositories without creating a new config entry by initializing the flow again. While
this works, it’s not ideal as you need to re-enter your GitHub access token each time.
In the next post we’ll look into how we might be able to use the <a href="https://developers.home-assistant.io/docs/config_entries_options_flow_handler">OptionsFlowHandler</a> to get around this limitation.</p>Aaron GodfreyPart 3 of building a custom component in Home Assistant. In this post we'll examine how to write a config flow so that your component can be added and configured through the configuration UI in Home Assistant.