<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Software on Tom's Journal</title><link>https://blog.tompaulus.com/tag/software/</link><description>Thoughts, stories and ideas about Events, Life, Software, and Everything Else.</description><generator>Hugo</generator><language>en</language><lastBuildDate>Fri, 11 Sep 2026 16:56:13 +0000</lastBuildDate><atom:link href="https://blog.tompaulus.com/tag/software/rss.xml" rel="self" type="application/rss+xml"/><item><title>Fledging the Nest</title><link>https://blog.tompaulus.com/fledging-the-nest/</link><pubDate>Wed, 24 Jun 2026 15:43:27 +0000</pubDate><guid>https://blog.tompaulus.com/fledging-the-nest/</guid><description>When we moved in to our house just over a year ago, it came with a Nest Thermostat. It worked well enough, and after paying the 3rd Party integration tax to Google, it worked with Home Assistant, so we left it at that – we had more impactful improvements to make. When we upgraded the 30+ year old HVAC system a few months later, the Air Handler that replaced our furnace had support for 24V …</description><category>Software</category><media:content medium="image" url="https://blog.tompaulus.com/fledging-the-nest/media/external/unsplash-E8fbT98Z4DE.jpg"/><content:encoded>&lt;p&gt;When we moved in to our house just over a year ago, it came with a Nest Thermostat. It worked well enough, and after paying the &lt;a href="https://www.home-assistant.io/integrations/nest/"&gt;3rd Party integration tax&lt;/a&gt; to Google, it worked with &lt;a href="https://www.home-assistant.io/"&gt;Home Assistant&lt;/a&gt;, so we left it at that – we had more impactful improvements to make. When we upgraded the 30+ year old HVAC system a few months later, the Air Handler that replaced our furnace had support for 24V thermostats (for backwards compatibility), so the Nest survived that change as well. However, as the months went on and I made more automations to coordinate the thermostats across the house (the HVAC upgrade added two additional heads to the mix), the reliance on Google&amp;rsquo;s cloud meant that updates to the Nest thermostat from Home Assistant were both slower than the local thermostats, and crucially, relied on the internet for them to work.&lt;/p&gt;
&lt;p&gt;I had looked around for other 24V thermostats that integrate directly with Home Assistant, but found nothing that stood out to me. There was a Honeywell Z-Wave thermostat that had good reviews, but was difficult to find as it had been discontinued in favor of the version that had WiFi built-in (and consequently used their cloud). EcoBee&amp;rsquo;s thermostats have HomeKit support, and therefore local control in Home Assistant, but the reviews were mixed, and the thermostats themselves are also rather expensive; so the Nest got to keep its spot on the wall a while longer.&lt;/p&gt;
&lt;p&gt;Meanwhile, the two new mini-split heads had been integrated into Home Assistant a few weeks after they were installed. This was trivial thanks to the &lt;a href="https://cloudfree.shop/product/ductless-hvac-wi-fi-module/"&gt;Smlite Ductless HVAC Wi-Fi Module&lt;/a&gt;. Unlike the air handler, these units were designed for a WiFi module to be plugged directly in to them to enable remote control. As for the air handler, &lt;a href="https://www.dcne.com/product/dls-wired-control-programmable-ksacn1401aaa"&gt;the wired remote accessory&lt;/a&gt; is the closest thing to a thermostat that is not a 24V thermostat. While it has built-in WiFi, it only integrates with the brand&amp;rsquo;s cloud, with mixed Home Assistant compatibly, and again relies on a working internet connection. A bit more searching and staring at the air handler&amp;rsquo;s control board revealed a path forward, the central communications bus otherwise used to network multiple units together. The community had reverse engineered the protocol used (&lt;a href="https://codeberg.org/xye/xye"&gt;XYE&lt;/a&gt;), and had built an &lt;a href="https://github.com/HomeOps/ESPHome-Midea-XYE"&gt;ESPHome custom component&lt;/a&gt; to integrate Home Assistant with XYE.&lt;/p&gt;
&lt;h3 id="the-requirements"&gt;The Requirements&lt;/h3&gt;
&lt;p&gt;Before we go too much further, I should clarify what I was looking for in a thermostat. This should not be too much to ask for, but in the era of the &lt;a href="https://x.com/internetofshit?lang=en"&gt;Internet of Shit&lt;/a&gt;, every smart home device wants to connect to their proprietary cloud. This would not be so bad if these proprietary clouds supported third party integrations (like Home Assistant), but where is the money in that. Add to that the inevitability of being left with a very nice paperweight (or &lt;a href="https://www.businessinsider.com/what-happened-when-petnet-smart-pet-feeder-went-out-of-business-2021-6"&gt;empty kibble bowl&lt;/a&gt;) when the cloud service goes out of business (or is deprecated).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fully Local – No Cloud Dependency&lt;/li&gt;
&lt;li&gt;Self Contained – Everything needed to fit within the controls area of the Air Handler&lt;/li&gt;
&lt;li&gt;Same capabilities as we had with the 24V thermostat – set desired temperature, set desired mode (heating / cooling), and read the temperature of the rooms affected by the unit.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since I could not find any thermostats off the shelf that addressed all of these desires, without introducing yet another cloud dependency, I decided to build my own.&lt;/p&gt;
&lt;h3 id="hardware"&gt;Hardware&lt;/h3&gt;
&lt;p&gt;It had been a while since I worked on hardware, but the various GitHub repos I landed on helped me to assemble a rough bill of materials. We needed only 3 main things: an ESP32 based micro-controller, a RS485 transceiver, and an AC to DC converter. The AC to DC converter was result of the self-containment requirement as the only power source available inside the control cabinet that was not 120VAC was the 24VAC terminals (R &amp;amp; C) that would otherwise power a traditional thermostat. I went to school for Computer Science where we all, for the most part, gladly abstracted away the hardware our precious programs ran on to the Computer Engineering and Electrical Engineering departments in the buildings down the road. Consequently, converting between AC and DC was not something I was particularly looking forward to. I bickered with Claude for a while about this problem and ended up with a simple enough 2 stage AC to DC conversion that while not super cost effective, was simple enough for me to build and understand.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2026/06/Schematic-1.jpg" alt="Schematic"&gt;&lt;/p&gt;
&lt;h4 id="bill-of-materials"&gt;Bill of Materials&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.digikey.com/en/products/detail/adafruit-industries-llc/5426/21283802?s=N4IgTCBcDaIIwFYwA4C0CAsYBsqByAIiALoC%2BQA"&gt;QT PY ESP32-S3 NO PSRAM&lt;/a&gt; - The microprocessor driving the whole show&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.digikey.com/en/products/detail/analog-devices-inc-maxim-integrated/MAX485CPA/948026?s=N4IgTCBcDaILIEEAaAWAHAVgMIAUEGoBaAOQBEQBdAXyA"&gt;MAX485CPA+&lt;/a&gt; - RS485 transceiver, since XYE runs over RS485&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.digikey.com/en/products/detail/mcc-micro-commercial-components/DB107-BP/773574?s=N4IgTCBcDaICICECMAGA7AWgQBQLIGUMA5OEAXQF8g"&gt;DB107-BP&lt;/a&gt; - A full bridge rectifier, converting AC to choppy DC&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.digikey.com/en/products/detail/panasonic-industry/ECA-1HM102B/2688712?s=N4IgTCBcDaIAoEYCcBWMSDCAVEBdAvkA"&gt;ECA-1HM102B&lt;/a&gt; - A 1000UF soothing capacitor&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.digikey.com/en/products/detail/traco-power/TSR-1-2450/9383780?s=N4IgTCBcDaIIwE4CscC0YDsAWDqByAIiALoC%2BQA"&gt;TSR 1-2450&lt;/a&gt; - DC to DC Converter to give us a smooth 5V DC bus&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.digikey.com/en/products/detail/adafruit-industries-llc/571/5353603"&gt;Adafruit 1/2 PermaProto&lt;/a&gt; - My favorite way to go from breadboard to perf-board&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.digikey.com/en/products/detail/on-shore-technology-inc/OSTVN02A150/1588862"&gt;OSTVN02A150&lt;/a&gt;, &lt;a href="https://www.digikey.com/en/products/detail/on-shore-technology-inc/OSTVN03A150/1588863"&gt;OSTVN03A150&lt;/a&gt; &amp;amp; &lt;a href="https://www.digikey.com/en/products/detail/assmann-wsw-components/A-08-LC-TT/821740"&gt;A 08-LC-TT&lt;/a&gt; - Terminal Blocks &amp;amp; IC Sockets&lt;/li&gt;
&lt;li&gt;1 10kΩ and 1 20kΩ ¼W resistor, and some wire&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="software"&gt;Software&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://esphome.io/"&gt;ESPHome&lt;/a&gt; is an absolute delight and makes integrating the real-world into Home Assistant a joy. I got my start in hardware over 18 years ago with an Arduino NG thanks to my &lt;a href="https://wolfpaulus.com/"&gt;Dad&lt;/a&gt; and things have come a long way since then. ESPHome makes it super simple to utilize ESP32, ESP8266, and RP2040 microcontrollers into build custom smart-home devices. You write YAML files that declare the various components and how they should interact with one another, and ESPHome generates and compiles the relevant C++ code before uploading it to your device. Could you do everything ESPHome does yourself? Sure, but I don&amp;rsquo;t feel like writing support for OTA, WiFi fallback, and all the other nice things that ESPHome brings. When ESPHome does not have support for something, you can extend it via External Components.&lt;/p&gt;
&lt;p&gt;Another wonderful feature of ESPHome is that you can pull data out of Home Assistant from your embedded device. This is particularly helpful when you want to supply the Air Handler with the room temperature which comes from a few Zigbee Temperature Sensors. All that leads us to a config file that looks something like this&amp;hellip;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;esphome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;air-handler&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;friendly_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Air Handler&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;esp32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;esp32s3&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;framework&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;arduino&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Climate is not supported in esp-idf&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;midea_xye&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;INFO&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;WARN&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;encryption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="l"&gt;secret air_handler__encryption_key&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;ota&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;esphome&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="l"&gt;secret ota_password&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;wifi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ssid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="l"&gt;secret wifi_ssid&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="l"&gt;secret wifi_password&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ssid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Air Handler Fallback Hotspot&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;!&lt;span class="l"&gt;secret fallback_password&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;captive_portal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;web_server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;external_components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;git&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://github.com/tpaulus/ESPHome-Midea-XYE&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;1min&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;components&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="l"&gt;midea_xye]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#UART settings for RS-485 converter&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;uart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# UART1 on QtPy&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tx_pin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;17&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rx_pin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;baud_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4800&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;flow_control_pin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GPIO9&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;sensor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;homeassistant&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ha_room_temp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;sensor.air_handler_room_temperature&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;climate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;midea_xye&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Air Handler&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;3s&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;500ms&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;use_fahrenheit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;follow_me_sensor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ha_room_temp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sync_fan_mode_from_device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="integration"&gt;Integration&lt;/h3&gt;
&lt;p&gt;Anyone who has worked in engineering long enough knows that building the individual components is only half the battle, the true &lt;em&gt;fun&lt;/em&gt; comes from trying to integrate all of the disparate pieces, especially when the interface between those pieces is fuzzy. That was especially true in this case. I assembled all of the hardware, but instead of using the resistor-divider for the RX line, I had a TXB0140 level-shifter in its place. I hooked it up to the air handler control board and got &lt;code&gt;00&lt;/code&gt; out. I hooked it up to the Air Handler and got back a slew of error messages: &lt;code&gt;Bad response length (0 bytes, expected 32) for Command C0; resyncing&lt;/code&gt;. Time to start troubleshooting.&lt;/p&gt;
&lt;h4 id="is-it-the-hardware"&gt;Is it the hardware?&lt;/h4&gt;
&lt;p&gt;The easiest answer here would be to grab a logic analyzer or a fancy digital oscilloscope and probe the X and Y (A and B for RS485) and see what was going on. Only one problem, I do not own an oscilloscope, let alone a logic analyzer. I have a multimeter, tenacity, and that&amp;rsquo;s it. More bickering with AI followed, the TXB0140 went out, in its place came a simple resistor divider – one less variable. The breadboard made its way in and out of the unit, up and down the stairs at least 5 times. Together with AI, I verified each part of the hardware to ensure that the basic behaviors were as expected. I had reached the point where there was either a software problem, or a comm bus issue.&lt;/p&gt;
&lt;h4 id="is-it-the-software"&gt;Is it the software?&lt;/h4&gt;
&lt;p&gt;I had started with fork of &lt;a href="https://github.com/HomeOps"&gt;HomeOps&lt;/a&gt;&amp;rsquo;s &lt;a href="https://github.com/HomeOps/ESPHome-Midea-XYE"&gt;ESPHome-Midea-XYE&lt;/a&gt; repo that applied a bug fix &lt;a href="https://github.com/Seerenity"&gt;Seerenity&lt;/a&gt; had made which supposedly fixed a dead-lock issue. I had initially sent AI into the mines to add support for Flow-control, as the MAX485 does not have automatic flow control, but during the hardware debugging phase, I had discovered the ESP32 has built-in support for Flow Control, so the custom flow-control changes also went out the window. However, much like with the hardware, there were too many variables. So, with the help of AI (once again), we took a much simpler &lt;a href="https://github.com/wtahler/esphome-mideaXYE-rs485/blob/main/esphome-mideaXYE.yaml"&gt;example&lt;/a&gt; from &lt;a href="https://github.com/wtahler"&gt;wtahler&lt;/a&gt; and manually constructed frames to send down the wire. Crickets&amp;hellip;&lt;/p&gt;
&lt;p&gt;Back to the hardware, AI suggested to permanently ground the Receive Enable pin on the MAX485. This would mean that we get an echo, since we would receive our own transmission, but it would eliminate yet another variable. This lead to a &lt;em&gt;key insight&lt;/em&gt; - we could hear our own messages. This proved that the hardware was working correctly, and we could send bytes from the ESP32 and parse them back again. That left only a communications issue.&lt;/p&gt;
&lt;h4 id="is-it-the-air-handler"&gt;Is it the Air Handler?&lt;/h4&gt;
&lt;p&gt;If I had one piece of feedback for the folks at Carrier about the 45MBAA Air Handler, it would be that the hardware documentation sucks. There are at least 10 dip-switch blocks across the unit and 3 different rotary encoders with tiny labels and close to no documentation on their purpose. The diagrams that are there are confusing and leave the reader to make assumptions. The unit had been configured for a 24V thermostat, this means setting SW1 (A block of 4 dip-switches) to &lt;code&gt;On On On Off&lt;/code&gt;. The sticker on the front door says for Wired Control, set them to &lt;code&gt;Off On On On&lt;/code&gt;. This was a lie - in tiny letters in the corner of the control board layout was the key &amp;ldquo;All DIP Switches default to Off&amp;rdquo;. So, I turned off the unit again, and set all of SW1 to Off, and turned it back on. This time, the seven-segment display on the unit was blank. The immediate first thought was &lt;em&gt;did I just release the magic (expensive) blue smoke from the control board?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The panic was thankfully short lived. The power board (right behind the control board), had a green LED light, so it wasn&amp;rsquo;t completely dead. The control board has only a single input, a tactile button next to the seven-segment display. So, like a curious Chile (or cat), I pushed the button and the display flashed on, &lt;code&gt;EF&lt;/code&gt;. I pushed the button again, &lt;code&gt;75&lt;/code&gt;. I pushed the button a third time and the display extinguished. This was different, before, it always read &lt;code&gt;00&lt;/code&gt; on the display, which I had learned to mean idle, since it went to &lt;code&gt;01&lt;/code&gt; for first stage cooling, &lt;code&gt;02&lt;/code&gt; for second stage, and &lt;code&gt;04&lt;/code&gt; meant heat mode.&lt;/p&gt;
&lt;p&gt;I went back upstairs to my desk and sent a new set of serial commands to the control board, and this time it got a response. Not just the echo of the command, but a response from the unit. It was alive and working! I changed the ESPHome configuration back to use the custom component and made a few more fixes to get support for the various modes into the Home Assistant climate integration and to fix the representation of the Follow-Me temperature, but it was working!&lt;/p&gt;
&lt;p&gt;My fork of the Custom Component is on GitHub for those also looking to convert their Carrier (Midea) Air Handler to direct control.&lt;/p&gt;
&lt;figure class="kg-card kg-bookmark-card"&gt;
&lt;a class="kg-bookmark-container" href="https://github.com/tpaulus/ESPHome-Midea-XYE"&gt;
&lt;div class="kg-bookmark-content"&gt;
&lt;div class="kg-bookmark-title"&gt;tpaulus/ESPHome-Midea-XYE&lt;/div&gt;
&lt;div class="kg-bookmark-description"&gt;ESPHome external component for controlling Midea HVAC systems over the XYE/CCM RS‑485 bus. Provides a native Home Assistant climate entity with full mode, fan, and setpoint support.&lt;/div&gt;
&lt;div class="kg-bookmark-metadata"&gt;
&lt;img class="kg-bookmark-icon" src="https://github.githubassets.com/favicons/favicon.svg" alt=""&gt;
&lt;span class="kg-bookmark-author"&gt;tpaulus&lt;/span&gt;
&lt;span class="kg-bookmark-publisher"&gt;GitHub&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="kg-bookmark-thumbnail"&gt;&lt;img src="https://opengraph.githubassets.com/1/tpaulus/ESPHome-Midea-XYE" alt=""&gt;&lt;/div&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;h3 id="keeping-it-dumb"&gt;Keeping it dumb&lt;/h3&gt;
&lt;p&gt;So having the air handler in Home Assistant is fantastic, but as a general rule I try to preserve physical controls for all of the things we make smart across the house. This means that at 3am when you want a glass of water, you don&amp;rsquo;t need to get your phone from the room next door and stare into the sun, just to nudge on the lights.&lt;/p&gt;
&lt;p&gt;For lighting, we use the Lutron Caseta family of switches which preserve the ability to press a button and have the lights turn on/off. When we added smart shades, a similar button went on the wall, which is powered by Home Assistant, but still obviates the need for a phone, or shouting at Siri to &lt;em&gt;&amp;ldquo;please lower the god-damn shades in the living room&amp;rdquo;&lt;/em&gt;. When it came to the air handler I wanted something similar, and preferably a control mechanism that did not rely on Home Assistant, just in case – I still want my wife to be able to turn on the heater when the server is down. The answer in this case was actually right in front of us, &lt;a href="https://www.dcne.com/product/dls-wired-control-programmable-ksacn1401aaa"&gt;the wired remote accessory&lt;/a&gt; from earlier. Notably, it did not rely on the 24V interface that we had disabled to enable the ESPHome device to integrate with the air handler, and could re-use the existing thermostat wires in the wall. eBay to the rescue, and a few days later, the control was on the wall and the best part – it reflects the changes made by Home Assistant, and vis-a-versa. Make a change on the control, Home Assistant reflects it moments later.&lt;/p&gt;</content:encoded></item><item><title>Backing up Unifi Protect</title><link>https://blog.tompaulus.com/backing-up-unifi-protect/</link><pubDate>Wed, 24 May 2023 01:16:32 +0000</pubDate><guid>https://blog.tompaulus.com/backing-up-unifi-protect/</guid><description>A few weeks ago, I was scrolling through the r/Ubiquiti Subreddit when I came across a post where someone had their NVR stolen during a break in. We have a number of Protect cameras across the house and the post got me thinking&amp;hellip; I have offsite backups of data stored on the NAS, but not the NVR. A lot of the recordings are useless, but it would be really nice to have the events stored …</description><category>Software</category><media:content medium="image" url="https://blog.tompaulus.com/backing-up-unifi-protect/media/external/unsplash-5l0v2_B9MNI.jpg"/><content:encoded>&lt;p&gt;A few weeks ago, I was scrolling through the &lt;a href="https://www.reddit.com/r/Ubiquiti/"&gt;r/Ubiquiti Subreddit&lt;/a&gt; when I came across a post where someone had their NVR stolen during a break in. We have a number of Protect cameras across the house and the post got me thinking&amp;hellip; I have offsite backups of data stored on the NAS, but not the NVR. A lot of the recordings are useless, but it would be really nice to have the events stored offsite for a few days, just in case.&lt;/p&gt;
&lt;p&gt;Luckily, the comments of the original post (which I can&amp;rsquo;t find anymore) had a bunch of helpful suggestions, one of which being to run &lt;a href="https://github.com/ep1cman/unifi-protect-backup"&gt;unifi-protect-backup&lt;/a&gt; on a Raspberry Pi. However, because Pis are in short supply right now, and because I don&amp;rsquo;t love running Pis for production workloads, I wanted to run the backups from the NVR itself. I thought that the NVR had enough spare CPU and Memory to do the work, and had all the data available locally, but boy was I mistaken.&lt;/p&gt;
&lt;p&gt;Since I wanted to both encrypt and compress the video files before uploading them, this turned out to be a computationally expensive operation and when run on the NVR, would result in a lock-up that would only be resolved by a power-cycle.&lt;/p&gt;
&lt;p&gt;Since I had &lt;a href="https://blog.tompaulus.com/resilient-homelab-infrastructure/"&gt;recently started using Nomad for my Container Orchestration&lt;/a&gt;, I opted create a Nomad Job for this task and have the job run from one of my servers, where significantly more CPU was available, and I did not risk locking up the NVR. The Nomad Job configuration ended up being much simpler than the work needed to get the backups running on the NVR (especially since the NVR likes to reset things after updates).&lt;/p&gt;
&lt;figure class="kg-card kg-bookmark-card"&gt;
&lt;a class="kg-bookmark-container" href="https://github.com/tpaulus/terraform-hashi/blob/main/jobs/backup-unifi-protect.hcl"&gt;
&lt;div class="kg-bookmark-content"&gt;
&lt;div class="kg-bookmark-title"&gt;tpaulus/terraform-hashi&lt;/div&gt;
&lt;div class="kg-bookmark-description"&gt;Hashicorp Product (Nomad, Consul, etc.) Terraform Configurations&lt;/div&gt;
&lt;div class="kg-bookmark-metadata"&gt;
&lt;img class="kg-bookmark-icon" src="https://github.githubassets.com/favicons/favicon.svg" alt=""&gt;
&lt;span class="kg-bookmark-author"&gt;tpaulus&lt;/span&gt;
&lt;span class="kg-bookmark-publisher"&gt;GitHub&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="kg-bookmark-thumbnail"&gt;&lt;img src="https://opengraph.githubassets.com/1/tpaulus/terraform-hashi" alt=""&gt;&lt;/div&gt;
&lt;/a&gt;
&lt;/figure&gt;</content:encoded></item><item><title>Weaving a New Solution to Container Networking</title><link>https://blog.tompaulus.com/weaving-a-new-solution-to-container-networking/</link><pubDate>Fri, 19 May 2023 19:48:40 +0000</pubDate><guid>https://blog.tompaulus.com/weaving-a-new-solution-to-container-networking/</guid><description>As I described in my recent post setting up Nomad, I restored to using macvlan to get containers their own network addresses, but this had the unfortunate side effect of container being unable to talk to their host due to intricacies of how the Linux kernel does packet routing. I wasn&amp;rsquo;t happy with the solution, both because containers would sometimes get stuck and not get cleaned up, …</description><category>Software</category><media:content medium="image" url="https://blog.tompaulus.com/weaving-a-new-solution-to-container-networking/media/external/unsplash-xsmtEGcaN4U.jpg"/><content:encoded>&lt;p&gt;As I described in &lt;a href="blog.tompaulus.com/resilient-homelab-infrastructure/"&gt;my recent post setting up Nomad&lt;/a&gt;, I restored to using &lt;code&gt;macvlan&lt;/code&gt; to get containers their own network addresses, but this had the unfortunate side effect of container being unable to talk to their host due to intricacies of how the Linux kernel does packet routing. I wasn&amp;rsquo;t happy with the solution, both because containers would sometimes get stuck and not get cleaned up, resulting in more than one container with the same IP address running on the network, leading to all sorts of strange issues; and because it meant that there was a single point of failure in my router, serving both Consul DNS and hosting cloudflared for the applications which used &lt;code&gt;macvlan&lt;/code&gt;. So I went looking for options.&lt;/p&gt;
&lt;h2 id="looking-for-a-solution"&gt;Looking for a Solution&lt;/h2&gt;
&lt;p&gt;I was weary of CNIs and their support in Nomad from the many hours I spent cursing the last time I tried to deal with this issue, so I was looking for solutions that had already been successfully integrated with Nomad in the past. I landed on three options: Calico, Flannel, and Weave, thanks to &lt;a href="https://www.suse.com/c/rancher_blog/comparing-kubernetes-cni-providers-flannel-calico-canal-and-weave/"&gt;a post by SUSE&amp;rsquo;s Rancher project&lt;/a&gt;. Each had their upsides and downsides.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Calico&lt;/strong&gt; - We use this at work, so I am somewhat familiar with its operations, but the use of BGP would cause issues as my router, a Unifi Dream Machine Pro, doesn&amp;rsquo;t support BGP out of the box. It also had a bunch of enterprise features, like network policies, I do not need, so I kept looking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flannel&lt;/strong&gt; - This seemed like a great option, until I got to the part where I read that it relies on an &lt;code&gt;etcd&lt;/code&gt; cluster. While I don&amp;rsquo;t have anything against &lt;code&gt;etcd&lt;/code&gt;, I did not want to spin up another state management cluster if I didn&amp;rsquo;t need to. So Flannel went in my back pocket, as a last resort.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Weave&lt;/strong&gt; - Weave uses a proprietary routing fabric, rather than linux primitives (like iptables), it seemed like a good fit for my application, given the low barrier to entry and the simple configuration options when using it alongside Docker.&lt;/p&gt;
&lt;h3 id="implementing-weavenet"&gt;Implementing Weavenet&lt;/h3&gt;
&lt;p&gt;I used Ansible to install Weave and its associated systemd unit.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Install Weave&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;hosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;nomad&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Install Weave&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/usr/local/bin/weave&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="m"&gt;304&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;https://git.io/weave&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0755&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Create Systemd Service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/etc/systemd/system/weave.service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0755&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; [Unit]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Description=Weave Network
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Documentation=http://docs.weave.works/weave/latest_release/
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; Requires=docker.service
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; After=docker.service
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; [Service]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; EnvironmentFile=-/etc/sysconfig/weave
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ExecStartPre=/usr/local/bin/weave launch --no-restart --no-dns --ipalloc-range $IP_CIDR --metrics-addr=0.0.0.0:21049 $PEERS
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ExecStart=/usr/bin/docker attach weave
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; ExecStop=/usr/local/bin/weave stop
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; [Install]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; WantedBy=multi-user.target&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Ensure sysconfig Directory Exists&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ansible.builtin.file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/etc/sysconfig&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;directory&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Create Weave Peers File&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/etc/sysconfig/weave&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="sd"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; PEERS=&amp;#34;10.0.10.48 10.0.10.64 10.0.10.80&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="sd"&gt; IP_CIDR=&amp;#34;172.30.0.0/16&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Start Weave Service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;systemd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;restarted&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;daemon_reload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;weave&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I ended up overriding the default CIDR (&lt;code&gt;10.0.0/8&lt;/code&gt;), as it conflicted with the parent network and explicitly listing all of the peer servers to avoid needing to join them manually.&lt;/p&gt;
&lt;p&gt;I then ran &lt;code&gt;weave expose&lt;/code&gt; on each of my nodes and added the &lt;code&gt;/24&lt;/code&gt; returned to my router&amp;rsquo;s static routes table, with the target being the node that returned the IP block. This enabled clients not part of the weave network to be able to access weave clients. From there, I just needed to explicitly set the &lt;code&gt;network_mode&lt;/code&gt; in my Nomad Job Docker Configuration and ensure that the address mode was set to driver for my service definitions.&lt;/p&gt;
&lt;figure class="kg-card kg-bookmark-card kg-card-hascaption"&gt;
&lt;a class="kg-bookmark-container" href="https://github.com/tpaulus/terraform-hashi/blob/main/jobs/alertmanager.hcl"&gt;
&lt;div class="kg-bookmark-content"&gt;
&lt;div class="kg-bookmark-title"&gt;tpaulus/terraform-hashi&lt;/div&gt;
&lt;div class="kg-bookmark-description"&gt;Hashicorp Product (Nomad, Consul, etc.) Terraform Configurations&lt;/div&gt;
&lt;div class="kg-bookmark-metadata"&gt;
&lt;img class="kg-bookmark-icon" src="https://github.githubassets.com/favicons/favicon.svg" alt=""&gt;
&lt;span class="kg-bookmark-author"&gt;tpaulus&lt;/span&gt;
&lt;span class="kg-bookmark-publisher"&gt;GitHub&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="kg-bookmark-thumbnail"&gt;&lt;img src="https://opengraph.githubassets.com/1/tpaulus/terraform-hashi" alt=""&gt;&lt;/div&gt;
&lt;/a&gt;&lt;figcaption&gt;Example Configuration using WeaveNet&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="hey-siri"&gt;Hey Siri&lt;/h2&gt;
&lt;p&gt;We have a handful of Apple&amp;rsquo;s HomePod Mini&amp;rsquo;s across the house and my partner and I both have iPhones, so it is handy to be able to ask Siri to turn on/off the lights and what not. The actual work is done via the &lt;a href="https://www.home-assistant.io/integrations/homekit/"&gt;Home Kit Integration&lt;/a&gt; in Home Assistant.&lt;/p&gt;
&lt;p&gt;One of my usual tests after I change something with either Home Assistant, or its network is to ask my office HomePod to turn on or off my Desk Lamp. Unsurprisingly, after I configured Weave on Home Assistant this stopped working. A bit of pondering lead to a potential issue. mDNS. When you pair a HomeKit device, you never put in an IP Address or other configuration, it is all handled via multicast DNS (mDNS), and now that Home Assistant was on a different subnet than the Home Pods, and iPhones, this connection was broken, despite the networks being accessible from one-another.&lt;/p&gt;
&lt;p&gt;Conveniently this is not an uncommon problem, and it’s already been solved via tools like &lt;a href="https://github.com/vfreex/mdns-reflector"&gt;mDNS Reflector&lt;/a&gt;. Deploying a container that bridged the weave network and the server host network (which is the same one as the network our phones, HomePods, etc are connected to) instantly restored the connection between HomeKit on my phone and Home Assistant. And just like that, we were back in business.&lt;/p&gt;</content:encoded></item><item><title>Resilient Infrastructure for the Homelab</title><link>https://blog.tompaulus.com/resilient-homelab-infrastructure/</link><pubDate>Thu, 18 May 2023 23:25:52 +0000</pubDate><guid>https://blog.tompaulus.com/resilient-homelab-infrastructure/</guid><description>As I&amp;rsquo;ve alluded to in the past, Home Assistant plays a major role in making my home work, from managing the HVAC system, to automatically turning on the lights in the utility closet when the door opens. For many years, I ran Home Assistant on an i5 Intel NUC within a Docker Container. This worked great, but when the NUC went down (be it for updates, or some unscheduled maintenance), so did …</description><category>Software</category><media:content medium="image" url="https://blog.tompaulus.com/resilient-homelab-infrastructure/media/external/unsplash-H2fQZ3CEqNQ.jpg"/><content:encoded>&lt;p&gt;As I&amp;rsquo;ve alluded to in the past, Home Assistant plays a major role in making my home work, from managing the HVAC system, to automatically turning on the lights in the utility closet when the door opens. For many years, I ran Home Assistant on an i5 Intel NUC within a Docker Container. This worked great, but when the NUC went down (be it for updates, or some unscheduled maintenance), so did Home Assistant and my ability to control the lights. While my partner is very forgiving and understanding, it&amp;rsquo;s rather annoying to not be able to turn on some of the lights for a few hours while I troubleshoot an issue.&lt;/p&gt;
&lt;p&gt;Single-points of failure are scary, and at work, we try to eliminate them wherever possible, so I thought it was time to do the same at home. There are a lot of existing solutions to this problem in the form of orchestration engines, like Docker Swarm, Kubernetes, K3s, etc. We use Kubernetes at work, but it was rather overkill for what I needed to do at home, which was schedule Docker Containers (and ideally Shell Scripts) to run across various nodes in my home-lab (and ideally on a specific schedule). I landed on using Hashicorp&amp;rsquo;s Nomad to get this done. Nomad met all of my needs, all without being too complex.&lt;/p&gt;
&lt;p&gt;From the beginning of this project, I had in mind to eventually get 2-3 new servers to enable the decommissioning of the NUC that had been working its little heart out for me for the last 4 years. As such, I wanted to automate as much of the provisioning and management of the new servers as much as possible. I wasn&amp;rsquo;t quite ready yet (or sure of what I wanted to get), so I used the NUC and a &lt;em&gt;Gaming&lt;/em&gt; Server I had built a while back to start experimenting.&lt;/p&gt;
&lt;h2 id="ansible-for-provisioning"&gt;Ansible for Provisioning&lt;/h2&gt;
&lt;p&gt;I love using Terraform for config management, but it doesn&amp;rsquo;t support configuring machines well (there are other tools in the Hashicorp offering that do this, but are heavily focused on the cloud). That left tools like Ansible, Chef, Puppet, and a few others. A little time spent on Google revealed that Ansible was the easiest to get started with, used one of my favorite languages (Python) for extensions, and didn&amp;rsquo;t require complex provisioning workflows.&lt;/p&gt;
&lt;p&gt;Ansible has the concept of sharing Roles and Tasks via the Ansible Galaxy. Conveniently for me, there was already a Galaxy Collection for configuring Consul (we&amp;rsquo;ll get to that in a bit) and Nomad. I had to make some adjustments to fit my needs, notably, running both the Nomad Server and Client on the same node.&lt;/p&gt;
&lt;h2 id="consul-for-service-discovery"&gt;Consul for Service Discovery&lt;/h2&gt;
&lt;p&gt;Before we can talk about Nomad, and container orchestration, we need to talk about Service Discovery. Service Discovery allows us to answer the question, where does X service live and how do I communicate with it. When running containers on a single node, this isn&amp;rsquo;t a significant issue, since the question of where is THE server, and the question of what port can be answered by the container orchestrator (Docker in my case). While we can pin specific container to specific nodes, that doesn&amp;rsquo;t allow us to build a resilient system, which is where tools like Consul come in.&lt;/p&gt;
&lt;p&gt;Consul provides a single source of truth for the answer to the what and where question. It adds some extra nice features, like health checks. Nomad more recently started offering a built-in, simple, service discovery option, but Consul still wins out for me because of 1 key feature - DNS. Consul provides a DNS interface for service discovery, which enables non-Consul aware tools, like Home Assistant, to use Consul. For example, by Home Assistant instance depends on an MQTT server, which may be on a different node than the one Home Assistant is running on. While I can’t use dynamic ports, I can use the DNS interface for Consul as the host name for the MQTT server in Home Assistant. In my case, this means that in the eyes of Home Assistant, it can reach the MQTT Server at &lt;code&gt;mqtt.service.seaview.consul&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="nomad-for-orchestration"&gt;Nomad for Orchestration&lt;/h2&gt;
&lt;p&gt;Nomad turned out to be the right balance complexity and resiliency, while not getting in my way too much with its relatively straightforward configuration DSL for jobs. Together with its support for CSI and CNIs it checked all the boxes, and was relatively easy to install and get going, given I had an already operational Consul Cluster.&lt;/p&gt;
&lt;h3 id="csis"&gt;CSIs&lt;/h3&gt;
&lt;p&gt;A lot less cool than Crime Scene Investigators, Container Storage Interfaces, provide a mechanism by which volumes can be mounted to containers. This is another case where having jobs potentially move across nodes presents an issue, as many containers are not stateless and need somewhere to persist their state.&lt;/p&gt;
&lt;p&gt;In preparation for the move to Nomad, I replaced the OS on my NAS with TrueNAS Scale, both because of some reliability and connectivity issues I was having with Unraid, but also because of the first-class support TrueNas had for CSIs. In my case, I ended up using the &lt;a href="https://github.com/democratic-csi/democratic-csi"&gt;democratic&lt;/a&gt; plugin to enable automatic NFS share creation and management. By connecting the plugin with the TrueNAS API, I am able to create, update, and delete shares from within Nomad, super convenient.&lt;/p&gt;
&lt;h3 id="cnis"&gt;CNIs&lt;/h3&gt;
&lt;p&gt;CNI (Container Network Interfaces) allow for adding custom networking interfaces to Docker Containers. This is super convenient if the default &lt;code&gt;bridge&lt;/code&gt; mode doesn&amp;rsquo;t work for your use case. For me, I like running Home Assistant on its own IP address since it has a bunch of different ports it uses for all the various integrations and those can clash with ports used by other containers. The &lt;code&gt;macvlan&lt;/code&gt; plugin allows for this (assuming your NIC allows for advertising multiple MAC Addresses). However, because of some fun Linux networking issues that I don&amp;rsquo;t fully understand, containers using &lt;code&gt;ipvlan&lt;/code&gt; or &lt;code&gt;macvlan&lt;/code&gt; cannot access the host via its IP address. This posed a problem since I needed to communicate with the Consul Agent on the host to resolve Service Discovery DNS Requests. Not a problem, I would use a plugin called &lt;a href="https://github.com/openshift/multus-cni"&gt;Multus&lt;/a&gt; to connect both a &lt;code&gt;macvlan&lt;/code&gt; and &lt;code&gt;bridge&lt;/code&gt; interface to the container.&lt;/p&gt;
&lt;p&gt;This didn&amp;rsquo;t work. I spent hours troubleshooting issues, ranging from &lt;code&gt;macvlan&lt;/code&gt; IP address not being released, to no requests making it to/from the container at all. Some more googling showed that there were a number of open issues with CNI support and Nomad dating back to 2021 that had clearly not been fixed. While not ideal, I could work around this problem. Rather than having the containers talk to their host for DNS resolution, I deployed a container to my router that augmented a DNS Server with a Consul agent to enable the containers to get their answers that way.&lt;/p&gt;
&lt;figure class="kg-card kg-bookmark-card"&gt;
&lt;a class="kg-bookmark-container" href="https://github.com/tpaulus/consul-core-dns"&gt;
&lt;div class="kg-bookmark-content"&gt;
&lt;div class="kg-bookmark-title"&gt;tpaulus/consul-core-dns&lt;/div&gt;
&lt;div class="kg-bookmark-description"&gt;CoreDNS Container also running a Consul Agent to serve Consul DNS Requests&lt;/div&gt;
&lt;div class="kg-bookmark-metadata"&gt;
&lt;img class="kg-bookmark-icon" src="https://github.githubassets.com/favicons/favicon.svg" alt=""&gt;
&lt;span class="kg-bookmark-author"&gt;tpaulus&lt;/span&gt;
&lt;span class="kg-bookmark-publisher"&gt;GitHub&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="kg-bookmark-thumbnail"&gt;&lt;img src="https://opengraph.githubassets.com/1/tpaulus/consul-core-dns" alt=""&gt;&lt;/div&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;h2 id="terraform-for-config-management"&gt;Terraform for Config Management&lt;/h2&gt;
&lt;p&gt;Since the goal of this project was to reduce the manual configuration, I opted to manage my Nomad Job configuration via Terraform. This not only allows my to quickly re-provision all of my jobs, it allows for automated image updates, which we will discuss in a bit.&lt;/p&gt;
&lt;figure class="kg-card kg-bookmark-card"&gt;
&lt;a class="kg-bookmark-container" href="https://github.com/tpaulus/terraform-hashi"&gt;
&lt;div class="kg-bookmark-content"&gt;
&lt;div class="kg-bookmark-title"&gt;tpaulus/terraform-hashi&lt;/div&gt;
&lt;div class="kg-bookmark-description"&gt;Hashicorp Product (Nomad, Consul, etc.) Terraform Configurations&lt;/div&gt;
&lt;div class="kg-bookmark-metadata"&gt;
&lt;img class="kg-bookmark-icon" src="https://github.githubassets.com/favicons/favicon.svg" alt=""&gt;
&lt;span class="kg-bookmark-author"&gt;tpaulus&lt;/span&gt;
&lt;span class="kg-bookmark-publisher"&gt;GitHub&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="kg-bookmark-thumbnail"&gt;&lt;img src="https://opengraph.githubassets.com/1/tpaulus/terraform-hashi" alt=""&gt;&lt;/div&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;h2 id="cloudflare-tunnels"&gt;Cloudflare Tunnels&lt;/h2&gt;
&lt;p&gt;While my partner may disagree, I don&amp;rsquo;t run a data center in our garage and we have a single non-static public IP address for the whole house. The solution here comes in the form of a tunnel, specifically Cloudflare Tunnels. I created a &lt;code&gt;system&lt;/code&gt; job in Nomad to run a Cloudflare Tunnel on every server. This gave me 3 different tunnels, pointing to the same services. While I could use DNS round-robin to route traffic between the servers, that would cause issues if any single server went down, since 1/3 of requests would fail. Using a Cloudflare Load Balancer solves this and allows for more intelligent routing of requests. In my case, I am simply using the health-check option to conditionally send traffic to each of the servers, ensuring that only healthy servers receive requests.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2023/04/Request-HLD-2.png" alt=""&gt;&lt;/p&gt;
&lt;h2 id="foreshadowing"&gt;Foreshadowing&lt;/h2&gt;
&lt;p&gt;One afternoon, I came into the garage to hear an unusual clicking noise coming from the rack. Some investigation yielded that the CPU Fan on the NUC had died. It was time to take advantage of the high-availability infrastructure I had spent so many hours configuring.&lt;/p&gt;
&lt;p&gt;I shut down the NUC and moved the Zigbee Dongle to another server and that was that. Nomad moved the allocations (jobs) from the NUC and scheduled them on another server in the rack. It was wonderful to see how quick and easy it was, especially given all of the headaches this failure would have caused before my Nomad adventure.&lt;/p&gt;
&lt;h2 id="optimizing-docker-image-upgrades"&gt;Optimizing Docker Image Upgrades&lt;/h2&gt;
&lt;p&gt;Back when I was hand managing container configurations on a single node, I would use Watchtower to automatically upgrade my Docker Containers to use the latest images. This approach doesn&amp;rsquo;t work when using Nomad though since it expects the docker configuration to be a certain way, and if the allocation were to get rescheduled to a different node, the updated image tag would not stick, resulting in an older image version being used until Watchtower updated it again (not ideal).&lt;/p&gt;
&lt;h3 id="renovate-to-the-rescue"&gt;Renovate to the Rescue&lt;/h3&gt;
&lt;p&gt;My first thought was to build a tool to do this myself, but that would be a lot of work. My second thought was to clone the GitHub action that bumps the Image Tag in Dockerfiles, but again, that was going to be fair bit of work. We recently started using Renovate to update our dependencies (including Docker Images) at work, and got to thinking if I could adapt Renovate to do this for me. Some digging through the docs later, I found the Regex Manager, which enabled using Regex Expressions to parse files with a specified extension type and pass those to different dependency managers. A bit of cursing at Regex later, I ended up with the following &lt;code&gt;rennovate.json&lt;/code&gt; which would look through all my &lt;code&gt;.hcl&lt;/code&gt; files, find the docker images, and upgrade them if needed.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;$schema&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://docs.renovatebot.com/renovate-schema.json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;extends&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;config:base&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;regexManagers&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;fileMatch&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;[^.]+\\.hcl&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;matchStrings&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;image\\s?=\\s?\&amp;#34;(?&amp;lt;depName&amp;gt;.*?):(?&amp;lt;currentValue&amp;gt;.*?)(?:@(?&amp;lt;currentDigest&amp;gt;sha256:[a-f0-9]+))?\&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;datasourceTemplate&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;docker&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;depTypeTemplate&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;docker_image&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;versioningTemplate&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;{{#if versioning}}{{{versioning}}}{{else}}semver{{/if}}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And because I configured the integration with the Terraform Cloud, whenever I update the repository (like merging the Renovate PR), the changes are applied to the Nomad Cluster and the running container is replaced.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been running Nomad as my container orchestration system for the last few months and have been rather pleased with the performance and resilience. Servers reboot for updates and I never notice, what more could I ask for. I&amp;rsquo;ve still got some more work ahead of me, especially on the container networking front, as I&amp;rsquo;m not happy having a single point of failure on the DNS side of things, but that&amp;rsquo;s work for another day. For now, I can sleep better knowing my home services are resilient should a server fail again (or just need to reboot for a kernel update).&lt;/p&gt;</content:encoded></item><item><title>Replacing PiHole with Cloudflare Gateway</title><link>https://blog.tompaulus.com/replacing-pihole-with-cloudflare-gateway/</link><pubDate>Sun, 30 Oct 2022 23:05:01 +0000</pubDate><guid>https://blog.tompaulus.com/replacing-pihole-with-cloudflare-gateway/</guid><description>I've been using PiHole for my home-network DNS for years, but Cloudflare Teams Gateway has enabled me to improve my DNS availability at home, and take it with me, on the go.</description><category>Software</category><media:content medium="image" url="https://blog.tompaulus.com/replacing-pihole-with-cloudflare-gateway/media/external/unsplash-nOVHbXwXCys.jpg"/><content:encoded>&lt;p&gt;Before I can dive into how I configured Cloudflare Teams Gateway to work for my home, I need to give a bit of context and what lead up to this. I began using PiHole a number of years ago, running on a Raspberry Pi Model 1B. That worked fine for a while until my partner moved-in. She would report that the internet would occasionally stop working for her, and most of the time, this turned out to be a DNS issue. Combined with the occasional hick-ups caused by running on an older, low-power SBC, the need for occasional reboots to install updates meant that it was time to setup a second PiHole to run along side the first. Those Raspberry Pis were eventually replaced with Docker Containers running on much beefier machines, but the occasional outages persisted. Either due to &lt;em&gt;fun&lt;/em&gt; intricacies of &lt;code&gt;macvlan&lt;/code&gt; which is how the Docker Containers get their IPs on the network, or an over aggressive block list, it lead to both of us turning off WiFi when a page wouldn&amp;rsquo;t load. Don&amp;rsquo;t get me wrong, I love hosting my own infrastructure for all the things we need at home, but DNS is a fickle thing and issues with DNS can manifest themselves in the most peculiar, unsuspecting ways.&lt;/p&gt;
&lt;p&gt;In a different thread, I started working for Cloudflare in April and quickly learned that a lot had changed since I became a Cloudflare User in 2014. More recently, Cloudflare has been expanding in the Zero-Trust space with tools and service aimed at making securing corporate infrastructure easier, while enabling work-from-home. One of these products is Gateway, which is part of the larger Zero Trust offering (and has a very generous free plan, which is what I use). Gateway DNS, which is what we&amp;rsquo;ll be discussing for the rest of this post, brings firewall style polices to DNS resolution, all while being wicked fast, which is critical for a DNS service.&lt;/p&gt;
&lt;p&gt;So, how did I go about moving from PiHole to Gateway for my at home DNS resolution. Well, making the actual switch is as easy as updating your router&amp;rsquo;s DHCP DNS settings to inform clients of the Gateway DNS IP address, instead of the IP addresses of the PiHole containers. While that will get your DNS request off of PiHole, there is a bit of extra work needed to get similar levels of privacy and security from Gateway DNS.&lt;/p&gt;
&lt;p&gt;First, a bit of house-keeping. Because there is a limited number of IPv4 addresses available (and they are in short supply), Gateway DNS re-uses the same IP addresses across many different customers (customizing the IP Address is an enterprise level feature), so Gateway DNS uses the source IP of the request to associate the incoming request with your account. For those with a static public IP address, it&amp;rsquo;s a set it and forget it affair, but for those who can&amp;rsquo;t get a static IP (or don&amp;rsquo;t want to pay $15/month for the privilege) we need to get a bit creative. I ended up writing this simple script which runs on one of my servers every 15 minutes to check my public IP and update it if necessary.&lt;/p&gt;
&lt;p&gt;So, at this point we have Gateway DNS configured to always have the correct Public IP, but we haven&amp;rsquo;t established any rules yet. I&amp;rsquo;ve been exploring Terraform in my free time, in part because of how much we use it internally at Cloudflare to manage our configuration. While updating the Public IP via Terraform seems a bit overly complex since it can change often, using Terraform to define the Gateway Policies seemed like a perfect fit. That&amp;rsquo;s a long, round about way of saying, I defined the Gateway Policies in Terraform.&lt;/p&gt;
&lt;figure class="kg-card kg-bookmark-card"&gt;
&lt;a class="kg-bookmark-container" href="https://github.com/tpaulus/terraform-cloudflare/blob/c2f4d6768d8f3ce3e805db532304c1e473ec6fc3/main.tf#L224-L294"&gt;
&lt;div class="kg-bookmark-content"&gt;
&lt;div class="kg-bookmark-title"&gt;tpaulus/terraform-cloudflare&lt;/div&gt;
&lt;div class="kg-bookmark-metadata"&gt;
&lt;img class="kg-bookmark-icon" src="https://github.githubassets.com/favicons/favicon.svg" alt=""&gt;
&lt;span class="kg-bookmark-author"&gt;tpaulus&lt;/span&gt;
&lt;span class="kg-bookmark-publisher"&gt;GitHub&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="kg-bookmark-thumbnail"&gt;&lt;img src="https://opengraph.githubassets.com/1/tpaulus/terraform-cloudflare" alt=""&gt;&lt;/div&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;You&amp;rsquo;ll want to configure the rules for your Gateway based off of your needs, but for me, I blocked content categories identified as security risks (i.e., New Domains, Parked Domains, Malware, Phishing) as well as any traffic destine for the TOR Network. Additionally, I maintain a manual list of Trackers and Advertisers I don&amp;rsquo;t want to see and block them by domain name. Cloudflare&amp;rsquo;s domain classification is pretty good, but the manual list allows me to quickly block a domain without updating the policies to include that specific hostname.&lt;/p&gt;
&lt;p&gt;With all that configuration complete and the logging tuned, we can actually update the router configuration to route DNS requests to Gateway. That covers requests coming from you home, but the awesome part of all of this is these protections and configurations can move with me via the Cloudflare 1.1.1.1 app (a.k.a. WARP), Cloudflare&amp;rsquo;s take on a VPN. By enrolling the device in my Zero Trust account, these same Gateway policies will apply to requests made from those devices, regardless of where they are in the world, as long as WARP is enabled. WARP also enables some really slick split tunneling that leverages Cloudflare Tunnels (f.k.a Argo Tunnels) which allow me to access my home network from anywhere, without using port-forwarding on my home router.&lt;/p&gt;
&lt;p&gt;Cloudflare Gateway is not designed to be an ad and tracker blocker like PiHole and it would take significant effort to configure Cloudflare Gateway to block 158,614 domains sourced from the known ad-lists. But so far, this replacement does an all right job, while ensuring DNS is highly available for my home network without too much hassle, with the bonus of being able to take it with me, wherever I go.&lt;/p&gt;</content:encoded></item><item><title>Breaking up with Logi Pop</title><link>https://blog.tompaulus.com/breaking-up-with-logi-pop/</link><pubDate>Sun, 01 Nov 2020 20:48:59 +0000</pubDate><guid>https://blog.tompaulus.com/breaking-up-with-logi-pop/</guid><description>I got my first set of Logitech Pop buttons (Small BT LE Buttons that interface with a WiFi Hub) in December 2017 and they have moved with me from college dorm room, to dorm room, finally making their way on the wall next to my bed in my apartment now where they control the various lights not only in my bedroom, but in the entire apartment. Ever since the beginning, I have leveraged the convenience …</description><category>Software</category><category>Life</category><media:content medium="image" url="https://blog.tompaulus.com/breaking-up-with-logi-pop/media/external/unsplash-AexHn1Bzb3Q.jpg"/><content:encoded>&lt;p&gt;I got my first set of &lt;a href="https://www.amazon.com/gp/product/B01JO8TIH4"&gt;Logitech Pop buttons&lt;/a&gt; (Small BT LE Buttons that interface with a WiFi Hub) in December 2017 and they have moved with me from college dorm room, to dorm room, finally making their way on the wall next to my bed in my apartment now where they control the various lights not only in my bedroom, but in the entire apartment. Ever since the beginning, I have leveraged the convenience of these tiny, reliable (I have yet to need to change the batteries in over 3 years of clicking) via IFTTT. However, with the &lt;a href="https://ifttt.com/explore/introducing_ifttt_pro"&gt;recent announcement&lt;/a&gt; of IFTTT moving to a subscription model, I needed to look elsewhere. While all of the lights in my bedroom are Philips Hue bulbs, many of the other smart devices I have in the rest of my place are not, leading me to use a central automation tool (&lt;a href="https://www.home-assistant.io/"&gt;Home Assistant&lt;/a&gt; in my case) to manage everything.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2020/11/IMG_1735-2.jpg" alt="Logitech Pop Button next to a Xiomi Aquara Button"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Logitech Pop Button (Left) next to a Xiomi Aquara Button (Right).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The Logitech Pop bridge supports a number of integrations, including Philips Hue, IFTTT, Smart Things, etc.; but what they do not support is local access, meaning that while getting bridge to talk to Home Assistant is theoretically possible using an emulated-hue bridge and a lot of time, it just is not feasible in my case. As such, I am parting ways with my beloved Pop buttons (oh, how beautiful they are), and replacing them.&lt;/p&gt;
&lt;p&gt;There are a number of IoT clicky things out there, one of the most notable being &lt;a href="https://flic.io/"&gt;Flic&lt;/a&gt;. However, not only is a set of Flic buttons incredibly pricy (a starter kit with 2 buttons costs $159.99 at the time of writing), they have many of the same pitfalls, although offering a Home Assistant Integration, it relies on bypassing the hub and using the bluetooth module on the host running Home Assistant. For some context, I run Home Assistant on an Intel NUC via docker, so Bluetooth device access is possible, but again not practice, plus I have some concerns about the Bluetooth range from where the NUC is, to where the buttons would be. Some googling lead me to &lt;a href="https://www.reddit.com/r/homebridge/comments/ghxqf7/any_alternative_wifi_buttons_to_the_logitech_pop/"&gt;this Reddit Post on r/homebridge&lt;/a&gt;. This post and its comments, along with a lot more googling (free time is in great abundance this year), I landed on the Xiomi Aquara Buttons, which run about $18 on &lt;a href="https://www.amazon.com/gp/product/B07D19YXND"&gt;Amazon&lt;/a&gt;. These are ZigBee buttons, which means that they require a compatible hub of some sorts. I didn&amp;rsquo;t think anything of this though, as I already had a working VeraPlus Hub, which among other things supports both ZigBee and Z-Wave devices. So I ordered 3 to replace my small family of Pop buttons and waited.&lt;/p&gt;
&lt;p&gt;When the buttons arrived, I was quite pleased with their design and build. Yes, nowhere close to as thin and sexy as the Logitech Pop buttons, but for the price, I was more than happy to put up with the slightly more clunky build, if I could get them to work. Keyword being &lt;em&gt;if&lt;/em&gt;. After a fair bit of fiddling, I got them to associate to the Vera, resetting them, and then pushing the &amp;ldquo;Effective Range Test Button&amp;rdquo; every few seconds while the Vera was in search mode eventually got them to associate. However, Vera does not officially support Aquara devices, so they showed up as Generic IO devices.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2020/09/Vera-Dashboard.png" alt="Vera Dashboard showing a Generic ZigBee device"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Vera Dashboard showing &amp;ldquo;Generic ZigBee device&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;There were a fair number of people trying to get Aquara devices to work on &lt;a href="https://community.getvera.com/t/xiaomi-aqara-stuff-works-natively/209255"&gt;Vera&amp;rsquo;s Community&lt;/a&gt;, but the post has been inactive for 9 months no, with only some success getting the Temperature sensor to work. Xiomi has a hub that you can plug in and add to your network, but given the &lt;a href="https://www.androidauthority.com/xiaomi-privacy-cheap-phone-1118444/"&gt;news on Xiomi&amp;rsquo;s data collection practices&lt;/a&gt;, I was hesitant to add one of their devices to my home network, even if it was on an isolated VLAN. On top of that, I generally try to avoid vendor lock-in wherever possible, especially in the IoT space, where the fall of a company can lead to a very hungry cat, &lt;a href="https://twitter.com/internetofshit/status/1228489002946375682"&gt;as we saw with Petnet&lt;/a&gt;. So, I did some more googling (again, free time is not hard to come by in the midst of a pandemic), and stumbled upon &lt;a href="https://www.zigbee2mqtt.io/"&gt;ZigBee2MQTT&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I had used MQTT before, when I built a &lt;a href="https://blog.tompaulus.com/project-heart/"&gt;ESP8266-powered, internet-connected heart light&lt;/a&gt;, so I was aware of how quick and powerful it was. So, I did some reading and ended up ordering a &lt;em&gt;zig-a-zig-ah&lt;/em&gt; from Electrolama on &lt;a href="https://www.tindie.com/products/electrolama/zzh-cc2652r-multiprotocol-rf-stick"&gt;Tindie&lt;/a&gt;, wanting to avoid the frustrations of flashing a CC2531 USB sniffer, and wait a short eternity for some &lt;em&gt;very&lt;/em&gt; reliable parts to arrive from AliExpress. So, I waited.&lt;/p&gt;
&lt;p&gt;I had some hardware issues with the adapter that I originally received, but Omer from Electrolama was very quick to respond to my concerns and before I knew it I had a new adapter in my hands. From there, it was almost plug and play. For compliance reasons, the adapter ships with a simple blink program, but there is some excellent documentation on the Electrolama website that details how to flash the correct firmware for ZigBee2MQTT on to the device, which is accomplished with some python libraries which are installed via pip, and a python script to handle the flashing. Once flashed, the &lt;em&gt;zig-a-zig-ah&lt;/em&gt; shows up as a USB serial device, so no extra drivers were needed (I am using Ubuntu Server 20.04 LTS, your milage may very). From there, I was on the home stretch. Because I already use Docker for most of my needs on my home server, I went with the Container version of ZigBee2MQTT.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -d &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --name&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;zigbee2mqtt&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -v /etc/docker/zigbee2mqtt/data:/app/data &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --device&lt;span class="o"&gt;=&lt;/span&gt;/dev/ttyUSB0 &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -e &lt;span class="nv"&gt;TZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;America/Los_Angeles &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --restart&lt;span class="o"&gt;=&lt;/span&gt;always &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --network host &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; koenkk/zigbee2mqtt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On first run, ZigBee2MQTT creates the default configuration file, which requires a slight bit of modification before ZigBee2MQTT can be started successfully. Namely, I needed to change the Serial device it was trying to use, and I reconfigured the Network Key while I was here and hadn&amp;rsquo;t paired any devices.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;homeassistant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;permit_join&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;mqtt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;base_topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;zigbee2mqtt&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mqtt://127.0.0.1&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;serial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/dev/ttyUSB0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;zstack&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;advanced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;rtscts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;network_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;GENERATE&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I was already running a MQTT server that I had already wired in with Home Assistant, but that too is relativity trivial. Home Assistant makes everything pretty easy, especially with their Discovery Functionality. ZigBee2MQTT includes some very helpful configuration cards that you can include in your dashboard, which I included via the &lt;em&gt;Package Method&lt;/em&gt; described on the &lt;a href="https://www.zigbee2mqtt.io/integration/home_assistant.html"&gt;documentation&lt;/a&gt;. I also added the Network Map Card, which all together looks something like this once I got all of my devices joined.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2020/10/image.png" alt="ZigBee Network Information in Home Assistant Lovelace"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;ZigBee Network Information in Home Assistant Lovelace&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I found the provided card to do everything I needed it to do. All that was left for me to do was to configure the automations that I used to have configured in the Pop software for interfacing directly with Hue and the Automations that I had in IFTTT. On each side of my bed, I have a button that I use to control the Hue bulb that is directly above it, tapping once for a night light, twice for a reading light, and holding to turn off all the lights in the apartment. The last one was the easiest; however the previous two were much harder due to some intricacies of how Home Assistant handles &lt;code&gt;light.toggle&lt;/code&gt; actions. Specifically, you can set the color and brightness of a light with &lt;code&gt;light.toggle&lt;/code&gt;, but you call &lt;code&gt;light.toggle&lt;/code&gt; again with a different color or brightness, rather than updating the color, it turns off the light. So after some searching and thinking, I came up with this automation.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;1601945836861&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Bedroom - Button - Right - Reading Light&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;device&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;mqtt&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;device_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;98de24c1076711eb9eb06778c10b3ef9&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;action&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;subtype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;double&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;discovery_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0x00158d00027bf559&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;action_double&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;choose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;conditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;template&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;value_template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{{ state_attr(&amp;#39;&amp;#39;light.hue_bedroom_bed_right_light&amp;#39;&amp;#39;, &amp;#39;&amp;#39;color&amp;#39;&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s1"&gt; == {&amp;#39;&lt;/span&gt;&lt;span class="nt"&gt;&amp;#39;x&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;0.447, &amp;#39;&amp;#39;y&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0.406&lt;/span&gt;}&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;and is_state(&amp;#39;&amp;#39;light.hue_bedroom_bed_right_light&amp;#39;&amp;#39;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="kc"&gt;on&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="l"&gt;)}}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;light.turn_off&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;light.hue_bedroom_bed_right_light&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;light.turn_on&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;reading&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;brightness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;254&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;light.hue_bedroom_bed_right_light&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;single&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I have 4 similar instances of this automation, one for the night light and one for the reading light, for each side of the bed. The differences being the colors/brightnesses and the devices respectively. Specifically what this automation does, is it checks if the light is in the desired state already (both in terms of color and state) and if it is, turns off the light, if it isn&amp;rsquo;t, it means that the light is in a different state and the user is requesting a state change, which is achieved via a call to &lt;code&gt;light.turn_on&lt;/code&gt; with the desired parameters.&lt;/p&gt;
&lt;p&gt;All in all, once I got everything working (which wasn&amp;rsquo;t too difficult honestly), things have worked great. The buttons are much more responsive, especially for the automations that used to leverage IFTTT. I have also already made some more additions to my ZigBee network, like moving my Hue Bulbs and remotes over to ZigBee2Mqtt and getting a SmartThings Motion Sensor and Door Contact Sensor to replace a similar ZWave set. From my small sample size, ZigBee is much more responsive than ZWave, although that may also be because of the setup I was using for ZWave (using a VeraPlus connected via Home Assistant) was less than ideal. I must give credit to the ZigBee2MQTT team and community for making a reliable, easy to use, and well supported piece of software that makes integrating these physical devices into our Home Automation Systems so easy (and dare I say it, fun).&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2020/11/IMG_1736.jpg" alt="A Xiomi Aquara button mounted on a bedside"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A Xiomi Aquara button mounted on my bed side.&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>Project Heart</title><link>https://blog.tompaulus.com/project-heart/</link><pubDate>Sat, 04 Aug 2018 02:35:57 +0000</pubDate><guid>https://blog.tompaulus.com/project-heart/</guid><description>Long Distance Relationships are tough, there is no denying that. But this isn&amp;rsquo;t a post about the woes of love, rather a little project to help bridge the distance. Project Heart is a light that is controlled by your partner, regardless of where they are in the world, as long as they have an internet connection, they can light up your light.</description><category>Software</category><category>Life</category><media:content medium="image" url="https://blog.tompaulus.com/project-heart/media/2018/08/IMG_0845-1.jpg"/><content:encoded>&lt;p&gt;Long Distance Relationships are tough, there is no denying that. But this isn&amp;rsquo;t a post about the woes of love, rather a little project to help bridge the distance. Project Heart is a light that is controlled by your partner, regardless of where they are in the world, as long as they have an internet connection, they can light up your light.&lt;/p&gt;
&lt;p&gt;Making use of the &lt;a href="https://www.adafruit.com/product/2471"&gt;Adafruit ESP8266 HUZZAH&lt;/a&gt; breakout, we can connect a series of &lt;a href="https://www.adafruit.com/category/168"&gt;NeoPixel LEDs&lt;/a&gt; to the network without too much hassle. The basic premise involves the ESP8266 running a simple Web Server that is listening to request on port 80, specifically POST requests with a plain text body of either &lt;code&gt;pulse&lt;/code&gt; or &lt;code&gt;chase&lt;/code&gt; to run an animation sequence on the LEDs.&lt;/p&gt;
&lt;h2 id="software"&gt;Software&lt;/h2&gt;
&lt;p&gt;Programming the ESP8266 with the Arduino software is relatively straightforward after you have set it up. However, keep in mind that you will need a USB to Serial Adapter if you are using the specified Breakout. The Adafruit Feather HUZZAH has this built in, but it a little more expensive. Instructions for setting up the Arduino IDE to program the ESP8266 can be found on the &lt;a href="https://learn.adafruit.com/adafruit-huzzah-esp8266-breakout/using-arduino-ide"&gt;Adafruit Learning System&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Allowing the lights to be controlled from anywhere, not just inside of your home network,  presents another fun, but easily solved challenge. All of this is done in your router&amp;rsquo;s settings (typically found in the Advanced Section). First we will want to assign a static IP to the HUZZAH to allow ensure that the device&amp;rsquo;s internal IP remains the same. Next, you will want to set up Port Forwarding to allow the outside world to talk to your device.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2018/07/image.png" alt=""&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Most ISPs block common ports like 80, 443, 8080, so some trial and error may be necessary to find an open port that will work for you. Any open port will suffice. Most routers allow you to map a different external and internal port, which allows us to run the HUZZAH on port 80 for convenience.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Finally, since most ISPs use DHCP themselves to assign your home an IP address, we will want to use a service like &lt;a href="https://www.noip.com/"&gt;NoIP&lt;/a&gt; to ensure that we can always reach our home network, even if the ISP decides to issue our Modem a new IP address. How this is setup depends on your router, and the NoIP site has setup instructions for all major manufactures. So, at this point, we should have a URL which we can use to access our HUZZAH from anywhere in the world. Mine looks something like this: &lt;code&gt;http://example.hopto.org:5000&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;You can test out your configuration using an HTTP request tool like &lt;a href="https://www.getpostman.com/"&gt;Postman&lt;/a&gt;, but that&amp;rsquo;s not very convenient. That&amp;rsquo;s where we can use a service like IFTTT to make our life easier. By combining the button trigger and the Webhook service we can use a widget in the iOS notification center to trigger the light in a matter of seconds.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2018/07/image-1.png" alt="IFTTT Widget Settings"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;IFTTT Widget Settings&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;You can then add the IFTTT Widget to your Today View to access the animation controls quickly and easily.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2018/07/IMG_932348607773-1.jpeg" alt=""&gt;&lt;/p&gt;
&lt;h2 id="hardware"&gt;Hardware&lt;/h2&gt;
&lt;p&gt;The project is pretty simple from a hardware perspective, with a short bill of materials, especially if you have some of the tools already lying around.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2018/08/wiring_diagram.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;The capacitor is optional and provides some power smoothing, which is especially useful for very flashy (high current draw) animations like the pulse. When things are all set and done, mount the board and leds how you see fit, Hot Glue is one of my go to favorites for things like this.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2018/08/IMG_0847-3.jpg" alt=""&gt;&lt;/p&gt;
&lt;h3 id="bill-of-materials"&gt;Bill of Materials&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.adafruit.com/product/2471"&gt;Adafruit HUZZAH ESP8266 Breakout&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.adafruit.com/product/70"&gt;FTDI Serial TTL-232 USB Cable&lt;/a&gt; (Required for programming - although any USB to Serial adapter will due)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.adafruit.com/product/1833"&gt;USB Micro-B Breakout Board&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.adafruit.com/product/3729"&gt;Side Light NeoPixel LED PCB Bar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Stranded-Core wire&lt;/li&gt;
&lt;li&gt;Micro USB Cable&lt;/li&gt;
&lt;li&gt;USB Power Supply (1A should be sufficient for the default animations)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;All of the code for this project is on GitHub and is free for you to use and modify to your heart&amp;rsquo;s content: &lt;a href="https://github.com/tpaulus/Project-Heart"&gt;https://github.com/tpaulus/Project-Heart&lt;/a&gt;&lt;/p&gt;</content:encoded></item><item><title>Daily White Card</title><link>https://blog.tompaulus.com/daily-white-card/</link><pubDate>Mon, 11 Sep 2017 05:53:50 +0000</pubDate><guid>https://blog.tompaulus.com/daily-white-card/</guid><description>Using Cards Against Humanity's (CAH) White Cards to create a unique and fun daily challenge. Using Python, GroupMe's APIs and the open source nature of CAH to create a bot that posts daily White Card Challanges to the Staff of Maya &amp; Olmeca.</description><category>Software</category><media:content medium="image" url="https://blog.tompaulus.com/daily-white-card/media/2017/09/Cards_Against_Humanity_-15711676205-.jpg"/><content:encoded>&lt;p&gt;&lt;a href="https://cardsagainsthumanity.com/"&gt;Cards Against Humanity&lt;/a&gt; is a College Favorite and often leads to some wonderful, even though somewhat disturbing, conversation. Through a strange series of events, a few of the staff of Maya Olmeca at the Front Desk ended up talking about the various White Cards&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; and how some of them would be quite funny to see in real life, like &amp;ldquo;Vigiorius Jazz Hands&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Aaron (RA for the Visual and Performing Arts Residential Learning Community in Maya) had the awesome idea of making a competition to find White Cards in real life. The rules would be simple, take a selfie with showing what every the White Card described.&lt;/p&gt;
&lt;p&gt;With a little bit of Python (~65 lines), GroupMe&amp;rsquo;s super simple &lt;a href="https://dev.groupme.com/tutorials/bots"&gt;API&lt;/a&gt;, and the work of &lt;a href="https://www.crhallberg.com/cah/json/"&gt;Chris Hallberg&lt;/a&gt;, I put together a simple bot that posts in a GroupMe&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; group every morning at 8 am with a Random White Card.&lt;/p&gt;
&lt;h2 id="how-it-works"&gt;How it Works&lt;/h2&gt;
&lt;p&gt;Using &lt;strong&gt;JSON Against Humanity&lt;/strong&gt; I generated a JSON object with all of the cards from the Original CAH Decks (The Original Set, The 6 expansions, and the Green Box), which totaled 1159 white cards. These cards are loaded into a python &lt;code&gt;dict&lt;/code&gt; object, from which one random card is selected whenever the program is executed. This card is then given some context, like &amp;ldquo;Today you are looking for&amp;hellip;&amp;rdquo; and the text of the White Card. This is then posted to the GroupMe Group via the Bot that was created specifically for this purpose on GroupMe&amp;rsquo;s Developer portal.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Message Context&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;MESSAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Today you are looking for... &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;Can you find... &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;How about... &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt; Find it!&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt; DO YOUR WORST!&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Pick a Random White Card &amp;amp; Message Context&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;secure_random&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SystemRandom&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secure_random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Cards&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_white_cards&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secure_random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MESSAGES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Post the Card text to GroupMe&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;GroupMe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Scheduling is accomplished via &lt;a href="https://elements.heroku.com/addons/scheduler"&gt;Heroku&amp;rsquo;s Scheduler Add-on&lt;/a&gt; and is run for free on one of their Dynos.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2017/09/Screen-Shot-2017-09-10-at-10.50.52-PM.png" alt="Scheduler"&gt;&lt;/p&gt;
&lt;p&gt;Taking only an evening to put together, I&amp;rsquo;m excited to see what &amp;ldquo;interesting&amp;rdquo; results this challenge will have. The Full Source Code is available on GitHub and is licensed under MIT.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/tpaulus/daily-white-card"&gt;https://github.com/tpaulus/daily-white-card&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Cover Photo: By tombullock (Cards Against Humanity) [&lt;a href="http://creativecommons.org/licenses/by/2.0"&gt;CC BY 2.0&lt;/a&gt;], &lt;a href="https://commons.wikimedia.org/wiki/File%3ACardsAgainstHumanity(15711676205).jpg"&gt;via Wikimedia Commons&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;White Cards are typically used to fill in the blanks for Black Cards and contain a multitude of various phrases, ranging from the simple (&amp;ldquo;God&amp;rdquo;) to the complex and disturbing.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;We use GroupMe across the department for communication, and it allows for super simple and easy photo sharing, which is why we used it for this task. Plus, everyone on our staff already has the app installed.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded></item><item><title>Backing Up with Amazon Glacier</title><link>https://blog.tompaulus.com/back-up-with-glacier/</link><pubDate>Thu, 05 Jan 2017 01:13:00 +0000</pubDate><guid>https://blog.tompaulus.com/back-up-with-glacier/</guid><description>Amazon Glacier is a long-term, low-cost data archival and backup service offered by AWS. It is surprisingly cheap at only $0.0041 per GB per month, it has some caveats though. Mainly, retrievals can take upto 5 hours, and archives must be stored for a minimum of 3 months2.</description><category>Software</category><media:content medium="image" url="https://blog.tompaulus.com/back-up-with-glacier/media/2017/01/Glacier-Banner.png"/><content:encoded>&lt;p&gt;&lt;a href="https://aws.amazon.com/glacier/"&gt;Amazon Glacier&lt;/a&gt; is a long-term, low-cost data archival and backup service offered by AWS. It is surprisingly cheap at only $0.004&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; per GB per month, it has some caveats though. Mainly, retrievals can take upto 5 hours, and archives must be stored for a minimum of 3 months&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Glacier is available in most, if not all, of their data centers, but I will be using the &lt;strong&gt;US East (N. Virginia)&lt;/strong&gt; datacenter, since that is where my &lt;a href="https://lightsail.aws.amazon.com"&gt;Lightsail&lt;/a&gt; instances are hosted, and inter-datacenter transmissions are free. If you are using Lightsail, make sure to turn on VPC Peering; this should facilitate communication between your LightSail VPS and the Glacier system.&lt;/p&gt;
&lt;p&gt;Since I want to use my storage space most efficiently, I will only be backing up the &amp;ldquo;non-recoverable&amp;rdquo; information I have saved on my server. This includes, database dumps, images, configuration files; however, does not include Docker Images, etc. My logic behind this is, that in the event of a failure, I can easily pull a docker image from the repository, or reinstall a tool via apt, so I do not need to save a copy. Also, since archives will be saved for an extended period of time, depending on your desired backup frequency, saving a few hundred megabytes can add up over a few months of backups.&lt;/p&gt;
&lt;h4 id="background-on-glacier"&gt;Background on Glacier&lt;/h4&gt;
&lt;p&gt;While the AWS management console allows you to do some work with Glacier, most interactions will occur via a CLI or API requests. Thankfully, Glacier is a relatively popular service, so other developers have already written scripts and tools to help us work with Glacier. &lt;em&gt;AWS has also written a well documented SDK for Glacier as well.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Glacier treats every file that is uploaded as an non-mutable archive, be it a photo, video, or in our case a ZIP file. Each file is assigned a unique ID which can be used to identify the archive in Glacier.&lt;/p&gt;
&lt;p&gt;Since our Archives will likely be larger than 100MB, we will be using a Multipart upload, which allows the upload of a single larger archive to be chunked and parallelized. It also allows for easy recovery if something goes wrong while uploading a chunk, as only that chunk needs to be reuploaded, not the entire archive. The Multipart Upload process is discussed in detail on the Developer Guide for Glacier: &lt;a href="http://docs.aws.amazon.com/amazonglacier/latest/dev/uploading-archive-mpu.html"&gt;http://docs.aws.amazon.com/amazonglacier/latest/dev/uploading-archive-mpu.html&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="configure-glacier"&gt;Configure Glacier&lt;/h4&gt;
&lt;p&gt;Before we get started on our backup system, we have to do some work in the AWS Management console. Before you start, make sure that you have set the correct region in AWS for where you want your Glacier Vault to be.&lt;/p&gt;
&lt;h5 id="iam"&gt;IAM&lt;/h5&gt;
&lt;p&gt;We will first create an IAM user with &lt;code&gt;Programmatic access&lt;/code&gt; which we will use to authenticate our backup agent with AWS. Next, we will attach the &lt;code&gt;AmazonGlacierFullAccess&lt;/code&gt; policy to our user, this will allow our user to upload archives to our Vault. Take note of the Access Key and Secret key for the user you just created, as you will need to save them in a config file a little later. &lt;strong&gt;This is the only time you will see these, however you will be able to create additional keys later if necessary.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2017/01/IAM_Management_Console.png" alt="IAM Console - Create User"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2017/01/IAM_Management_Console-2.png" alt="IAM Console - Set ACP"&gt;&lt;/p&gt;
&lt;h5 id="glacier"&gt;Glacier&lt;/h5&gt;
&lt;p&gt;Navigate to the Glacier service in the management console, and click get started. The region that you chose earlier should be listed, as well as a prompt for the nave of your vault. The name of your vault must be unique across the region! You can setup SES notifications if you like about Vault Statuses, but you can skip that safely if you like.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2017/01/Glacier_Management_Console.png" alt="Glacier Console - Create Vault"&gt;&lt;br&gt;
&lt;img src="media/2017/01/Glacier_Management_Console-2.png" alt="Glacier Console - Vault List"&gt;&lt;/p&gt;
&lt;h4 id="configure-system-to-be-backedup"&gt;Configure System to be Backedup&lt;/h4&gt;
&lt;p&gt;Clone the &lt;a href="https://github.com/tpaulus/glacier-backup"&gt;GitHub Repository&lt;/a&gt;, which contains the uploader.jar, backup script, and aws.properties template via &lt;code&gt;git clone https://github.com/tpaulus/glacier-backup.git&lt;/code&gt;. You may need to make the backup script and the uploader jar executable, which can be achieved by &lt;code&gt;cd&lt;/code&gt;ing into the folder and executing &lt;code&gt;chmod +x *.sh *.jar&lt;/code&gt;.&lt;/p&gt;
&lt;h5 id="configure-backup-script"&gt;Configure Backup Script&lt;/h5&gt;
&lt;p&gt;Once you have cloned the repo, modify &lt;code&gt;aws.properties&lt;/code&gt; with the &lt;code&gt;accessKey&lt;/code&gt; and &lt;code&gt;secretKey&lt;/code&gt; that you took note of when you crated your IAM user. Next update the fields at the top of in &lt;code&gt;backup.sh&lt;/code&gt; with the correct values. The correct endpoint url can be found at: &lt;a href="http://docs.aws.amazon.com/general/latest/gr/rande.html#glacier_region"&gt;http://docs.aws.amazon.com/general/latest/gr/rande.html#glacier_region&lt;/a&gt;. You should use HTTPS wherever possible.&lt;/p&gt;
&lt;p&gt;The script assumes that you have your MySQL running in a docker container, whose name is a variable defined at the top of the script. Modify this section, or the other sections to suite your needs. If you are trying things out, set &lt;code&gt;SANDBOX&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; to disable the uploader and cleanup portions of the script. This prevents the compressed archive and backup folder from being deleted, so you can check if everything worked as desired, without having to deal with Glacier.&lt;/p&gt;
&lt;h5 id="running-the-script"&gt;Running the Script&lt;/h5&gt;
&lt;p&gt;Before you can run the script, you may need to install Java, you can check if Java is installed by running &lt;code&gt;java -version&lt;/code&gt; in the command line. If an error is returned, you need to install Java. On Ubuntu, this can be achieved by running &lt;code&gt;sudo apt-get install openjdk-8-jre-headless&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Once you have verified that the script works as desired, you will want to add it as a cron job to run regularly.&lt;/p&gt;
&lt;h5 id="cron-job"&gt;Cron Job&lt;/h5&gt;
&lt;p&gt;Lastly, we will want to have our backup run automatically on a schedule, which can be easily achieved with the help of CRON. CRON allows you to input when you want a certain command or task run, and it will take care of the rest for you automatically. To help generate your crontab line, tools like &lt;a href="http://crontab-generator.org/"&gt;Crontab Generator&lt;/a&gt; can take out most of the guess work and frustration. For the command, you will want to &lt;code&gt;cd&lt;/code&gt; into the directory to which you cloned the repository, and then execute the script. My finished crontab looks something like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;0 12 * * * cd /usr/share/glacier-backup; ./backup.sh &amp;gt;&amp;gt; /var/log/glacier-backup.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I decided to have my script run at 12PM (UTC) everyday, since that is a time when I expect not to make any changes to the server. If you are unsure of the timezone of your server, run &lt;code&gt;date&lt;/code&gt; to find out the current date, time, and timezone of your system.&lt;/p&gt;
&lt;p&gt;Now run &lt;code&gt;sudo crontab -e&lt;/code&gt; to add the line you got from the Crontab Generator to your local &lt;code&gt;crontab&lt;/code&gt; file. The sudo is necessary, as we want our script to be run by the root user to bypass any permissions issues.&lt;br&gt;
&lt;img src="media/2017/01/1__ubuntu_Londo___usr_share__ssh_.png" alt=""&gt;&lt;br&gt;
Save and exit, and you should see &lt;code&gt;crontab: installing new crontab&lt;/code&gt;. This means that your line was added successfully to the crontab for the root user.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s it, now you can rest assured that if something should go wrong, you have a recent backup of your essential files and databases that are safely stored offsite, ready for your retrieval within 5 hours&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h3 id="recovery-procedure"&gt;Recovery Procedure&lt;/h3&gt;
&lt;p&gt;In the event that you should need to use one of the Backups from your Glacier Vault, you will first need to retrieve the Inventory from the Glacier System. Setting the environment variables as shown below will make our lives a little easier as we complete the steps necessary to recover an archive.&lt;/p&gt;
&lt;p&gt;Additionally, the steps listed below use the same Command Line tool as we used above to send our Backup to Glacier, so ensure that the commands below are being executed in the same directory as the &lt;code&gt;backup.sh&lt;/code&gt; file. This is done to make sure that the &lt;code&gt;aws.properties&lt;/code&gt; file is located in the correct place in relation to the JAR file being executed to interface with Glacier.&lt;/p&gt;
&lt;h5 id="set-common-environment-variables"&gt;Set Common Environment Variables&lt;/h5&gt;
&lt;p&gt;The values for most of these variables will be the same as those found at the top of the &lt;code&gt;backup.sh&lt;/code&gt; file. They are not in a shell file, however, because this process can take some time to execute&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;CREDENTIALS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;aws.properties&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;https://glacier.us-east-1.amazonaws.com&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;VAULT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;myvault&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="list-inventory"&gt;List Inventory&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;java -jar glacieruploader.jar --endpoint &lt;span class="nv"&gt;$ENDPOINT&lt;/span&gt; --vault &lt;span class="nv"&gt;$VAULT&lt;/span&gt; --credentials &lt;span class="nv"&gt;$CREDENTIALS&lt;/span&gt; --list-inventory
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will give you a job ID for the inventory listing, make note of this, as you will need it to access the result.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;java -jar glacieruploader.jar --endpoint &lt;span class="nv"&gt;$ENDPOINT&lt;/span&gt; --vault &lt;span class="nv"&gt;$VAULT&lt;/span&gt; --credentials &lt;span class="nv"&gt;$CREDENTIALS&lt;/span&gt; --list-inventory yourjobidfromthepreviousstep
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Be sure to include the jobid from the previous step in the above command. You will receive an error massage (status 400) until the inventory has been generated for you to retrieve. This can take about 4 hours, depending on Vault Size and Demand, according to the Amazon Developer Documentation.&lt;/p&gt;
&lt;h4 id="download-archive"&gt;Download Archive&lt;/h4&gt;
&lt;p&gt;Once you have found the desired archive that you would like to retrieve from your inventory, making note of its &lt;code&gt;Archive ID&lt;/code&gt;, you can request it via:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;java -jar glacieruploader.jar --endpoint &lt;span class="nv"&gt;$ENDPOINT&lt;/span&gt; --vault &lt;span class="nv"&gt;$VAULT&lt;/span&gt; --credentials &lt;span class="nv"&gt;$CREDENTIALS&lt;/span&gt; --download myarchiveid --target path/to/filename.zip
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Make sure to change the Archive ID that you want to download to one in the inventory, and the target to the location you want the archive saved.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In order for you to retrieve the Archive, your IAM User that you have configured (see above) will need access to the Amazon SQS and SNS Service. This is a separate permission than the one granted for Write Access to Glacier.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2017/04/IAM_Management_Console.png" alt="Required IAM Permissions for Archive Retrieval"&gt;&lt;/p&gt;
&lt;p&gt;If you are getting errors in regard to the &amp;ldquo;current data retrieval policy,&amp;rdquo; have a look in your Vault General Settings, accessed via the Management Console, and make sure that you allow for enough data to be downloaded per hour. You should set the cap a little above the estimated size of the archive that you want to retrieve. The higher you set your retrieval cap, the more you can be charged. You are only charged for retrieval, this is not a monthly fixed charge.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2017/04/Glacier_Management_Console.png" alt="Setting Button in the Glacier Management Console"&gt;&lt;br&gt;
&lt;img src="media/2017/04/Glacier_Management_Console_-_Data_Retrieval.png" alt="Ensure that the Max Retrieval Rate is set either to No Limit, or to a size larger than the archive you are going to be downloading"&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s It. While retrievals can be time intensive, they significantly lower cost makes it worth it for most users. If you find yourself making a significant number of retrievals, you may want to consider configuring Expedited Retrievals via provisioned capacity, or moving to S3 for Hot-Storage, rather than Glacier, which is considered Cold-Storage.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;At time of writing, each GB of Glacier storage in Region: US-East-1 (N. Virginia) cost $0.004. Updated pricing can be found at &lt;a href="https://aws.amazon.com/glacier/pricing/"&gt;https://aws.amazon.com/glacier/pricing/&lt;/a&gt;&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;While archives can be deleted at anytime, the documentation states that you will be charged a prorated fee for the remaining duration. &lt;a href="https://aws.amazon.com/glacier/faqs/"&gt;https://aws.amazon.com/glacier/faqs/&lt;/a&gt;&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Archive retrieval times depend on the archive size and retrieval option selected. More information can be found in the Glacier Developer documentation: &lt;a href="https://docs.aws.amazon.com/amazonglacier/latest/dev/getting-started-download-archive.html"&gt;https://docs.aws.amazon.com/amazonglacier/latest/dev/getting-started-download-archive.html&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;While this process can be automated with the help of Amazon SNS and an Amazon SQS queue, that can be a bit more complex to setup and configure. If you will be frequently be retrieving archives (at which point you should re-consider your use of Glacier) it may be advantageous to look into this: &lt;a href="https://docs.aws.amazon.com/amazonglacier/latest/dev/retrieving-vault-inventory-java.html"&gt;https://docs.aws.amazon.com/amazonglacier/latest/dev/retrieving-vault-inventory-java.html&lt;/a&gt;&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content:encoded></item><item><title>Installing Fish Terminal on MacOS</title><link>https://blog.tompaulus.com/journal-installing-fish/</link><pubDate>Fri, 23 Sep 2016 20:23:00 +0000</pubDate><guid>https://blog.tompaulus.com/journal-installing-fish/</guid><description>Fish is a great alternative to the default bash or shell that is installed on your computer by default, while Fish is available for a variety of platforms, even Windows (with Cygwin); but this guide will focus on macOS, because that is what I use on a daily basis. It is very easily to install, since they provide a package that you simply download and run and it installs fish to /usr/local/bin. …</description><category>Software</category><media:content medium="image" url="https://blog.tompaulus.com/journal-installing-fish/media/2016/12/FishBanner.png"/><content:encoded>&lt;p&gt;Fish is a great alternative to the default bash or shell that is installed on your computer by default, while Fish is available for a variety of platforms, even Windows (with Cygwin); but this guide will focus on macOS, because that is what I use on a daily basis. It is very easily to install, since they provide a package that you simply download and run and it installs fish to &lt;code&gt;/usr/local/bin&lt;/code&gt;. However, if you are a bit more savvy and already have Homebrew installed, you can install fish with a single command in your currently very boring shell.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;brew install fish
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After the install completes, you will need to allow/whitelist the fish shell to be used, and make it your default shell, unless you want to type &lt;code&gt;fish&lt;/code&gt; every time you start a session.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo echo &amp;#39;/usr/local/bin/fish&amp;#39; &amp;gt;&amp;gt; /etc/shells chsh -s /usr/local/bin/fish
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;chsh -s /usr/local/bin/fish
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now that you have installed, whitelisted, and made it your default shell, you are probably wondering why fish is so cool. Fish’s power lies in its expansibility, especially in the awesome things that other fish enthusiasts have already created. One of these, that is likely already installed is Tackle. To use it, you will likely need to add a few extra dependencies, which can be easily done via another brew install command, and an install script. This script can be run implicitly, but you should understand what the script does before doing so, else bad things could happen. Justin has documented the Tackelbox framework and the code very thoroughly, and I recommend checking out the framework and the install code: &lt;a href="https://github.com/justinmayer/tacklebox"&gt;https://github.com/justinmayer/tacklebox&lt;/a&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;brew install vcprompt grc
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;curl -L https://raw.githubusercontent.com/justinmayer/tacklebox/master/tools/install.fish | fish
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Tackle and Tacklebox requires some configuration to work, but the config is easily modified as it is stored in a hidden file in your home folder, under &lt;code&gt;~/.config/fish/config.fish&lt;/code&gt;. I have included my config file, adds extra colors and functionality with tools like git, pip, etc.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;# Paths to your tackle
set tacklebox_path ~/.tackle ~/.tacklebox
# Theme
set tacklebox_theme entropy
# Which modules would you like to load? (modules can be found in ~/.tackle/modules/*)
# Custom modules may be added to ~/.tacklebox/modules/
# Example format: set tacklebox_modules virtualfish virtualhooks
set tacklebox_modules virtualfish virtualhooks
set tacklebox_plugins extract grc pip python up
# Which plugins would you like to enable? (plugins can be found in ~/.tackle/plugins/*)
# Custom plugins may be added to ~/.tacklebox/plugins/
# Example format: set tacklebox_plugins python extract
# Load Tacklebox configuration
. ~/.tacklebox/tacklebox.fish
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src="media/2016/09/fish_sh_example.png" alt="fish_sh_example"&gt;&lt;/p&gt;
&lt;p&gt;If you have issues, the Fish Shell Website has some great examples and documentation:  &lt;a href="https://fishshell.com/"&gt;https://fishshell.com/&lt;/a&gt;.&lt;/p&gt;</content:encoded></item><item><title>Quiver, My New Favorite Note Taking App</title><link>https://blog.tompaulus.com/quiver-sync/</link><pubDate>Mon, 04 Jul 2016 21:40:53 +0000</pubDate><guid>https://blog.tompaulus.com/quiver-sync/</guid><description>Evernote was the note taking application was the most popular option for a number of years and was the least bad of the options out there. However, recently Evernote has been promoting their paid plans in a way the hurts their free users. Removing popular features, like emailing in notes (1), and imposing extra restrictions, like limiting you to only 2 devices (2), made me reconsider if Evernote …</description><category>Software</category><media:content medium="image" url="https://blog.tompaulus.com/quiver-sync/media/2016/12/qs-banner.png"/><content:encoded>&lt;p&gt;Evernote was the note taking application was the most popular option for a number of years and was the least bad of the options out there. However, recently Evernote has been promoting their paid plans in a way the hurts their free users. Removing popular features, like emailing in notes &lt;a href="https://help.evernote.com/hc/en-us/articles/209005347-How-to-save-email-into-Evernote"&gt;(1)&lt;/a&gt;, and imposing extra restrictions, like limiting you to only 2 devices &lt;a href="https://blog.evernote.com/blog/2016/06/28/changes-to-evernotes-pricing-plans/"&gt;(2)&lt;/a&gt;, made me reconsider if Evernote was really the best option out there. Popular alternatives to Evernote at the time of writing include: &lt;a href="https://simplenote.com/"&gt;Simplenote&lt;/a&gt;, and Microsoft’s &lt;a href="https://www.onenote.com/"&gt;OneNote&lt;/a&gt;. I like to avoid Microsoft wherever possible, and Simplenotes offerings were average at best, and the reviews did not help sell me on their product.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2016/07/quiver_sync-evernot-lib.png" alt="I have quite a few notes in Evernote. Thankfully, Quiver can import notes in the Evernote .enex format."&gt;&lt;em&gt;I have quite a few notes in Evernote. Thankfully, Quiver can import notes in the Evernote .enex format.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The advantage to products like OneNote, Evernote, and Simplenote is that they offer Mobile apps and Syncing across devices. I rarely find myself editing, or creating notes on my device, but I would like to be able to access notes on the go, just in case (usually for reference purposes). The Apple App Store features &lt;a href="http://happenapps.com/#quiver"&gt;Quiver&lt;/a&gt; as one of their “Apps for Developers” and since starting college, I have used Evernote primarily to take notes in class (many of which are Computer Science Courses). If you have ever tried formatting a note in Evernote, you know the nightmare that it is. And while apps like &lt;a href="http://alternoteapp.com/"&gt;Alternote&lt;/a&gt; make it a little easier, they still don’t make it easy. Quiver fixes all of their with their innovative approach, which they call cells. Cells are different areas of your note that can be formated as text, code (with syntax highlighting for a bunch of languages), Markdown, LaTeX, or Diagram; all of which are rendered beautifully by the App. The app even supports importing Evernote Notebooks! The only downside for me is that there is no sync engine, or way to view my notes on the go (either on a mobile device or in the cloud).&lt;/p&gt;
&lt;p&gt;In order to take my notes on the go, I took advantage of their export script that converts entire notebooks of notes into HTML folders. The files themselves are hosted on my webserver and are displayed via a beautiful jQuery, PHP file explorer from &lt;a href="http://tutorialzine.com/2014/09/cute-file-browser-jquery-ajax-php/"&gt;Tutorialzine&lt;/a&gt;. While the Quiver Notebook Library is saved in Google Drive (mostly for backups and safe keeping), I am using the very powerful rsync command to sync the library between my mac and the web server.&lt;/p&gt;
&lt;h3 id="server-setup"&gt;Server Setup&lt;/h3&gt;
&lt;p&gt;Set up a folder (with password protection if you like) with your web server of choice (mine is NGINX) and copy the files from the repo into the folder. Take note of the path of the folder since you will need to enter it into the sync script that we will install on the mac.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/tpaulus/quiver_sync/tree/master/Server"&gt;Download the File Explorer and Assets from GitHub.&lt;/a&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install the &lt;a href="https://github.com/HappenApps/quiver2html"&gt;quiver2html&lt;/a&gt; node module. &lt;code&gt;npm install -g quiver2html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;You will want to set up Apache, NGINX, or your web server of choice to serve the PHP content in the folder you cloned the server files into. Google is your friend here.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When you are done, navigate to the URL for your folder and you should see something like this.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2016/07/quiver_sync-Server-Setup-Complete.png" alt="quiver_sync - Server Setup Complete"&gt;&lt;/p&gt;
&lt;h3 id="mac-setup"&gt;Mac Setup&lt;/h3&gt;
&lt;p&gt;We need to configure the computer to sync the Notebook Library automatically and request that the server convert the library into HTML. This is achieved with a simple shell script that is run every 5 minutes by launchctl, Apple’s version of CRON on newer OSs. Launchctl uses specialized plist files (which can be created with &lt;a href="http://launched.zerowidth.com/"&gt;launched&lt;/a&gt;) to tell it when to run what command.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/tpaulus/quiver_sync/tree/master/Mac"&gt;Download the Shell Script and Plist File from GitHub.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Update the script’s variables to match your installation.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;QUIVERLIB_LOC=&amp;#34;Path/to/Quiver.qvlibrary&amp;#34;
SERVER=&amp;#34;lorien&amp;#34; # Host Name of your server, as set in .ssh/config
SRV_DEST=&amp;#34;/srv/notes/&amp;#34; # Public HTTP Directory where Notebooks folder is
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Copy the shell script to your local/bin folder.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo cp quiver_sync.sh /usr/local/bin/quiverSync
sudo chmod +x /usr/local/bin/quiverSync
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The advantage with placing the file in your local bin folder is that you can call “quiverSync” at anytime from your command line to toggle a sync. Now would be a good time to test the script to see if it works before continuing. Take a look at the website and you should see something like this.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2016/07/quiver_sync-Mac-Complete.png" alt="quiver_sync - Mac Complete"&gt;&lt;/p&gt;
&lt;p&gt;If everything is working the way it should, place the launchctl script into the Launch Agents folder. If the &lt;code&gt;~/Library/LaunchAgents&lt;/code&gt; directory does not yet exists, you can create it with “mkdir”. There are several Launch Agent folders on your computer, the folder in your user library only runs when you are logged in, which is what we want, since you will likely not be making changes to your quiver notes while you are not logged in.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo cp com.whitestarsystems.quiver_sync.plist \
~/Library/LaunchAgents/com.whitestarsystems.quiver_sync.plist
launchctl load -w ~/Library/LaunchAgents/com.whitestarsystems.quiver_sync.plist
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="conclusion"&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;I think that Quiver, with the addition of the sync tool we implemented, will replace Evernote for me. It is an extremely powerful note-taking tool that won’t be charging you monthly per-device. It may not have as many features as the competition, but it serves its purpose for me very well.&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2016/07/quiver_sync-mobile.png" alt="Looks awesome on mobile devices"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2016/07/quiver_sync-Example-Note.png" alt="Notes render beautifully"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2016/07/quiver_sync-Example-in-App.png" alt="quiver_sync - Example in App"&gt;&lt;/p&gt;</content:encoded></item><item><title>Trinket Timer</title><link>https://blog.tompaulus.com/trinket-timer/</link><pubDate>Mon, 25 Aug 2014 07:15:15 +0000</pubDate><guid>https://blog.tompaulus.com/trinket-timer/</guid><description>On my last school free Sunday afternoon of the summer, I built a little project to help my Frugal Father. My dad had bought a small timer for his DJI Phantom to monitor the flight time, a $6 investment straight from Mainland China. It quickly lived up to the stereotypical expectation of all Chinese products, it failed. The seven segment display began to malfunction, only displaying the tops of the …</description><category>Software</category><media:content medium="image" url="https://blog.tompaulus.com/trinket-timer/media/2014/08/IMG_20140824_154825.jpg"/><content:encoded>&lt;p&gt;On my last school free Sunday afternoon of the summer, I built a little project to help my Frugal Father. &lt;img src="media/2014/08/DJI-Timer.jpg" alt="DJI Timer"&gt;&lt;br&gt;
My dad had bought a small timer for his DJI Phantom to monitor the flight time, a $6 investment straight from Mainland China. It quickly lived up to the stereotypical expectation of all Chinese products, it failed. The seven segment display began to malfunction, only displaying the tops of the numbers, rendering the timer useless. I felt bad for my dad, I use a &lt;a href="http://www.hobbyking.com/hobbyking/store/uh_viewitem.asp?idproduct=8992&amp;amp;aff=568798" title="HobbyKing - Turnigy 9X Transmiter (Mode 2)"&gt;Turnigy 9x&lt;/a&gt; with &lt;a href="https://code.google.com/p/er9x/" title="ER9X Frimware"&gt;ER9X&lt;/a&gt; firmware which allows me to run a timer, which is triggered when I move the throttle stick.&lt;/p&gt;
&lt;p&gt;I decided to put my last day off to good use; I pulled out my box of electronics parts, filled with mostly &lt;a href="http://adafruit.com"&gt;Adafruit&lt;/a&gt; parts. I found my favorite Seven Segment display, a .56″ White display with an I2C backpack. I had planned to use a Trinket, but quickly realized that I would need to use an Arduino to do the initial prototyping. The Arduino allows me to use the Serial Monitor, a key tool in debugging sketches. The Uno, my favorite of the Arduino boards, also has an auto-reset function which makes uploading sketches a breeze.&lt;/p&gt;
&lt;p&gt;I used 2 buttons, I like Adafruit’s &lt;a href="http://www.adafruit.com/product/1010"&gt;Colorful Square Tactile Button Switch&lt;/a&gt;, because they fit nicely in a breadboard and they add a dash of color to an otherwise colorless project. It was a relatively simple project to wire up, 10 jumper wires and 2 resistors. Things got a bit more interesting when it got to the code though.&lt;/p&gt;
&lt;p&gt;I wanted to stay away from the delay function to increment the clock as it pauses the code entirely for the allowed time. I found a library by Simon Monk on the &lt;a href="http://playground.arduino.cc/code/timer" title="Arduino Playground - Timer"&gt;Arduino Playground&lt;/a&gt;. It was just what I needed. I then began to code. I wrote my code objectively, using the things that I had learned from Python to make my code more understandable and easier to implement and maintain. The Timer library allowed my to focus my attention to reading the buttons and updating the display with the correct time, not worrying that I was slowly getting out of sync. The Timer Library is like a timer, you tell it to do something every X milliseconds, or you can tell it to hold pin 10 high for X milliseconds, while your program does something completely different.&lt;/p&gt;
&lt;p&gt;After working out the major bugs, I migrated to the Trinket and faced a few more problems. Specifically, I found that pin 3 constantly output 3.3 volts, which made reading the button that was connected to pin 3 impossible. The source of this problem is very clear, pins 3 and 4 are shared with the USB port which is used during programming. The problem was easily solved my moving the button to pin 1. To make your experience a bit smoother, I have rendered the breadboard with Frizzing, and the code is on GitHub.&lt;br&gt;
&lt;img src="media/2014/08/Trinket_Timer_bb.png" alt="Breadboard"&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/tpaulus/Trinket-Timer"&gt;View the Code on GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Happy Timing!&lt;/p&gt;</content:encoded></item><item><title>The Morning Mailer</title><link>https://blog.tompaulus.com/morning-mailer/</link><pubDate>Fri, 01 Nov 2013 17:03:59 +0000</pubDate><guid>https://blog.tompaulus.com/morning-mailer/</guid><description>Every morning, I use the same 3 apps: Todoist -to find out what I have to do today, Weather Underground – will it be warm, hot, cold, and a news app – what’s happened while I was sleeping. I needed some app that did all 3 things for me, but after searching Google’s Play store, nothing suited my needs. I decided that I would design my own app.</description><category>Software</category><content:encoded>&lt;p&gt;Every morning, I use the same 3 apps: Todoist -to find out what I have to do today, Weather Underground – will it be warm, hot, cold, and a news app – what’s happened while I was sleeping. I needed some app that did all 3 things for me, but after searching Google’s Play store, nothing suited my needs. I decided that I would design my own app.&lt;/p&gt;
&lt;p&gt;The Morning Mailer is a &lt;a href="http://python.org" title="Python Programming Language – Official Website"&gt;Python&lt;/a&gt; script that runs on my server, tompaulus.com, and sends me an email, every week day at 6 in the morning. First, greeting me in a foreign language, and then telling me what the weather will be like. Utilizing &lt;a href="http://wunderground.com"&gt;Weather Underground&lt;/a&gt;‘s API I gain access to a  concise text forecast for the day and a high-resolution, animated Radar map of my area. The greetings are stored on the server in a “.csv” file, and I can add/remove greetings simply, without having to modify the Python code.&lt;/p&gt;
&lt;p&gt;I use &lt;a href="http://todoist.com" title="Todoist: To-do list and task manager. Free, easy, online and mobile"&gt;Todoist&lt;/a&gt; to  keep track on my schoolwork, like homework and upcoming tests and other events. Todoist’s API allows me to access all the pertinent data, making it simple to stay on-track.&lt;/p&gt;
&lt;p&gt;The rest of the email contains the four top stories from the &lt;a href="http://nytimes.com/"&gt;New York Times&lt;/a&gt;, including a picture and summary.&lt;/p&gt;
&lt;p&gt;The information is parsed by Python, then injected into a beautiful HTML email, which was downloaded from &lt;a href="http://mailchimp.com"&gt;MailChimp&lt;/a&gt;‘s Email Blueprint Github repository. &lt;a href="https://github.com/mailchimp/email-blueprints" title="MailChimp Email Markup Layouts"&gt;Clone their repo here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The documentation is available on the GitHub Page for this repo: &lt;a href="http://tpaulus.github.io/MorningMailer/" title="GitHub Pages"&gt;http://tpaulus.github.io/MorningMailer/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Clone the repo: &lt;a href="http://github.com/tpaulus/MorningMailer" title="GitHub Repository"&gt;http://github.com/tpaulus/MorningMailer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="media/2013/10/Email.png" alt="Email Sample"&gt; &lt;em&gt;Morning Mailer did its job well!&lt;/em&gt;&lt;/p&gt;</content:encoded></item></channel></rss>