<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by JJ Underwood on Medium]]></title>
        <description><![CDATA[Stories by JJ Underwood on Medium]]></description>
        <link>https://medium.com/@jj.underwood?source=rss-53516c79bb78------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*kZD0XubHHx6H8iSdbtaT0A.jpeg</url>
            <title>Stories by JJ Underwood on Medium</title>
            <link>https://medium.com/@jj.underwood?source=rss-53516c79bb78------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 18:53:17 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@jj.underwood/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Building HAL 9000 Smart Home Device]]></title>
            <link>https://medium.com/@jj.underwood/building-hal-9000-smart-home-device-1b40095d5199?source=rss-53516c79bb78------2</link>
            <guid isPermaLink="false">https://medium.com/p/1b40095d5199</guid>
            <category><![CDATA[smart-home]]></category>
            <category><![CDATA[hal-9000]]></category>
            <category><![CDATA[esphome]]></category>
            <category><![CDATA[iot]]></category>
            <category><![CDATA[home-assistant]]></category>
            <dc:creator><![CDATA[JJ Underwood]]></dc:creator>
            <pubDate>Fri, 16 May 2025 16:42:58 GMT</pubDate>
            <atom:updated>2025-05-16T17:24:30.777Z</atom:updated>
            <content:encoded><![CDATA[<p>I built my first version (V1) of <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">All-in-One DIY Environment Monitor</a> almost one year ago, which is an ESPHome based sensor node to measure environment metrics (Temperature, Relative Humidity, Illuminance, CO2, VOC Index, NOx Index, PM: Particulate Matter 0.5/1.0/2.5/4.0/10.0, Pressure and Presence) and Home Assistant to collect measurement data. I installed nine of V1 sensor nodes in my house and they have been working very well since then. I also tried to build <a href="https://medium.com/@jj.underwood/diy-all-in-one-environment-monitor-v2-e65dd4e96cf2">V2 sensor node</a> to add an exhaust fan for cooling, an LED strip for the night light, an I2S mic for monitoring Ambient Sound Level and a buzzer for notification. Because the result of V2 was a little bit disappointing for me, I have been thinking how I could make improvements based on lessons learned. In V1 and V2, I used an affordable plastic case from AliExpress as an enclosure. Although it was enough for environment metrics monitoring in V1, I found some limitations once I tried to do more in V2 even though I managed to put all the components into the case by designing my custom PCB board. But I have not yet been able to find any other good enclosures for my weekend projects as I explained in <a href="https://medium.com/@jj.underwood/comparing-enclosures-for-smart-home-projects-2e80e82ebee6">another post</a>. And creating an entire enclosure with 3D printing is still too much for me. Eventually, I stumbled upon <a href="https://www.amazon.com/dp/B07SVCLLFW">the HAL 9000 replica model</a> on the Internet. It’s just a plastic model, but it looks good, and it is large enough compared to an affordable plastic case which I have been using. It will allow me to have more flexibility. So, I decided to turn the HAL 9000 model into the special version of DIY Environment Monitor and even more.</p><p><strong>Disclaimer</strong>: Before I dive into the details, let’s set some ground rules:</p><ol><li>Educational Purpose Only: This blog post serves as an educational resource. I’m sharing my experiences, but I’m just a hobbyist, not a professional. Always consult experts for specific advice.</li><li>Assumption of Risk: By following my instructions, you assume full responsibility for any consequences. DIY projects involve risks, so proceed with caution.</li><li>Safety First: Handle electronic components carefully. Use appropriate tools and safety gear. Be aware of electrical hazards. Follow manufacturer guidelines for microcontrollers and sensors.</li></ol><h3>Objectives</h3><p>There are many prior projects of creating HAL 9000 replicas, like <a href="https://www.thingiverse.com/thing:1805762">the very precise 3D data</a>, <a href="https://learn.adafruit.com/hal-9000-replica/overview">the affordable replica</a> and <a href="https://learn.adafruit.com/hal-9000-rp2040-prop-maker/overview">the talking replica</a> from Adafruit, and <a href="https://www.indiegogo.com/projects/hal-9000-product-line-from-2001-a-space-odyssey#/">the unsuccessful but good-looking crowdfunding project</a>. This time, I’m going to use <a href="https://www.amazon.com/dp/B07SVCLLFW">Moebius Models 1:1 HAL 9000 kit</a>, which I bought on Amazon.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*caYa8qNKOjZYNVEeuOWe9w.jpeg" /><figcaption>Moebius Models 1:1 HAL 9000 kit</figcaption></figure><p>It’s already almost complete and easy to assemble for a casual DIYer like me. It comes with an LED for its iconic eye, but nothing else. It does not have even a back panel. So, I’m going to achieve followings in this project.</p><ul><li><strong>Adding environment monitoring</strong>: I’m going to add capabilities to measure all the environment metrics (Temperature, Relative Humidity, Illuminance, CO2, VOC Index, NOx Index, PM 0.5/1.0/2.5/4.0/10.0, Pressure and Presence) by using I2C and UART sensors.</li><li><strong>Adding a microphone and a speaker</strong>: Since HAL 9000 is supposed to have listening and speaking capabilities, I’m going to use an I2S microphone, an I2S amplifier and a small speaker to add at least Ambient Sound Level monitoring and sound streaming capabilities, and possibly Voice Assistant capability.</li><li><strong>Adding buttons</strong>: Although HAL 9000 is supposed to communicate with us through voice interface, it will be nice to have some physical buttons for some use cases. I’ve already built my <a href="https://medium.com/@jj.underwood/diy-smart-button-43d4b461ee5d">DIY smart button in another post</a>. If I add buttons, I can configure them later for any use case. They will be useful at least for speaker volume control.</li><li><strong>Adding LEDs</strong>: Although the HAL 9000 model already has an LED for its eye, it will be nice if it can change the light of the eye and have additional ambient light around it. So, I’m going to use addressable LEDs to add more lighting capabilities.</li><li><strong>Adding an emergency battery</strong>: Since HAL 9000 is supposed to work even in an emergency situation, I’m going to add a UPS module and a battery so that it could work even when there is a power outage. I’m not trying to make it completely battery powered. It’s just for an emergency battery so that HAL 9000 can work for a short period of time after a power outage.</li><li><strong>Adding an RTC (Real Time Clock)</strong>: Since HAL 9000 is supposed to have some level of autonomy even in an emergency situation, I’m going to add an RTC module so that it could recognize time even when there is a network outage or a power outage.</li><li><strong>Creating a PCB board</strong>: I’m going to design a custom PCB board to connect all the components as I tried in my <a href="https://medium.com/@jj.underwood/diy-all-in-one-environment-monitor-v2-e65dd4e96cf2">V2</a> sensor node.</li><li><strong>Creating a back panel</strong>: This is the most important part of this project. Since the HAL 9000 model does not have a back panel and I want to keep the HAL 9000 model intact, I’m going to design a custom back panel by using 3D CAD for the first time and 3D print it so that I can put all the components on my custom back panel without modifying anything of the HAL 9000 model. Designing the entire enclosure is still too much for me. But designing only a back panel will be relatively easier because the back panel is hidden behind the good-looking HAL 9000 model.</li></ul><h3>Environment Monitoring</h3><p>Based on my experience in V1/V2 sensor nodes and <a href="https://medium.com/@jj.underwood/comparing-i2c-sensors-e451c4a447ab">comparison of sensor boards</a>, I’m going to use SHT41 for Temperature and Relative Humidity, BH1750 for Illuminance, SCD40 for CO2, SGP41 for VOC Index and NOx Index, SPS30 for PM 0.5/1.0/2.5/4.0/10.0, BMP280 for Pressure and LD2410C for Presence.</p><p>Regarding SHT41 and BH1750, I’m going to use sensor chips and create custom sensor boards so that I can expose them to the outside of the HAL 9000 model. It also allows me to reduce redundant pull-up resistors. Especially, exposing SHT41 to the outside will be an improvement from V1/V2. But it might be still possible that there could be thermal conduction through the warmed air inside the HAL 9000 model or through the back panel. So, I’m going to try an exhaust fan again as I tried in V2. Based on my failure in V2, I’m going to use a bit larger blower fan this time. I bought <a href="https://www.aliexpress.us/item/3256806360734140.html">an affordable 5V powered 30mm PWM 4 pins blower fan from AliExpress</a>, which seems to be aimed at Raspberry Pi 5 Active Cooler.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a2vVQaq_vfa2j3uw_x4j0w.jpeg" /><figcaption>30mm 5V PWM 4pin blower fan</figcaption></figure><p>In terms of SCD40, SGP41, SPS30, BMP280 and LD2410C, I decided to use affordable sensor boards or sensor modules which I bought from AliExpress before US-China trade war.</p><h3>Microphone, Amplifier and Speaker</h3><p>Like V2, I’m going to use an <a href="https://www.adafruit.com/product/6049">ICS-43434 sensor board from Adafruit</a> for an I2S mic. When it comes to an I2S amp, MAX98357A seems to be popular in online communities. I’m going to use an <a href="https://www.aliexpress.us/item/3256805196806369.html">affordable one from AliExpress</a>. It looks exactly the same with <a href="https://www.adafruit.com/product/3006">one from Adafruit</a> for some reasons. Regarding a speaker, there are some options for MAX98357A — 3W 4Ω or 8Ω. The HAL 9000 model has a kind of speaker grille on its bottom, but its size is limited. There will be only a limited space to put a speaker behind the speaker grille. I’m not sure if a small speaker will really work, and I’m not sure either one big mono speaker unit or a small stereo pair of speaker units will work better. I’m definitely not an expert of audio. I just decided to use <a href="https://www.amazon.com/dp/B01LN8ONG4">3W 4Ω stereo speaker units that I found on Amazon</a>. And I bought two MAX98357A for stereo.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7Trpc6NRkXVhAqiypUzcIw.jpeg" /><figcaption>I2S mic and amp</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3q5rprjaDNBPebCyyxNOCA.jpeg" /><figcaption>Speaker unit</figcaption></figure><p>As I may try Voice Assistant with a mic, amps and speaker units, I’m going to use a powerful ESP microcontroller board this time — <a href="https://www.wemos.cc/en/latest/s3/s3.html">Lolin S3</a> which has more memory than D1 mini or S3 mini that I used in V1/V2. I have used Lolin S3 in <a href="https://medium.com/@jj.underwood/sending-car-data-from-esp32-to-aws-iot-and-home-assistant-aabf542bf5f9">another project to monitor car data</a> and it has been working great since then.</p><p>Regarding connections between ICS-43434, a pair of MAX98357A and Lolin S3, there seem to be some options for I2S communication. Since I2S is a bus protocol, we can define just one I2S interface for both audio input and output among a mic and amps. Which means that we need only one pair of GPIO pins for LRCLK (Left/Right Clock) and BCLK (Bit Clock). On the other hand, we can also define multiple I2S interfaces by using <a href="https://esphome.io/components/i2s_audio.html">ID</a>. For example, one for audio input and another one for audio output. In that case, we need a pair of GPIO pins for LRCLK and BCLK of audio input and another pair of GPIO pins for LRCLK and BCLK of audio output. Which means that we need 4 GPIO pins. In my case, I may use an I2S mic for Ambient Sound Level monitoring and amps for sound streaming. This use case requires two I2S interfaces at the time of this writing. And I may use an I2S mic and amps together for Voice Assistant. Either one I2S interface or two I2S interfaces would work for this use case. Since I’m still not sure either one of use cases would fit in my daily life, I decided to define two I2S interfaces for now. In addition to GPIO pins for LRCLK/BCLK, we also need GPIO pins for data — data input for a mic and data output for amps. And it seems that we don’t need separated GPIO pins for stereo data output. So, I need to use 6 of GPIO pins for I2S in total — 2 pins for LRCLK/BCLK of a mic, 1 pin for data input for a mic, 2 pins for LRCLK/BCLK of amps and 1 pin for data output for amps. For amps, I use the same GPIO pins for a pair of MAX98357A amps. As I describe later, I can configure each MAX98357A amp to output left, right or mixed channel by adding resistors.</p><p>And I’m going to use a buzzer as well for just in case. Though it might be unnecessary since I use a speaker.</p><h3>Buttons</h3><p>In <a href="https://medium.com/@jj.underwood/diy-smart-button-43d4b461ee5d">my DIY smart button project</a>, I used arcade style buttons. Although it has been working well since I installed it in my house, arcade style buttons won’t fit in the appearance of HAL 9000. Based on <a href="https://www.engadget.com/hal-9000-replica-crowdfunding-project-goes-bust-171823930.html">the good-looking prior example</a>, I decided to use a square LED button. And I found <a href="https://www.aliexpress.us/item/3256807166202763.html">an affordable and small square LED button</a> on AliExpress. It’s basically a tact switch with an LED and it has 6 pins (4 pins for a tact switch and +/- pins for an LED). According to <a href="https://www.honyone.com/en/products/show_117.html">its description</a>, it is 12V 50mA rating. It will work with 3.3V output from Lolin S3 with a small resistor (like 100Ω).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YMJnK96_sjOqjEk6nucN3g.jpeg" /><figcaption>LED button</figcaption></figure><p>Regarding connections between buttons and Lolin S3, there might be some options depending on how I want to turn their LEDs on — making them always on, turning them on only when I push them, controlling their LEDs with more flexibility (e.g., using LEDs for guidance). If we want flexibility, we need to use GPIO pins to control their LEDs as well as GPIO pins for their switches. Because I need many GPIO pins in this project, I decided to go with a simple approach for buttons — turning their LEDs on only when I push them. In this case, I need only one GPIO pin for each button and can turn its LED on with just circuit connection (connecting a positive pin of a button’s LED to 3.3V, a negative pin of a button’s LED to one end of a button’s switch pins, and another end of a button’s switch pins to GND).</p><h3>LEDs</h3><p>Regarding an LED for the eye of HAL 9000, the HAL 9000 model comes with a 5mm DIP type LED. I found an addressable LED in the shape of 5mm DIP (like <a href="https://www.adafruit.com/product/1938">this </a>and <a href="https://www.aliexpress.us/item/2255799958606325.html">this</a>). Although it has only one LED, it allows me to control color, brightness, and effect. I bought an affordable one from AliExpress. In terms of ambient lighting, I’m going to use <a href="https://www.aliexpress.us/item/2251832290002435.html">5V SK6812 LED strip</a>, especially 60 LEDs/m version which is denser than 30 LEDs/m version that I used in V2. It seems that it draws 60mA/LED at maximum. If I use the entire 1m, it consumes 3.6A in total.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0mHHeGxJn_6SsxiPp5hhvg.jpeg" /><figcaption>Addressable LED, 5mm DIP type</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fkkbzAflJMZ_Xmnrva_EQA.jpeg" /><figcaption>Addressable LED, string</figcaption></figure><h3>Emergency Battery</h3><p>I’ve found an affordable <a href="https://www.aliexpress.us/item/3256805009119745.html">UPS module</a> on AliExpress. It is 15W 3A rating and it has a USB-C input. It also comes with a holder for two 18650 batteries. I bought a 5V/3A version of the UPS module and two flat top <a href="https://www.18650batterystore.com/products/samsung-25r-18650">18650 Li-ion batteries</a>. Because the UPS module is 5V/3A, I better avoid using the entire 1m LED string.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CXMv1w_uRm1CYAA-e9Q_qg.jpeg" /><figcaption>5V UPS module</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8d60CER98PbsZNc53ICtoA.jpeg" /><figcaption>Li-ion batteries</figcaption></figure><p>A USB-C cable goes to the input of the UPS module and output pins (pads, actually) of the UPS module provides power to other components. When it comes to connection between the UPS module and other components, there seem to be two options. I could either (A) provide power from the UPS module to Lolin S3 and then provide power from Lolin S3 to other components, or (B) provide power from the UPS module to a shared 5V line and then provide power from the 5V shared line to all the components. According to <a href="https://www.wemos.cc/en/latest/_static/files/sch_s3_v1.0.0.pdf">Lolin S3 schematic</a>, it does not have an explicit VBUS pin, and it has a 1N5819W, which is a 40V/1A Schottky barrier diode, between VBUS (USB-C input) and 5V. Which means that 5V pin can provide 1A at maximum while the UPS module can provide 3A at maximum. So, I decided to go with (B). I’m going to create a shared 5V line on the PCB board and the 5V line provides power to 5V powered components like Lolin S3, LEDs, the exhaust fan, SPS30, LD2410C. And then, Lolin S3 provides power to 3.3V powered components from its 3.3V pin.</p><p>I need to monitor at least input power voltage of the UPS module so that I can detect a power outage. I’m going to use a simple voltage divider circuit and ESPHome <a href="https://esphome.io/components/sensor/adc.html">Analog to Digital Sensor</a>.</p><p>By the way, when I ordered UPS modules, one of them arrived with one of capacitors coming off. It might be better to order more than you need.</p><h3>RTC</h3><p>ESPHome already supports I2C RTC module (<a href="https://esphome.io/components/time/ds1307.html">DS1307</a>). And it seems that the latest version (DS3231) is also <a href="https://liberit.ca/blog/exploring-ds3231-rtc-esphome-setting-time/">supported</a>. It is relatively expensive on major online stores, but I found an affordable one on AliExpress (like <a href="https://www.aliexpress.us/item/3256806957282138.html">this</a>). It can work with a CR2032 coin battery.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TvSLMAvr1--1RE49odFv7Q.jpeg" /><figcaption>DS3231 RTC module</figcaption></figure><h3>PCB Board</h3><p>Because the HAL 9000 model is large enough, I could design a large custom PCB board for it. But there will be just one HAL 9000 in my house while the minimum order of PCB board for JLCPCB is 5. So, I decided to make it smaller as much as I can so that I can use the rest of them in other projects in the future, hopefully. As I’m going to make it work with a UPS module, I decided to make it the same size as the UPS module for my convenience.</p><p>After many tries-and-errors, my PCB design became like the following. To be honest, I mistakenly connected components and fried a couple of ESP32 board and UPS module during prototyping of the PCB board design.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1016/1*iTP9J7_NQ9pVn2YDHH3reA.png" /><figcaption>Schematic of PCB</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RxKnZ0pgEd3gmKCteO2sLA.png" /><figcaption>2D and 3D views of PCB</figcaption></figure><p>As you can see, I’ve used the same approach with V2 and put different boards in one design so that I can have the main board and sub boards for BH1750, SHT41, DIP LED and buttons all at once to reduce the PCB manufacturing cost.</p><p>The main board hosts through-holes for Lolin S3, through-holes for female headers to attach sensor boards (SCD40, SGP41, BMP280) and amps (MAX98357A). Regarding SGP41 and BMP280, I’m going to stack them onto one female header. It also hosts a JST SH header for the exhaust fan and JST ZH headers for SPS30, ICS-43434, a buzzer and sub boards, and a JST XH header for LED strip. And it has a JST XH header for power input from the UPS module and a JST ZH header for the input power monitoring of the UPS module. Basically, I selected XH over ZH in case I expect current more than 1A.</p><p>As I described above, a USB-C cable basically goes to the UPS module and the UPS module provides power to the 5V line on the PCB board. But sometimes, I need to plug a USB-C cable directly into Lolin S3 (e.g., flushing the firmware in case Wi-Fi is not available, etc.). Although I can see SS54, which is a 40V/5A Schottky barrier diode, on the UPS module, I placed diodes around power input from the UPS module and input power monitoring of the UPS module to prevent current from flowing to the UPS module from the PCB board.</p><p>I also placed through-holes for resistors for SD pin and GAIN pin of MAX98357A so that I can add necessary values of resistors later for the purpose of channel and gain selection.</p><p>Finally, I placed an order to JLCPCB. It costed me 74 USD + shipping fee 11 USD. The unit cost was 17 USD.</p><p>After 2 weeks, I received the PCB board from JLCPCB. It looks great. I could easily detach sub boards from a main board with a diagonal cutter.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KUaiYmOPOF8u6JUos3QfQg.jpeg" /><figcaption>PCB board</figcaption></figure><h3>Back Panel</h3><p>As I described in <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">V1 project</a>, I have used <a href="https://www.freecad.org/">FreeCAD</a> to just cut an official Sensirion SEN55 holder. It was the only one experience for me to use 3D CAD. I have never designed anything from scratch with 3D CAD. Since I’m a completely newbie, I’m not going to compare or review 3D CAD software. I just decided to use <a href="https://www.tinkercad.com/">Tinkercad</a> this time because I don’t have to install anything on my PC and it looks easy to use for a complete beginner like me.</p><p>The HAL 9000 model is a kind of box without a back. The back of it is completely open. There is no mechanism to attach a back panel, like a latch, a screw hole, a slit and so forth. So, I’m going to create a similar but a little bit smaller box without a front panel so that it can tightly fit into the HAL 9000 model. It’s going to be like an inner box of the HAL 9000 model. But hereinafter I call it just a back panel anyway. I used a caliper and measured the inside dimensions of the HAL 9000 model to design the back panel. The measured dimensions were a little bit different from <a href="https://learn.adafruit.com/hal-9000-replica/overview">the precise 3D data </a>by the way.</p><p>After many tries-and-errors, my 3D design became like the following. It looks like a ship.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bh1ar0TKRz0hQHLtXzD38w.png" /><figcaption>Back panel design in Tinkercad</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bwZj2BQ1LrlG981JpNulmw.png" /><figcaption>Another angle of back panel design</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*mbWzfMBBX_JkT7c4z_o7Zw.png" /><figcaption>Back of back panel design</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7dgO3M84DlW38gxjKPLZSA.png" /><figcaption>Another angle of back of back panel design</figcaption></figure><p>As you might be able see, I placed threaded screw holes to attach components. I used ISO metric screw thread object in Tinkercad. And I used dimensions of M2 screw (0.4mm pitch). It seems that using a thread insert or using a tapping tool would be a better idea rather than trying to directly print a threaded screw hole especially for a small screw like M2. But I decided to give it a try.</p><p>Because I want to attach an LED strip on the back of the back panel, I added an angled step to the back instead of making the back completely flat so that an LED strip can create nice ambient lighting around the HAL 9000 model against the wall. It is an improvement from V2. When it comes to a DIP LED for the eye, I precisely measured the expected position of the DIP LED so that it can fit into the hole of the eye of the HAL 9000 model when I put the back panel into the HAL 9000 model.</p><p>I created holes or slits on both sides of the back panel so that components like SHT41, BH1750, SPS30, ICS-43434 and the exhaust fan can be directly exposed to the outside. Placing holes for BH1750 and ICS-43434 on sides is not ideal. They should face in front. But I had no choice because I didn’t want to modify the HAL 9000 model.</p><p>I placed speaker boxes for a pair of speaker units behind the speaker grille. I also placed the buzzer mount close to the speaker grille rather than creating a hole on the side.</p><p>I placed a screw hole on the right bottom side so that it is exposed through the small cutout on the HAL 9000 model, which is originally designed for the switch of the LED of the eye. It will be helpful if I can use a screw to hold the HAL 9000 model.</p><p>Finally, I placed an order of my 3D design. I used <a href="https://craftcloud3d.com/">Craftcloud</a> again, which I used in V1 project to create a SEN55 holder. I exported a STL file of the back panel from Tinkercad and uploaded it to Craftcloud. Because the back panel is hidden behind the HAL 9000 model, I selected an affordable PETG material with black color and without a smoothing option. And I have chosen a service provider in Oregon who offered me the most affordable price. It costed me 31 USD + shipping fee (12 USD). Although it was the most affordable option I could get, 43 USD was almost the same with the HAL 9000 model itself.</p><p>The 3D print service provider contacted me to inform the risk of print failures in some areas like buttons and air vents because the wall thickness was too thin. Because I’ve already ordered the PCB board, I asked them to proceed printing anyway. It will be a future improvement.</p><p>After 7 days, I received the 3D printed back panel.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JcGXWtwz0NwsPUo_Mr6IwA.jpeg" /><figcaption>3D printed back panel</figcaption></figure><p>It looks good overall. But there was relatively large distortion especially in the upper area. I should have designed the back panel differently or should have chosen a different material. Because of distortion, it is a bit tight to fit the back panel into the HAL 9000 model. But fortunately, it was not a big problem.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uTH7Gh0RDS70-RXE20tnaA.jpeg" /><figcaption>Distortion in the upper area of the back panel</figcaption></figure><p>Although there are still some to improve, my 3D design is <a href="https://github.com/jj-underwood/hal9000.git">here</a>.</p><h3>Building HAL 9000 version of Sensor Node</h3><p><strong>The HAL 9000 Model</strong></p><p>The HAL 9000 model comes with 4 gray parts (the faceplate, the lens frame, the lens mount/focusing ring and the speaker grille), 2 clear parts (the inner lens and the outer lens), the display base, the LED and decals. I won’t use the display base and the LED in this project. Assembling those parts is easy. But I need to overcome two things — making holes on the speaker grille and painting gray parts.</p><p>In terms of making holes on the speaker grille, I used <a href="https://www.amazon.com/dp/B08VHYX96W">a mini rotary tool</a> and created 24 columns x 18 rows = 432 holes.</p><p>The most difficult part was painting. When I was a child, I tried painting Gundam models (Gunpla) and I always ended up in miserable and horrible results. Since then, I have never done any model building and painting. I saw instructions on the Internet (like <a href="https://www.youtube.com/watch?v=8ayj0K1sJRM">this</a>, <a href="https://www.youtube.com/watch?v=WQg_OyNpalY">this</a> and <a href="https://forum.finescale.com/t/moebius-2001-hal-9000-kit/251766/12">this</a>) and bought TAMIYA paint spray rattle cans</p><ul><li><a href="https://www.tamiyausa.com/shop/finishing/fine-surface-primer-l/">FINE SURFACE PRIMER L</a> for all parts.</li><li><a href="https://www.tamiyausa.com/shop/spray-ts-plastics/ts-29-semi-gloss-black/">TS-29 SEMI GLOSS BLACK</a> (I could not find Semi Matt Black and bought Semi Gloss Black instead) for the faceplate and the outside of the lens mount/focusing ring.</li><li><a href="https://www.tamiyausa.com/shop/spray-ts-plastics/ts-6-matt-black/">TS-6 MATT BLACK</a> for the inside of the lens mount/focusing ring.</li><li><a href="https://www.tamiyausa.com/shop/spray-ts-plastics/ts-17-gloss-aluminum/">TS-17 GLOSS ALUMINUM</a> for the frame of the faceplate and the lens frame.</li><li><a href="https://www.tamiyausa.com/shop/spray-ts-plastics/ts-42-light-gun-metal/">TS-42 LIGHT GUN METAL</a> for the speaker grille.</li></ul><p>I created a paint booth with some cardboards in my garage and painted all the parts. I forgot to wash them before painting as suggested in some instructions. And I didn’t use a box fan as well. Although my process might have not been ideal, I’ve done my paint job successfully, I think. My wife complained about paint smell though.</p><p>I’ve put decals on the faceplate and the lens mount/focusing ring. As suggested on the Internet (like <a href="https://www.dembrudders.com/moebius-11-hal-9000-kit-review.html">this</a>), I used <a href="https://www.hobbylobby.com/crafts-hobbies/model-kits/tools-adhesives/parts-cement-window-maker/p/1079">Testers Parts Cement &amp; Window Maker</a> to glue the inner lens and the outer lens on the lens mount/focusing ring. Lastly, I glued the lens mount/focusing ring, the lens frame and the speaker grille to the faceplate.</p><p>Now, the HAL 9000 model has been completed. It looks really good.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eUnVkasG1DWAthi8SLujsQ.jpeg" /><figcaption>The HAL 9000 model — painted</figcaption></figure><p><strong>PCB board and components</strong></p><p>As I use a stereo pair of speaker units, I attach two MAX98357A amps to the main board — one for the left speaker unit and another one for the right speaker unit. We can configure MAX98357A to output only left channel, only right channel or mixed channels by soldering a resistor around SD pin. Honestly, it was <a href="https://learn.adafruit.com/adafruit-max98357-i2s-class-d-mono-amp/pinouts#sd-slash-mode-2693328">confusing </a>to me, but <a href="https://www.az-delivery.de/en/blogs/azdelivery-blog-fur-arduino-und-raspberry-pi/max98357-was-ist-eigentlich-i2s">this article</a> helped me a lot. I soldered 100K resistor between 5V and SD pin of MAX98357A for the left and 500K resistor between 5V and SD pin of MAX98357A for the right. I also soldered 100K resistor between GND and GAIN pin of MAX98357A for the left and 100K resistor between GND and GAIN pin of MAX98357A for the right to have the maximum 15dB gain.</p><p>And then, I soldered a Lolin S3 directly onto the main board with male pin headers. Although it is better to make it detachable, I prioritized on reducing the height.</p><p>I used <a href="https://www.amazon.com/dp/B0C6WY7DZM">a pre-crimpled cable kit of JST ZH connectors</a> for sub boards (SHT41 and BH1750) and SPS30 to connect them to the main board. And I soldered female headers for BMP280, SGP41 and SCD40 sensor boards onto the main board. And I soldered male headers to them so that I can just attach them to the main board. Because I placed through holes for BMP280/SGP41 and SCD40 too close, there was a little bit conflict between them, but I managed to place them. This will be a future improvement. Regarding LD2410C, I cut one end of a pre-crimpled JST ZH cable and directly soldered it on a pin. In terms of the exhaust fan, I just connected it onto the JST SH header on the main board.</p><p>For ICS-43434, I’ve done the same approach with LD2410C by cutting one end of a pre-crimpled JST ZH cable and directly soldered it on a pin. And for MAX98357A, I’ve done the same approach with sensor boards by using female headers and male headers. Regarding a buzzer, I created conversion cables from JST ZH connectors to DuPont female connectors.</p><p>Regarding buttons, I placed them onto the sub board and soldered them. Although I designed through holes for buttons based on <a href="https://www.honyone.com/en/products/show_117.html">the official layout data</a>, I broke two buttons’ LED pins when I placed them onto the sub board because LED pins are too thin. This will be yet another future improvement.</p><p>I used a pre-crimpled JST ZH cable for the DIP LED sub board. But when I tried to solder a DIP LED onto the sub board, I realized that I made a mistake in the sub board design for the DIP LED. I placed through-holes in the order of Vin, Data-in and GND on the sub board. But the order of legs of DIP LED was like <a href="https://www.adafruit.com/product/1938">Data-in, Vin and GND</a>. So, I had to bend legs to solder them to through-holes on the sub board. I also had to use heat shrink tubes so that DIP LED legs would not touch each other. This will be another future improvement. In terms of the LED string, I used <a href="https://www.amazon.com/dp/B08G188ZQ8">a pre-crimpled cable kit of JST XH connectors</a>. I cut off the existing JST SM connector from the strip and soldered JST XH cables instead.</p><p>When it comes to connections between the main board and the UPS module, I used a pre-crimpled JST XH cable for the power input from the UPS module and a pre-crimpled JST ZH cable for input power monitoring.</p><p><strong>Back Panel</strong></p><p>I’ve put all the components onto the back panel. I used M2 screws for almost all the components except SPS30, LD2410C and speaker units which don’t have any screw holes.</p><p>3D printed M2 screw holes were not precisely accurate but mostly worked well. Although some screw holes didn’t work because of distortion or my design flaw, I could use self-tapping screws and hold components anyway. It was a bit difficult to use screws for some screw holes because of their positions and orientations. But I managed to put all the components anyway. If I have another opportunity to build the HAL 9000 version of the sensor node again, I may want to improve my design.</p><p>Regarding SPS30, it was a bit tight to place it on the back panel because my design was too tight and there was even distortion. But I managed to push it down into its place anyway.</p><p>In terms of LD2410C and speaker units, I used hot glue. Before I glued speaker units, I stuffed <a href="https://www.hobbylobby.com/fabric-sewing/batting-fillings-forms/filling/premium-polyester-fiberfill---12-ounce/p/36555">polyfill</a> into speaker boxes expecting improvement of sound quality.</p><p>I’ve put a <a href="https://www.amazon.com/dp/B0BVGHRKC4">5mm glass lens</a>, which I bought from Amazon in my V2 project, to the hole for BH1750.</p><p>I attached the LED strip to the angled step surrounding the back panel. Although I could attach all of 60 LEDs, I attached only 23 of them and covered just the upper part of the back panel to reduce power usage for just in case.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7_YDAYrYkpu1RlJyAs9Dvg.jpeg" /><figcaption>All the components put into the back panel</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RN5BI1ff4a0RQshq2ntXkw.jpeg" /><figcaption>Closer look at air vents and buttons</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EE4jMsvk8fUdIxAeAI0Hcw.jpeg" /><figcaption>Back view of the back panel</figcaption></figure><p><strong>HAL 9000 version of Sensor Node</strong></p><p>Now, I just put the back panel, which is packed with the PCB board and all the components, into the HAL 9000 model. It is a bit tight because of distortion and the soldering position of the DIP LED. But I’ve successfully put them all together. Because there is just friction between the HAL 9000 model and the back panel, there might be a risk that the HAL 9000 model fall off from the back panel hooked on the wall. I may use a screw in the screw hole which I created on the right bottom of the back panel. Although it just increases friction and does not securely lock the HAL 9000 model, it might be better than nothing.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4AlKwy7ml8f8rvOVGgi8Sw.jpeg" /><figcaption>HAL 9000 Smart Home Device</figcaption></figure><p><strong>Software Configuration</strong></p><p>Configuration of ESPHome was basically straightforward. But I realized that I could not <a href="https://esphome.io/components/fan/speed.html">change the speed of the fan with a simple configuration</a>. I thought that my choice of GPIO40 for the PWM signal output was a mistake because the official document says that <a href="https://esphome.io/components/output/ledc.html">only GPIO0 to GPIO33 can be used</a>. However, I found that <a href="https://community.home-assistant.io/t/pwm-fan-controller/433316">a configuration example on ESPHome community</a> would work even in my case, and I could change the speed of the fan. When it comes to the lighting of the eye, I decided to use <a href="https://esphome.io/components/light/index.html#pulse-effect">the pulse effect</a> with a little bit customizing for now after testing <a href="https://esphome.io/components/light/index.html#light-effects">existing light effects</a>.</p><pre>esphome:<br>  name: s3-01<br>  friendly_name: s3-01<br>  platformio_options:<br>    board_build.flash_mode: dio<br>  on_boot: <br>    - priority: 200<br>      then:<br>        - ds1307.read_time:<br>            id: time_rtc<br><br>esp32:<br>  board: lolin_s3<br>  framework:<br>    type: arduino<br><br>psram:<br>  mode: octal<br>  speed: 80MHz<br><br>logger:<br>  baud_rate: 0<br><br>api:<br>  encryption:<br>    key: !secret api_key<br>  actions:<br>    - action: rtttl_play<br>      variables:<br>        song_str: string<br>      then:<br>        - rtttl.play:<br>            rtttl: !lambda &#39;return song_str;&#39;<br><br>ota:<br>  - platform: esphome<br>    password: !secret ota_password<br><br>wifi:<br>  ssid: !secret wifi_ssid<br>  password: !secret wifi_password<br><br>  ap:<br>    ssid: !secret ap_ssid<br>    password: !secret ap_password<br><br>external_components:<br>  - source: github://stas-sl/esphome-sound-level-meter<br><br>captive_portal:<br><br>i2c:<br>  sda: GPIO42<br>  scl: GPIO41<br>  scan: True<br><br>uart:<br>  tx_pin: GPIO17<br>  rx_pin: GPIO18<br>  baud_rate: 256000<br>  parity: NONE<br>  stop_bits: 1<br><br>ld2410:<br><br>sensor:<br>  - platform: adc<br>    pin: GPIO8<br>    name: Battery Voltage<br>    unit_of_measurement: V<br>    update_interval: 10s<br>    attenuation: 11db<br>    filters:<br>      - multiply: 2.0<br>  - platform: adc<br>    pin: GPIO9<br>    name: Power Voltage<br>    unit_of_measurement: V<br>    update_interval: 10s<br>    attenuation: 11db<br>    filters:<br>      - multiply: 2.0<br>  - platform: bh1750<br>    name: Illuminance<br>    address: 0x23<br>    update_interval: 60s<br>  - platform: sht4x<br>    address: 0x44<br>    update_interval: 60s<br>    temperature:<br>      name: Temperature<br>      id: temperature_sht41<br>    humidity:<br>      name: Humidity<br>  - platform: sgp4x<br>    address: 0x59<br>    update_interval: 60s<br>    voc: <br>      name: VOC Index<br>    nox: <br>      name: NOx Index<br>  - platform: scd4x<br>    address: 0x62<br>    update_interval: 60s<br>    co2:<br>      name: CO2<br>    temperature:<br>      name: Temperature SCD40<br>    humidity:<br>      name: Humidity SCD40<br>  - platform: sps30<br>    pm_1_0:<br>      name: PM 1.0<br>      id: pm_1_0<br>    pm_2_5:<br>      name: PM 2.5<br>      id: pm_2_5<br>    pm_4_0:<br>      name: PM 4.0<br>      id: pm_4_0<br>    pm_10_0:<br>      name: PM 10.0<br>      id: pm_10_0<br>    pmc_0_5:<br>      name: PMC 0.5<br>      id: pmc_0_5<br>    pmc_1_0:<br>      name: PMC 1.0<br>      id: pmc_1_0<br>    pmc_2_5:<br>      name: PMC 2.5<br>      id: pmc_2_5<br>    pmc_4_0:<br>      name: PMC 4.0<br>      id: pmc_4_0<br>    pmc_10_0:<br>      name: PMC 10.0<br>      id: pmc_10_0<br>    pm_size:<br>      name: Typical Particle Size<br>      id: pm_size<br>    address: 0x69<br>    update_interval: 60s<br>  - platform: bmp280_i2c<br>    address: 0x76<br>    update_interval: 60s<br>    temperature: <br>      name: Temperature BMP280<br>      id: temperature_bmp280_aliexpress<br>      oversampling: 16X<br>      accuracy_decimals: 2<br>    pressure:<br>      name: Pressure<br>  - platform: pulse_counter<br>    pin: GPIO39<br>    name: Fan RPM<br>    id: fan_rpm<br>    unit_of_measurement: &quot;RPM&quot;<br>    accuracy_decimals: 1<br>    filters:<br>      - multiply: 0.5<br>    count_mode:<br>      rising_edge: INCREMENT<br>      falling_edge: DISABLE<br>    update_interval: 10s<br>  - platform: ld2410<br>    light:<br>      name: light<br>    moving_distance:<br>      name : Moving Distance<br>    still_distance:<br>      name: Still Distance<br>    moving_energy:<br>      name: Move Energy<br>    still_energy:<br>      name: Still Energy<br>    detection_distance:<br>      name: Detection Distance<br><br>binary_sensor:<br>  - platform: ld2410<br>    has_target:<br>      name: Presence<br>    has_moving_target:<br>      name: Moving Target<br>    has_still_target:<br>      name: Still Target<br>    out_pin_presence_status:<br>      name: out pin presence status<br>  - platform: gpio<br>    name: Button 01<br>    pin:<br>      number: GPIO21<br>      inverted: True<br>      mode:<br>        input: True<br>        pullup: True<br>    internal: True<br>    on_multi_click:<br>    - timing:<br>      - ON for at most 1s<br>      - OFF for at least 0.5s<br>      then:<br>        - script.execute: volume_up<br>    - timing:<br>      - ON for at most 1s<br>      - OFF for at most 1s<br>      - ON for at most 1s<br>      - OFF for at least 0.2s<br>      then:<br>      - logger.log: &quot;Double Clicked&quot;<br>      - binary_sensor.template.publish:<br>          id: button_01_double_click<br>          state: ON<br>      - delay: 0.5s<br>      - binary_sensor.template.publish:<br>          id: button_01_double_click<br>          state: OFF<br>    - timing:<br>      - ON for 1s to 2s<br>      - OFF for at least 0.5s<br>      then:<br>      - logger.log: &quot;Single Long Clicked&quot;<br>      - binary_sensor.template.publish:<br>          id: button_01_single_long_click<br>          state: ON<br>      - delay: 0.5s<br>      - binary_sensor.template.publish:<br>          id: button_01_single_long_click<br>          state: OFF<br>  - platform: template<br>    id: button_01_double_click<br>    name: Button 01 Double Click<br>    publish_initial_state: True<br>  - platform: template<br>    id: button_01_single_long_click<br>    name: Button 01 Single Long Click<br>    publish_initial_state: True<br>  - platform: gpio<br>    name: Button 02<br>    pin:<br>      number: GPIO14<br>      inverted: True<br>      mode:<br>        input: True<br>        pullup: True<br>    internal: True<br>    on_multi_click:<br>    - timing:<br>      - ON for at most 1s<br>      - OFF for at least 0.5s<br>      then:<br>      - logger.log: &quot;Single Short Clicked&quot;<br>      - binary_sensor.template.publish:<br>          id: button_02_single_short_click<br>          state: ON<br>      - delay: 0.5s<br>      - binary_sensor.template.publish:<br>          id: button_02_single_short_click<br>          state: OFF<br>    - timing:<br>      - ON for at most 1s<br>      - OFF for at most 1s<br>      - ON for at most 1s<br>      - OFF for at least 0.2s<br>      then:<br>      - logger.log: &quot;Double Clicked&quot;<br>      - binary_sensor.template.publish:<br>          id: button_02_double_click<br>          state: ON<br>      - delay: 0.5s<br>      - binary_sensor.template.publish:<br>          id: button_02_double_click<br>          state: OFF<br>    - timing:<br>      - ON for 1s to 2s<br>      - OFF for at least 0.5s<br>      then:<br>      - logger.log: &quot;Single Long Clicked&quot;<br>      - binary_sensor.template.publish:<br>          id: button_02_single_long_click<br>          state: ON<br>      - delay: 0.5s<br>      - binary_sensor.template.publish:<br>          id: button_02_single_long_click<br>          state: OFF<br>  - platform: template<br>    id: button_02_single_short_click<br>    name: Button 02 Single Short Click<br>    publish_initial_state: True<br>  - platform: template<br>    id: button_02_double_click<br>    name: Button 02 Double Click<br>    publish_initial_state: True<br>  - platform: template<br>    id: button_02_single_long_click<br>    name: Button 02 Single Long Click<br>    publish_initial_state: True<br>  - platform: gpio<br>    name: Button 03<br>    pin:<br>      number: GPIO13<br>      inverted: True<br>      mode:<br>        input: True<br>        pullup: True<br>    internal: True<br>    on_multi_click:<br>    - timing:<br>      - ON for at most 1s<br>      - OFF for at least 0.5s<br>      then:<br>        - script.execute: volume_down<br>    - timing:<br>      - ON for at most 1s<br>      - OFF for at most 1s<br>      - ON for at most 1s<br>      - OFF for at least 0.2s<br>      then:<br>      - logger.log: &quot;Double Clicked&quot;<br>      - binary_sensor.template.publish:<br>          id: button_03_double_click<br>          state: ON<br>      - delay: 0.5s<br>      - binary_sensor.template.publish:<br>          id: button_03_double_click<br>          state: OFF<br>    - timing:<br>      - ON for 1s to 2s<br>      - OFF for at least 0.5s<br>      then:<br>      - logger.log: &quot;Single Long Clicked&quot;<br>      - binary_sensor.template.publish:<br>          id: button_03_single_long_click<br>          state: ON<br>      - delay: 0.5s<br>      - binary_sensor.template.publish:<br>          id: button_03_single_long_click<br>          state: OFF<br>  - platform: template<br>    id: button_03_double_click<br>    name: Button 03 Double Click<br>    publish_initial_state: True<br>  - platform: template<br>    id: button_03_single_long_click<br>    name: Button 03 Single Long Click<br>    publish_initial_state: True<br><br>text_sensor:<br>  - platform: ld2410<br>    version:<br>      name: Firmware Version<br>    mac_address:<br>      name: MAC Address<br><br>output:<br>  - platform: ledc<br>    pin: GPIO40<br>    id: fan_pwm<br>  - platform: ledc<br>    pin: GPIO4<br>    id: buzzer<br><br>fan:<br>  - platform: speed<br>    output: fan_pwm<br>    name: Fan Toggle<br>    id: fan_toggle<br>    on_turn_on:<br>     - output.set_level:<br>          id: fan_pwm<br>          level: !lambda |-<br>             return id(fan_speed_override).state/100.0;<br><br>select:<br>  - platform: ld2410<br>    light_function:<br>      name: light function<br>    out_pin_level:<br>      name: out pin level<br><br>light:<br>  - platform: esp32_rmt_led_strip<br>    id: onboard_led<br>    name: Onboard LED<br>    rgb_order: RGB<br>    chipset: WS2812<br>    num_leds: 1<br>    pin: 38<br>    rmt_channel: 0<br>  - platform: esp32_rmt_led_strip<br>    id: strip_led<br>    name: Strip LED<br>    rgb_order: GRB<br>    chipset: SK6812<br>    num_leds: 23<br>    pin: 47<br>    is_rgbw: True<br>    rmt_channel: 2<br>  - platform: esp32_rmt_led_strip<br>    id: dip_led<br>    name: DIP LED<br>    rgb_order: RGB<br>    chipset: WS2811<br>    num_leds: 1<br>    pin: 48<br>    rmt_channel: 1<br>    effects:<br>      - pulse:<br>      - pulse:<br>          name: Custom Pulse<br>          min_brightness: 50%<br>          max_brightness: 100%<br>          transition_length:<br>            on_length: 2s<br>            off_length: 1s<br>          update_interval: 3s<br><br>rtttl:<br>  output: buzzer<br>  id: my_rtttl<br>  gain: 60%<br>  on_finished_playback:<br>    - logger.log: &#39;Song ended!&#39;<br><br>i2s_audio:<br>#  - id: i2s_in<br>#    i2s_lrclk_pin: GPIO5<br>#    i2s_bclk_pin: GPIO7<br>  - id: i2s_out<br>    i2s_lrclk_pin: GPIO10<br>    i2s_bclk_pin: GPIO11<br><br>#microphone:<br>#  - platform: i2s_audio<br>#    id: i2s_mic<br>#    i2s_audio_id: i2s_in<br>#    adc_type: external<br>#    pdm: False<br>#    channel: left<br>#    i2s_din_pin: GPIO6<br><br>media_player:<br>  - platform: i2s_audio<br>    id: i2s_speaker<br>    name: I2S Speaker<br>    i2s_audio_id: i2s_out<br>    dac_type: external<br>    i2s_dout_pin: GPIO12<br>    mode: stereo<br><br>#speaker:<br>#  - platform: i2s_audio<br>#    id: i2s_speaker<br>#    i2s_audio_id: i2s_out<br>#    dac_type: external<br>#    i2s_dout_pin: GPIO12<br><br>i2s:<br>  ws_pin: 05<br>  bck_pin: 07<br>  din_pin: 06<br>  sample_rate: 48000<br>  bits_per_sample: 32<br>  channel: left<br>  bits_shift: 8<br><br>sound_level_meter:<br>  update_interval: 150s<br>  buffer_size: 1024<br>  mic_sensitivity: -26dB<br>  mic_sensitivity_ref: 94dB<br>  groups:<br>    - filters:<br>        - type: sos<br>          coeffs:<br>            # A-weighting:<br>            #       b0           b1            b2             a1            a2<br>            - [0.16999495, 0.741029, 0.52548885, -0.11321865, -0.056549273]<br>            - [1., -2.00027, 1.0002706, -0.03433284, -0.79215795]<br>            - [1., -0.709303, -0.29071867, -1.9822421, 0.9822986]<br>      # nested groups<br>      sensors:<br>        - type: eq<br>          name: LAeq_1min<br>          id: LAeq_1min<br>          unit_of_measurement: dBA<br>        - type: max<br>          name: LAmax_125ms_1min<br>          id: LAmax_125ms_1min<br>          window_size: 125ms<br>          unit_of_measurement: dBA<br>        - type: min<br>          name: LAmin_125ms_1min<br>          id: LAmin_125ms_1min<br>          window_size: 125ms<br>          unit_of_measurement: dBA<br><br>script:<br>  - id: volume_up<br>    then:<br>      - homeassistant.service:<br>          service: media_player.volume_up<br>          data:<br>            entity_id: media_player.s3_02_i2s_speaker<br>      - logger.log: &quot;vol up trigger&quot;<br>  - id: volume_down<br>    then:<br>      - homeassistant.service:<br>          service: media_player.volume_down<br>          data:<br>            entity_id: media_player.s3_02_i2s_speaker<br>      - logger.log: &quot;vol down trigger&quot;<br><br>number:<br>  - platform: template<br>    name: Fan Speed Override<br>    id: fan_speed_override<br>    internal: false<br>    max_value: 100.0<br>    min_value: 0.0<br>    step: 10.0<br>    optimistic: true<br>    mode: slider<br>    on_value:<br>      then:<br>        if:<br>          condition:<br>            fan.is_on: fan_toggle<br>          then:<br>          - output.set_level:<br>              id: fan_pwm<br>              level: !lambda &quot;return x/100;&quot;<br><br>time:<br>  - platform: ds1307<br>    id: time_rtc<br>    address: 0x68<br>    timezone: America/Los_Angeles<br>    update_interval: never<br>  - platform: homeassistant<br>    id: time_ha<br>    update_interval: 60s<br>    on_time_sync: <br>      then:<br>        - ds1307.write_time:<br>            id: time_rtc<br>        - delay: 1s<br>        - ds1307.read_time:<br>            id: time_rtc</pre><h3><strong>Result</strong></h3><p>Overall, it was successful.</p><ul><li><strong>Environment Monitoring</strong>: Regarding Temperature and Relative Humidity, the approach of using a sub board has worked well. Unlike V2, measurement is almost the same with my reference when the back panel is not closed. When the back panel is closed with the HAL 9000 model, measurement seems to be affected by thermal conduction probably through the warmed air inside or through the back panel (+2°C to +3°C). But once I turn on the exhaust fan in 30% speed (3000 to 3500 rpm), measurement becomes reasonable compared to my reference (less than +1°C). Although the exhaust fan makes some level of noise, I think it is effective this time. The position of BH1750 is not good as expected. Because it is on the side of the back panel, it does not capture enough ambient light. And because there is the LED strip surrounding the back panel, Illuminance measurement is affected by the light from the LED when the LED is turned on. Other metrics like CO2, VOC index, NOx index, PM, Pressure and Presence are measured as expected.</li><li><strong>Microphone, Amplifier and Speaker</strong>: Because the hole for the mic is on the side, it might not be ideal for capturing voice in Voice Assistant. But it has worked well at least for Ambient Sound Level monitoring. The sound of the speaker is cheap. I’m not sure if polyfill stuffing actually made any difference for affordable tiny speaker units and small PETG speaker boxes. And the sound is somewhat unclear because the speaker grille blocks the sound. I created a lot of holes on the speaker grille though. But anyway, it’s acceptable. When it comes to Voice Assistant, it has worked as well. But I’m not sure if it would fit in my daily life.</li><li><strong>Button</strong>: All the three buttons have worked well. When I push a button, a button LED turns on and a button click event is captured. I used single click events of two buttons as volume-up and volume-down of the sound. They have worked well.</li></ul><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FDOKj0NZgNdk%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DDOKj0NZgNdk&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FDOKj0NZgNdk%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/fee50b0c0a923900963e1c97e6f6f252/href">https://medium.com/media/fee50b0c0a923900963e1c97e6f6f252/href</a></iframe><ul><li><strong>LED</strong>: The DIP LED for the eye has worked very well. Although the eye of HAL 9000 is supposed to be just “red”, adding capabilities of changing color, brightness and even having some effects is an upgrade from the original HAL 9000 model. The pulse effect with a little bit customization gives a great character. The LED strip on the back panel has also worked well. Because I created a gap and an angle between the LED strip and the wall, it creates great ambient lighting.</li></ul><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FnuhhhuBi3sI%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DnuhhhuBi3sI&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FnuhhhuBi3sI%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/3710f40e629e8754c29ba89dc98eb5bf/href">https://medium.com/media/3710f40e629e8754c29ba89dc98eb5bf/href</a></iframe><ul><li><strong>Emergency Battery</strong>: The UPS module has actually worked well. When I plugged off a USB-C cable, Lolin S3 and other components continue working without any disruption. Based on the measurement of the output of the UPS module, the output voltage has fluctuation, and it is around 4.5 V or 4.6 V in average. The measurement of the input of the UPS module has also worked. When I plug in a USB-C cable, it shows voltage around 4.9 V to 5 V with a little bit fluctuation. And when I plug off a USB-C cable, it goes to 0 V. Which means that I can use the measurement as a trigger for some automations in an emergency situation.</li><li><strong>RTC</strong>: Although I still have not used it for any use case, it has worked well. I realized that I could use DS3231 for waking up ESP32 from deep sleep by connecting SQW pin of DS3231 to one of GPIO pins of ESP32. This will be an improvement for the future.</li></ul><p>Environment metrics data measured by HAL 9000 are collected to Home Assistant, <a href="https://medium.com/@jj.underwood/sending-environment-data-from-home-assistant-to-aws-3d9a793974b4">sent to AWS</a>, <a href="https://medium.com/@jj.underwood/using-dynamodb-and-timestream-together-for-environment-data-281a45b25959">stored in DynamoDB and Timestream</a> and <a href="https://medium.com/@jj.underwood/visualizing-environment-data-in-amazon-timestream-5e6aa4d2bc35">visualized with my serverless application</a>. Click events of buttons are also captured, used for volume-up and volume-down and <a href="https://medium.com/@jj.underwood/diy-smart-button-43d4b461ee5d">sent to AWS</a> as well. Although it is not recommended, I could use HAL 9000 as a Player Provider through Home Assistant MediaPlayers on Music Assistant. It does not always work though.</p><h3><strong>Final Thoughts</strong></h3><p>I successfully transformed the HAL 9000 model with ESPHome and Home Assistant, and achieved All-in-One environment monitoring and even more. Now, HAL 9000 is in my house. Its retro-futuristic and somewhat dystopian appearance matches DIY smart home system in this old house. Honestly, this project went well more than expected. And through the entire process, I could even achieve some level of confidence about PCB board design and 3D design. Next time, it may finally be time to 3D design an entire enclosure.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uohE-I-WacLV723NrrwVsw.jpeg" /><figcaption>HAL 9000 at night</figcaption></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1b40095d5199" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[DIY All-in-One Environment Monitor V2]]></title>
            <link>https://medium.com/@jj.underwood/diy-all-in-one-environment-monitor-v2-e65dd4e96cf2?source=rss-53516c79bb78------2</link>
            <guid isPermaLink="false">https://medium.com/p/e65dd4e96cf2</guid>
            <category><![CDATA[iot]]></category>
            <category><![CDATA[esphome]]></category>
            <category><![CDATA[home-assistant]]></category>
            <category><![CDATA[diy]]></category>
            <category><![CDATA[pcb]]></category>
            <dc:creator><![CDATA[JJ Underwood]]></dc:creator>
            <pubDate>Tue, 08 Apr 2025 22:01:52 GMT</pubDate>
            <atom:updated>2025-04-24T05:21:45.760Z</atom:updated>
            <content:encoded><![CDATA[<p>I built my first All-in-One DIY Environment Monitor several months ago as I described in <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">another post</a>, which is ESPHome based sensor nodes to measure environment metrics and Home Assistant to collect measurement data. Hereinafter I call it V1. I installed nine of V1 sensor nodes in my house. They have been working very well since then and measuring temperature, relative humidity, illuminance, CO2, VOC index, NOx index, PM (Particulate Matter), Pressure, UV index and Human Presence. But in V1 project, I had somethings for the future improvement like heat inside an enclosure affecting measurements, wiring difficulty and so forth. This time, I’m going to build a new version “V2” to address some of them.</p><p><strong>Disclaimer</strong>: Before I dive into the details, let’s set some ground rules:</p><ol><li>Educational Purpose Only: This blog post serves as an educational resource. I’m sharing my experiences, but I’m just a hobbyist, not a professional. Always consult experts for specific advice.</li><li>Assumption of Risk: By following my instructions, you assume full responsibility for any consequences. DIY projects involve risks, so proceed with caution.</li><li>Safety First: Handle electronic components carefully. Use appropriate tools and safety gear. Be aware of electrical hazards. Follow manufacturer guidelines for microcontrollers and sensors.</li></ol><h3>Objectives</h3><p>I’m going to upgrade the <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">V1 sensor node</a> as follows while keeping all of its capabilities to measure environment metrics.</p><ul><li><strong>Adding Exhaust Fan</strong>: In V1, I tried to carefully place components inside an enclosure so that the heat, which comes from components themselves (especially ESP8266 microcontroller), doesn’t affect temperature measurement and warmed air goes from bottom to top through vents. But the result was not successful. The temperature inside the enclosure went up, and I had to adjust measurements anyway. In V2, I’m going to try an exhaust fan to take warmed air out of the enclosure to minimize adjustment, hopefully.</li><li><strong>Adding Night Light</strong>: In V1, I chose a simple plastic case as an enclosure so that it can fit in my house from an aesthetic perspective. Although it is subtle and inconspicuous, my family members and I can still see it, and it doesn’t make anything explicit because it is just an environment monitor. So, I decided to assign an explicit role to a sensor node so that my family members and I can accept that it’s there. Since a sensor node is placed in each room, it’s good to have a role that is necessary or good to have in each room. In V2, I’m going to add a night light. I actually have been using simple night lights, which I bought on Amazon, in my house for more than a year and some of them have been broken recently. It’s a good timing to replace them.</li><li><strong>Adding Ambient Noise Monitoring</strong>: In V1, I decided not to include ambient noise monitoring which I used to measure when I was using <a href="https://global.weathernews.com/innovation/wxbeacon2/">WxBeacon2</a>. In V2, I’m going to undo my decision to achieve all-in-one monitoring.</li><li><strong>Adding Notification</strong>: In V1, I collected measurement data from sensor nodes to Home Assistant. The dashboard of Home Assistant has been the only way to see measurement data. But my family members don’t necessarily want to see the dashboard. So, I’m going to add a feature to let a sensor node alert us when there is something to know (like CO2 level increase, etc.). I could blink or change color of the night light. But it could be difficult to recognize during daytime. In V2, I’m going to add a simple buzzer.</li><li><strong>Adding BLE</strong>: In V1, I used <a href="https://www.wemos.cc/en/latest/d1/d1_mini.html">Lolin D1 mini</a> for ESP8266 microcontroller. It didn’t have BLE capability. But when I used ESP32 like <a href="https://www.wemos.cc/en/latest/s3/s3_mini.html">Lolin S3 mini</a> and <a href="https://www.wemos.cc/en/latest/s3/s3.html">S3</a> in another project to <a href="https://medium.com/@jj.underwood/collecting-data-from-car-with-esphome-and-home-assistant-4e36b48fb24e">collect data from my car</a> and <a href="https://medium.com/@jj.underwood/sending-car-data-from-esp32-to-aws-iot-and-home-assistant-aabf542bf5f9">send them to AWS</a>, I thought BLE capability might have some use cases on a sensor node as well by combining with human presence detection while still balancing privacy and home security. In V2, I’m going to switch from ESP8266 to ESP32, especially from Lolin D1 mini to Lolin S3 mini. S3 mini is a little bit expensive but still small and almost the same size with D1 mini.</li><li><strong>Designing PCB</strong>: In V1, I used a sensor board to measure environment metrics, which is a breakout board with an actual sensor chip on it and easy to use for a casual DIYer like me. I connected sensor boards and a microcontroller with wiring and soldering. Although a sensor board itself is easy to use, a process of wiring and soldering multiple sensor boards and a microcontroller was very challenging for me. In V2, I’m going to design a custom PCB board for the first time to reduce wiring. This is not about a feature of a sensor node, but it is very important for me because I have never had any knowledge about PCB. I’m completely a newbie. But I’m going to give it a try.</li></ul><h3>Requirements</h3><p>In V1, I set three requirements — “Cost”, “Appearance” and “Easy to build” and I’m going to stick with them this time as well. Especially regarding “Cost”, I realized that most of the cost comes from expensive sensors to measure air quality like CO2, VOC, NOx and PM. I need to keep my eyes on the cost in this V2 project as well since I’m going to add more features and even create a custom PCB.</p><h3>Exhaust Fan</h3><p>In V1, I used a simple plastic case as an enclosure that comes in three sizes (like <a href="https://www.aliexpress.com/item/3256803466337955.html">this</a>, <a href="https://www.aliexpress.com/item/3256805757021386.html">this</a>, <a href="https://www.aliexpress.com/item/3256805431668233.html">this</a> and <a href="https://www.aliexpress.com/item/3256801663357671.html">this</a>). Given the size of the case and its vents on top and bottom, the size of a fan needs to be very small. Even the largest case has less than 30mm in height. I’m not sure if such a small fan will be actually effective. But I’m going to give it a try to see how it may or may not work.</p><p>I’d like to power and control a fan from a S3 mini for simplicity. A 5V powered fan will be a simple choice because a S3 mini has a 5V output (VBUS) pin when we power it from USB-C. And I’d like to use a PWM fan to monitor and control fan speed, which has 4 pins — Vin (5V), GND, RPM signal (tacho) and PWM signal. I found several affordable options on AliExpress. One option is just a tiny fan like 20mm or 25mm (like <a href="https://www.aliexpress.com/item/3256805861593014.html">this</a> and <a href="https://www.aliexpress.com/item/3256807382431687.html">this</a>). The air goes into one of large open side and goes out from an opposite open side. Another option is a blower fan (like <a href="https://www.aliexpress.com/item/2255800631575011.html">this</a> and <a href="https://www.aliexpress.com/item/3256806360734140.html">this</a>). The air goes into a large open side and goes out from a sleek outlet in right-angled direction. In either option, I need to decide whether I blow the air into a case or suck the air out of a case. It seems that blowing into the air is considered to be efficient for cooling purpose in the computer case area. But I also need to consider how the air flow could affect PM measurement inside a case. Based on an official document from Sensirion (because I’m using Sensirion PM sensor), I might better avoid high air flow around the inlet and the outlet of the PM sensor. I decided to try sucking the air this time. I’m going to place a tiny fan on the top vents to let it suck the air out of a case.</p><p>I bought <a href="https://www.aliexpress.com/item/3256807382431687.html">a 5V powered, PWM, 4 pins, 25mm fan which I found on AliExpress</a>. It seems to be intended for a raspberry pi. But unlike the official raspberry pi active cooler, which is a 30mm blower fan, it’s a little bit smaller 25mm non-blower tiny fan. It has a JST SH connector like the official raspberry pi active cooler. The pinout looks the same with the official one. If I place a JST SH header on a PCB board, I will be able to easily connect a fan.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gzgmtH9hMNkeSyIPjfz7UA.jpeg" /><figcaption>5V PMW 4pin 25mm Fan</figcaption></figure><h3>Night Light</h3><p>Night light does not have to be very bright. It can be just some level of indirect lighting. Adding a couple of LEDs on the back of the plastic case or around the case would be enough. For simplicity, I prefer to power and control the light from a S3 mini. So, I’m going to use a 5V addressable LED light strip because a S3 mini has a 5V output (VBUS) pin when we power it from USB-C. Because my main use case is a night light, I use SK6812, especially a RGBWW version to have warm white lighting.</p><p>I bought <a href="https://www.aliexpress.com/item/2251832290002435.html">a SK6812 LED strip on AliExpress</a>. I chose 30 LEDs/m density. The strip has 3 wires — Vin (5V), GND and Data. Because I will need only a short length of LED strip, it might be OK to just directly connect a S3 mini to a LED strip — VBUS to 5V input, GND to GND and one of GPIO to Data. Though, it seems to be better to use a level shifter between GPIO and Data. I found <a href="https://electricfiredesign.com/2021/03/12/logic-level-shifters-for-driving-led-strips/">a great article</a> on the Internet and decided to use 74LV1T34.</p><p>The LED strip came with a JST SM connector which already attached to 3 wires. And it came with another short cable which has a JST SM connector in one end. Since JST SM is for wire-to-wire connection, there is no JST SM header which I can place on a PCB board. I’m going to cut the attached 3 wires and solder wires with XH connector instead. I bought <a href="https://www.amazon.com/dp/B08G188ZQ8">a pre-crimped cable set from Amazon</a>.</p><h3>Ambient Noise Monitoring</h3><p>I need a microphone to measure noise level. I found that ESPHome supports <a href="https://esphome.io/components/i2s_audio.html">I2S Audio</a> and there are I2S Microphones. At the time of this writing, ICS-43434 seems to be a good option. I bought <a href="https://www.adafruit.com/product/6049">an ICS-43434 breakout board from Adafruit</a>.</p><p>I also found <a href="https://github.com/stas-sl/esphome-sound-level-meter">a very nice external component to measure ambient sound level</a> with I2S microphone.</p><h3>Buzzer</h3><p>Because it’s just for a notification, I’m going to use a simple one. I noticed that <a href="https://esphome.io/components/rtttl.html">a passive piezo buzzer is already supported by ESPHome</a>.</p><p>I found <a href="https://www.aliexpress.com/item/3256806584017777.html">an affordable one on AliExpress</a>, which looks like the same type with one supported by ESPHome. The buzzer came with 3 pins — Vin (5V), GND and Data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*t5SXw8zXvkVHhBzXzq-VQQ.jpeg" /><figcaption>Buzzer</figcaption></figure><h3>BLE</h3><p>Lolin S3 mini already has BLE capability. One concern is that it is resource intensive. Since I2S is also resource intensive, using both I2S and BLE might cause some issues as described on <a href="https://esphome.io/components/microphone/i2s_audio">ESPHome web site</a>. But I’m going to give it a try.</p><h3>PCB</h3><p>There are many CAD software and PCB manufacturing services. I’m not going to compare them and discuss which one is good or not because I don’t have enough knowledge anyway. I decided to use <a href="https://easyeda.com/">EasyEDA</a> and <a href="https://jlcpcb.com/">JLCPCB</a> this time because I don’t have to install anything on my computer and thus found them easy to try.</p><p>Using EasyEDA was lots of try-and-error for me. I used “Std” version. Although it is free and easy-to-start, it was not necessarily intuitive for a newbie like me. Many online articles and blogs helped me a lot. After lots of try-and-error processes, I needed to make some decisions like “PCB or PCBA” and “Sensor Chip or Sensor Board”.</p><p><strong>PCB or PCBA</strong></p><p>Once we design a PCB board on EasyEDA, we can place an order of either just manufacturing the PCB board or even assembling components on the PCB board as an option (PCBA). A component is anything we put on a PCB board, like a resistor, a capacitor, a pin header, a sensor chip and so forth. If we just order a PCB board, we need to solder necessary components on the PCB board by ourselves. If we order a PCB board + PCBA option, we can receive the PCB board with components soldered on it. For each component, we can choose either assembling it with PCBA option or not just by checking “Add into BOM” option on EasyEDA. And there are some components that we want to use but are not listed as “JLCPCB Assembled” in the parts library on EasyEDA, which means that we cannot add them into BOM for the PCBA option. In case of that, we can still design a PCB board thanks to the community feature of EasyEDA. We can find a wide variety of components listed as “User Contributed” in the parts library on EasyEDA. By using “User Contributed” components which have footprint data, we can design a PCB board. And we can buy those components somewhere else (AliExpress, Adafruit, and so forth) and solder them on the PCB board by ourselves after we receive the PCB board. Since I’m not good at soldering, PCBA will be helpful. But of course, it comes with the cost. The cost of PCBA depends on some factors like the followings.</p><ul><li><strong>Assembly Type (“Economic” or “Standard”)</strong>: Assembly requires “setup fee” and “Standard” assembly needs higher setup fee than “Economic” assembly. If we want to go cheap, we need to avoid some conditions that require “Standard” assembly. For example, some very tiny components need “Standard” assembly, placing a component on the back of the board (double-sided PCB) needs “Standard” assembly, and so forth.</li><li><strong>Component Cost</strong>: We need to make sure the unit cost of a component is acceptable by comparing it with the same one which we can buy somewhere else like AliExpress, Adafruit and so forth.</li><li><strong>Component Type (“Basic” or “Extended”)</strong>: Resistors, capacitors and so forth, especially in ordinary sizes, tend to be categorized as “Basic” component. But sensor chips tend to be categorized as “Extended” component. “Extended” component requires an additional fee (3 USD at the time of this writing). For example, if we place 5 different “Extended” sensor chips, we need additional 15 USD. Considering affordable sensor boards we can buy from AliExpress, we cannot just ignore 3 USD.</li></ul><p>If we order lots of PCB boards and components, these additional costs will be balanced. But these will be relatively huge for a weekend project like this. If we want to keep the cost lower, we need to consider factors above when we design a PCB board.</p><p><strong>Sensor Chip or Sensor Board</strong></p><p>As I described above, I prefer to use a sensor board in general. Soldering a tiny sensor chip is way beyond my poor soldering skill. But a sensor board tends to come with on-board pull-up resistors for I2C bus. And a sensor board is much larger than an actual sensor chip. If we use multiple sensor boards, we will have redundant pull-up resistors and we will need a larger enclosure. In this project, I might be able to use a sensor chip if I use PCBA without soldering by myself. But it depends on the cost as described. If the cost of PCBA is too expensive for a sensor chip, I need to use an equivalent sensor board instead which I can buy from AliExpress, Adafruit and so forth.</p><ul><li><strong>Temperature and Relative Humidity</strong>: Based on my experience in V1, I stick with <a href="https://esphome.io/components/sensor/sht4x.html">SHT41</a> in V2. I can find some SHT41 sensor chips as “JLCPCB Assembled” in the parts library on EasyEDA, like SHT41-AD1B, SHT41-AD1F (with membrane cover), SHT41I (for Industry), SHT41A (for Automotive) and some revisions of each (R1 or R2). At the time of this writing, they are all “Extended” components. But I found that at least <a href="https://jlcpcb.com/partdetail/Sensirion-SHT41_AD1BR2/C7461861">SHT41-AD1B-R2</a> is eligible for “Economic” assembly and its unit cost is not that expensive compared to a sensor board I bought from AliExpress. So, I decided to use a sensor chip this time.</li><li><strong>Illuminance and UV index</strong>: In V1, I used <a href="https://esphome.io/components/sensor/bh1750.html">BH1750</a> for illuminance and <a href="https://esphome.io/components/sensor/ltr390.html">LTR390</a> for illuminance and UV index. Since I have not found a good use case for UV index, I’m going to use either one of them in V2 to measure at least illuminance. I can find a <a href="https://jlcpcb.com/partdetail/ROHMSemicon-BH1750FVITR/C78960">BH1750 sensor chip</a> as “JLCPCB Assembled” in the parts library, and it is categorized as “Extended” but eligible for “Economic” assembly at the time of this writing. But as I described in <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">another post about V1</a>, having an optical window for a sensor chip and guiding the light to a sensor chip is important for measuring illuminance. If I place a sensor chip on a PCB board, that I will place on the back plate of the case, there will be distance from the cover of the case, and it will be challenging to guide the light onto the sensor chip. So, I need to use a sensor board so that I can place it closer to the cover. But it does not necessarily mean I cannot use a sensor chip. If I can design a custom sensor board for a BH1750 sensor chip by myself, I can use a sensor chip on it. Regarding LTR390, I couldn’t find any in the parts library at the time of this writing. So, I need to buy a sensor board somewhere if I use LTR390.</li><li><strong>CO2</strong>: In V1, I used <a href="https://esphome.io/components/sensor/scd4x.html">SCD40</a>. I’m going to use the same SCD40 or similar SCD4X for V2. I can find some SCD4X sensor chips as “JLCPCB Assembled” in the parts library. But at the time of this writing, the unit cost is relatively expensive compared to a sensor board I can buy from AliExpress. And they require “Standard” assembly. So, I decided to continue to use a sensor board from AliExpress.</li><li><strong>VOC and NOx</strong>: In V1, I used <a href="https://esphome.io/components/sensor/sgp4x.html">SGP41</a>. I’m going to use the same SGP41 for V2. I can find a SGP41 sensor chip as “JLCPCB Assembled” in the parts library. But at the time of this writing, it’s out of stock. So, I decided to use a sensor board from AliExpress.</li><li><strong>PM</strong>: In V1, I used <a href="https://esphome.io/components/sensor/sen5x.html">SEN55</a>. But it’s large and could not fit in a plastic case with an official holder from Sensirion. I managed to fit it inside the case by thinning the official holder. But the PCB board design will be challenging this time. Newly released SEN6x looks relatively smaller than SEN5x, but it is still thick and expensive. Although SEN5x allows me to ditch other sensor chips or boards like temperature, relative humidity, VOC and NOx since it includes those within it, I decided to use <a href="https://esphome.io/components/sensor/sps30.html">SPS30</a> instead of SEN5x this time. SPS30 seems to be a little bit old compared to SEN5X, but it is much smaller. Like SEN5x, SPS30 is more like a sensor “module” rather than a sensor “chip” or a sensor “board”. I could not find SPS30 as “JLCPCB Assembled” in the part library. So, I decided to buy SPS30 somewhere and found one on AliExpress.</li><li><strong>Pressure</strong>: In V1, I used <a href="https://esphome.io/components/sensor/bmp280.html">BMP280</a>. There seem to be other options like <a href="https://esphome.io/components/sensor/bmp3xx.html">BMP388 or BMP390</a> nowadays. I can find those sensor chips as “JLCPCB Assembled” in the parts library. But BMP280 is out of stock at the time of this writing and BMP388/BMP390 are relatively expensive compared to sensor boards I can buy from AliExpress. They require even “Standard” assembly. I also considered other options like <a href="https://esphome.io/components/sensor/mpl3115a2.html">MPL3115A2</a>, <a href="https://esphome.io/components/sensor/ms5611.html">MS5611</a>, and <a href="https://esphome.io/components/sensor/dps310.html">DPS310</a>, but they are relatively expensive and requires “Standard” assembly as well. I decided to use a sensor board of BMP280 from AliExpress.</li><li><strong>Presence</strong>: In V1, I used <a href="https://esphome.io/components/sensor/ld2420.html">LD2420</a> because <a href="https://esphome.io/components/sensor/ld2410.html">LD2410C</a>, which I tried first, was somehow unstable. I’m going to use LD2410C this time again. I can find LD2410C sensor “board’ as “JLCPCB Assembled” in the parts library at the time of this writing. But since I already have LD2410C, I’m going to use the one I already have.</li><li><strong>Ambient Noise</strong>: As I described earlier, I’m going to use ICS-43434. I can find an ICS-43434 sensor chip as “JLCPCB Assembled” in the parts library. It is “Extended” component but eligible for “Economic” assembly. However, it seems that it requires an additional fee for X-ray soldering check. And most importantly, we need a mic hole on a PCB board. If I place a sensor chip on a PCB, a mic hole needs to be opened to the back of the case, which is not good. So, I decided to use a sensor board so that I can place a mic hole closer to the cover of the case.</li></ul><p>As a result, I can use a sensor chip for SHT41 and possibly BH1750. If I use a sensor chip for BH1750, there will be 3 sets of pull-up resistors on I2C bus (SCD40, SGP41 and BMP280). If I buy a sensor board of BH1750 or LTR390, there will be 4 sets of pull-up resistors. Although combined resistance of 4 sets of pull-up resistors is still higher than the minimum requirement and it might be OK, it’s a little bit disappointing because I cannot reduce redundant pull-up resistors compared to V1. So, I’m going to figure out a way to use a sensor chip for BH1750 and create a custom sensor board.</p><p><strong>Designing and Ordering PCB</strong></p><p>Now, I’m going to design a PCB board to place necessary components and connect all of them.</p><p>In V1, I used three sizes of cases and built the Basic, the Plus and the Max sensor nodes where each of them has only necessary sensor boards. In V2, I’m going to design the only one PCB board for the large case that has only an on-board SHT41 sensor chip and female headers or connectors for other sensors. I can just attach necessary sensor boards or sensor modules for each sensor node. Though I’m not sure if it is a good idea to make it more like a custom breadboard and rely on pin headers for connections.</p><p>Regarding placement of components, I decided to place a S3 mini at the bottom center of the PCB board so that I can connect a USB-C cable from outside of the case. And I place SPS30 on the left side and the exhaust fan on the top right so that the air coming out from the outlet of SPS30 won’t go back to the inlet, hopefully. I also place a sensor chip and sensor boards on the right side to get benefit of the air flow, hopefully.</p><p>When it comes to actual connections between the PCB board and components, I decided as follows.</p><ul><li><strong>Microcontroller</strong> (S3 mini): I placed through-holes and female headers for a S3 mini so that I can attach a S3 mini on those female headers. If I use male pin headers, I can reduce the height of a USB-C connector of a S3 mini. But it will be still higher than a cable hole on the bottom of the case and it cannot be accessible from outside of the case. So, I’m going to open or widen a hole on the bottom of the case somehow. And I decided to use PCBA option for female headers because it’s just a lot of soldering. I also placed additional through-holes and female headers for S3 mini breakout for future experiment. And I placed decoupling capacitors (ceramic, 0603 size, 10µF) near 3.3V and 5V (VBUS) pins and decided to use PCBA option for them.</li><li><strong>Sensor Chip</strong> (SHT41): I placed a sensor chip of SHT41 directly on the PCB board and decided to use PCBA option as I described. I didn’t place pull-up resistors for I2C because I will add one or more sensor boards which have already on-board pull-up resistors. According to <a href="https://sensirion.com/media/documents/FC5BED84/662B494D/Sensirion_Humidity_Temperature_Design_Guide.pdf">the official design guide</a>, I need to avoid thermal conduction through the PCB board. It seems that I better use “milled slits” around a sensor chip. So, I decided to place the sensor chip close to the bottom vents and create cutouts around the sensor chip. I also placed a small decoupling capacitor (ceramic, 0603 size, 0.1µF) near a sensor chip and decided to use PCBA option.</li><li><strong>Sensor Chip </strong>(BH1750): I decided to create a custom sensor board with a sensor chip of BH1750 as I described. But I didn’t want to order two different PCB designs because it would be costly. I realized that it would be possible to <a href="https://easyeda.com/forum/topic/How-to-incorporate-a-smaller-pcb-in-cutout-of-another-pcb-f9cca898b3d348929aeb294a52c0f858">put different PCB designs in one design</a>. So, I decided to put a PCB board for V2 and a custom sensor board for BH1750 together in one PCB design. I placed through-holes for female headers to attach a custom sensor board on the main PCB board and placed through-holes for male pin headers on the custom sensor board. I also placed a small decoupling capacitor (ceramic, 0603 size, 0.1µF) near a sensor chip and decided to use PCBA option.</li><li><strong>Sensor Board</strong> (SCD40, SGP41, BMP280, ICS-43434 and LD2410C): I placed through-holes for sensor boards and decided to use female headers so that I can attach necessary sensor boards by myself. I decided to use PCBA option for female headers if they are available in stock. And I’m not going to place decoupling capacitors for sensor boards because they seem to already have capacitors.</li><li><strong>Sensor Module</strong> (SPS30): I placed a JST ZH header so that I can connect a SPS30 with a cable that has JST ZH connectors in both ends. And I decided to use PCBA option for a JST ZH header. Regarding the cable, I bought <a href="https://www.amazon.com/dp/B0C6WY7DZM">a pre-crimped ZH cable set from Amazon</a>. I also placed a small decoupling capacitor (ceramic, 0603 size, 0.1µF) near the JST ZH header and decided to use PCBA option.</li><li><strong>Exhaust Fan</strong>: I placed a JST SH header so that I can connect an exhaust fan with a cable that has a JST SH connector in one end. I also placed a 2K resistor as pull-up for RPM signal reading and decided to use PCBA option for the resistor. I also placed a small decoupling capacitor (ceramic, 0603 size, 0.1µF) near the JST SH header and decided to use PCBA option.</li><li><strong>LED Strip</strong> (SK6812): I decided to place a JST XH header so that I can connect a LED strip with a cable that has a JST XH connector in one end. And I decided to use PCBA option for a JST XH header. Regarding the cable, I bought a pre-crimped XH cable set as described earlier. I also placed a level shifter (74LV1T34) and decided to use PCBA option for it. I also placed a small (ceramic, 0603 size, 0.1µF) and a large decoupling capacitor near a the JST XH header and decided to use PCBA option. Regarding a large capacitor, it seems that the <a href="https://learn.sparkfun.com/tutorials/ws2812-breakout-hookup-guide/hardware-hookup">capacitance between 100µF and 1000µF is recommended</a>. Considering the unit price and the size, I used <a href="https://jlcpcb.com/partdetail/KyoceraAVX-TAJD477K006RNJ/C7229">a tantalum 470µF capacitor</a>.</li><li><strong>Buzzer</strong>: I placed through-holes for a buzzer so that I can attach the buzzer by myself. I also placed a small decoupling capacitor (ceramic, 0603 size, 0.1µF) near the buzzer and decided to use PCBA option.</li></ul><p>After many try-and-errors, my PCB design became like the following. Although I’m just a newbie and it would be just horrible, you can see my PCB design in the pictures below. Since I’ve put the main PCB board and a custom sensor board for BH1750 together in the same PCB design, you can see the BH1750 sensor board sticking out. It’s basically separated from the main PCB board and can be easily detached from the main board.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1018/1*F-tOcq2Es0Qtnmw4UDAXbA.png" /><figcaption>Schematic of PCB Board</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0MbmvjEuTlVwmXcg6G90_A.png" /><figcaption>2D View and 3D View of PCB Board on EasyEDA</figcaption></figure><p>Once I had done my PCB design on EasyEDA, I could seamlessly order the PCB board on JLCPCB. To be honest, I went back and forth between EasyEDA and JLCPCB many times in my try-and-errors though. Since the minimum order is 5 for “Economic Assembly”, the total PCB cost was about 55 USD (PCB: 2 USD and PCBA: 53 USD). It means that the unit cost of the PCB board was 11 USD. Considering it includes SHT41 and BH1750 on-board and eliminates all the wiring tasks, it might be worth it. But the shipping fee was quite expensive and costed 13.4 USD for the cheapest shipping method (Global Standard Direct Line). I’m still not sure if the total cost + the shipping fee + tax would worth it or not, especially for my weekend project. Although EasyEDA gives us discount coupons like “10 USD off for the first time order”, I didn’t take them into account when I calculated the cost above because they are not what we always get.</p><p>This is what I got from JLCPCB after 16 days. It looks great.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Tr9Nyf0KOHAf5kgIsrwGpw.jpeg" /><figcaption>PCB Board from JLCPCB</figcaption></figure><p>As intended, I could easily detach a BH1750 sensor board from a main board with a diagonal cutter.</p><h3>Building DIY Environment Monitor V2</h3><p>The custom PCB board allowed me to make building process much easier. I still needed soldering, but just for pin headers.</p><ul><li>Microcontroller (S3 mini): I soldered male pins to a S3 mini and attached it on the PCB board. Because a USB-C port is way above the cable hole on the bottom of the case, I cut vents on the bottom of the case and created another cable hole so that I can access the USB-C port from outside the case.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kVR3t-dBjJhky15XeYzR7A.jpeg" /><figcaption>Holes for BH1750 (right) and ICS-43434 (left) and USB-C cable</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*e2spTB3quS0oY2thw5iuQg.jpeg" /><figcaption>USB-C cable going through the hole</figcaption></figure><ul><li>Sensor Board (BH1750, SCD40, SGP41, BMP280, ICS-43434 and LD2410C): I soldered male pins to them and attached them to the PCB board. Regarding LD2410C, because it came with male pins soldered, I could just attach them to the PCB board. Because I need to place BH1750, ICS-43434 and LD2410C closer to the cover of the case and avoid conflict with SPS30, I used short female headers and double-stacked them on female headers on the PCB board. I also drilled small holes for BH1750 and ICS-43434 on the cover of the case. Honestly, I regret that holes are not well aligned. To increase the light through the hole, I used a small glass lens that I bought from Amazon.</li><li>Sensor Module (SPS30): I created a cable which has JST ZH connectors in both ends by using a pre-crimped cable kit and connected SPS30 to the JST ZH header on the PCB board. I used a tie-band to place SPS30 on the PCB board.</li><li>Exhaust Fan: Because it came with a JST SH connector, I could just connect it to the JST SH header on the PCB board. I used hot glue to attach the fan to the backplate of the case. Because the clearance between the fan and the cover was too tight than I expected, I needed to tilt the fan so that I could manage to fit it inside the case.</li><li>LED Strip: I created a cable which has JST XH connector on one end by using a pre-crimped cable kit and soldered on another end to the LED strip. I drilled a hole on the back plate of the case and routed the cable of the LED strip to the back of the case. Since the case is too small for a 30LEDs/m strip, I could use only two LEDs.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1IVLwLwy3dHzX2RDGQ2ABA.jpeg" /><figcaption>Drilled a hole for LED strip</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LiWmKfN5b4u3fwIxufgKuA.jpeg" /><figcaption>LED Strip on Back Plate</figcaption></figure><ul><li>Buzzer: Because it came with male pin headers, I could just solder it to the PCB board.</li></ul><p>This is what I’ve put all together.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*21_ZA5AKyBPvXOxujBVUwA.jpeg" /><figcaption>Assembled V2 Sensor Node</figcaption></figure><h3>Configuration</h3><p>Since I used only ESPHome-ready components, configuration was straightforward.</p><pre>esphome:<br>  name: s3-mini<br>  friendly_name: s3-mini<br>  platformio_options:<br>    platform: https://github.com/platformio/platform-espressif32.git<br>    board: lolin_s3_mini<br>    framework: arduino<br>    board_build.flash_mode: dio<br><br>esp32:<br>  variant: esp32s3<br>  board: lolin_s3_mini<br>  framework: <br>    type: arduino<br>    version: latest<br><br>logger:<br>  baud_rate: 0<br><br>api:<br>  encryption:<br>    key: !secret api_key<br><br>ota:<br>  - platform: esphome<br>    password: !secret ota_password<br><br>wifi:<br>  ssid: !secret wifi_ssid<br>  password: !secret wifi_password<br><br>  ap:<br>    ssid: &quot;S3-Mini Fallback Hotspot&quot;<br>    password: !secret ap_password<br><br>captive_portal:<br><br>esp32_ble_tracker:<br><br>external_components:<br>  - source: github://stas-sl/esphome-sound-level-meter<br><br>i2c:<br>  sda: GPIO35<br>  scl: GPIO36<br>  scan: True<br><br>uart:<br>  tx_pin: GPIO43<br>  rx_pin: GPIO44<br>  baud_rate: 256000<br><br>ld2410:<br><br>sensor:<br>  - platform: bh1750<br>    name: Illuminance<br>    address: 0x23<br>    update_interval: 60s<br>  - platform: sht4x<br>    address: 0x44<br>    update_interval: 60s<br>    temperature:<br>      name: Temperature<br>      id: temperature_sht41<br>    humidity:<br>      name: Humidity<br>  - platform: sgp4x<br>    address: 0x59<br>    update_interval: 60s<br>    voc: <br>      name: VOC Index<br>    nox: <br>      name: NOx Index<br>  - platform: scd4x<br>    address: 0x62<br>    update_interval: 60s<br>    co2:<br>      name: CO2<br>    temperature:<br>      name: Temperature SCD40<br>    humidity:<br>      name: Humidity SCD40<br>  - platform: sps30<br>    pm_1_0:<br>      name: PM 1.0<br>      id: pm_1_0<br>    pm_2_5:<br>      name: PM 2.5<br>      id: pm_2_5<br>    pm_4_0:<br>      name: PM 4.0<br>      id: pm_4_0<br>    pm_10_0:<br>      name: PM 10.0<br>      id: pm_10_0<br>    pmc_0_5:<br>      name: PMC 0.5<br>      id: pmc_0_5<br>    pmc_1_0:<br>      name: PMC 1.0<br>      id: pmc_1_0<br>    pmc_2_5:<br>      name: PMC 2.5<br>      id: pmc_2_5<br>    pmc_4_0:<br>      name: PMC 4.0<br>      id: pmc_4_0<br>    pmc_10_0:<br>      name: PMC 10.0<br>      id: pmc_10_0<br>    pm_size:<br>      name: Typical Particle Size<br>      id: pm_size<br>    address: 0x69<br>    update_interval: 60s<br>  - platform: bmp280_i2c<br>    temperature:<br>      name: Temperature BMP280<br>      oversampling: 16x<br>    pressure:<br>      name: Pressure<br>    address: 0x76<br>    update_interval: 60s<br>  - platform: pulse_counter<br>    pin: GPIO4<br>    name: Fan RPM<br>    id: fan_rpm<br>    unit_of_measurement: &quot;RPM&quot;<br>    accuracy_decimals: 1<br>    filters:<br>      - multiply: 0.5<br>    count_mode:<br>      rising_edge: INCREMENT<br>      falling_edge: DISABLE<br>    update_interval: 10s<br><br>binary_sensor:<br>  - platform: ld2410<br>    has_target:<br>      name: Presence<br>    has_moving_target:<br>      name: Moving Target<br>    has_still_target:<br>      name: Still Target<br>    out_pin_presence_status:<br>      name: out pin presence status<br>  - platform: gpio<br>    pin: GPIO16<br>    name: gpio out pin presence<br>    device_class: presence<br><br>select:<br>  - platform: ld2410<br>    light_function:<br>      name: light function<br>    out_pin_level:<br>      name: out pin level<br><br>output:<br>  - platform: ledc<br>    pin: GPIO2<br>    id: fan_pwm<br>  - platform: ledc<br>    pin: GPIO10<br>    id: buzzer<br><br>fan:<br>  - platform: speed<br>    output: fan_pwm<br>    name: Fan Toggle<br>    id: fan_toggle<br><br>light:<br>  - platform: esp32_rmt_led_strip<br>    id: onboard_led<br>    name: Onboard LED<br>    rgb_order: RGB<br>    chipset: WS2812<br>    rmt_channel: 0<br>    num_leds: 1<br>    pin: 47<br>  - platform: neopixelbus<br>    type: GRBW<br>    variant: sk6812<br>    pin: GPIO18<br>    num_leds: 2<br>    name: &quot;LED Light&quot;<br><br>rtttl:<br>  output: buzzer<br>  id: my_rtttl<br>  gain: 60%<br><br>i2s:<br>  ws_pin: 01<br>  bck_pin: 05<br>  din_pin: 03<br>  sample_rate: 48000<br>  bits_per_sample: 32<br>  channel: left<br>  bits_shift: 8<br><br>sound_level_meter:<br>  update_interval: 150s<br>  buffer_size: 1024<br>  mic_sensitivity: -26dB<br>  mic_sensitivity_ref: 94dB<br>  groups:<br>    - filters:<br>        - type: sos<br>          coeffs:<br>            # A-weighting:<br>            #       b0           b1            b2             a1            a2<br>            - [0.16999495, 0.741029, 0.52548885, -0.11321865, -0.056549273]<br>            - [1., -2.00027, 1.0002706, -0.03433284, -0.79215795]<br>            - [1., -0.709303, -0.29071867, -1.9822421, 0.9822986]<br>      sensors:<br>        - type: eq<br>          name: LAeq_1min<br>          id: LAeq_1min<br>          unit_of_measurement: dBA<br>        - type: max<br>          name: LAmax_125ms_1min<br>          id: LAmax_125ms_1min<br>          window_size: 125ms<br>          unit_of_measurement: dBA<br>        - type: min<br>          name: LAmin_125ms_1min<br>          id: LAmin_125ms_1min<br>          window_size: 125ms<br>          unit_of_measurement: dBA</pre><h3>Result</h3><p>Technically, my V2 sensor node worked as expected. It measured all the environment metrics — Temperature and Relative Humidity, Illuminance, CO2, VOC Index, NOx Index, PM 0.5/1.0/2.5/4.0/10.0, Pressure, Presence and Ambient Noise Level. The exhaust fan started spinning when I turned it on, the LED turned on, the buzzer worked, and the PCB board worked as well. All the components worked and ESPHome worked great.</p><p>But considering objectives, I have to admit that the result was completely <strong>failure</strong> this time.</p><ul><li>Although I placed cutouts around a SHT41 sensor on the PCB board, it measured very higher values (+3°C to +5°C) than my reference (WxBeacon2 and SwitchBot Meter) even when I didn’t close the cover. And when I closed the cover, it measured even way higher (+8°C to +9°C) than my reference. Probably because I packed too many components and features within the case, there would be more heat coming out from components and I couldn’t avoid thermal conduction enough. I probably might better separate a temperature and humidity sensor as a sensor board rather than placing it on the main PCB board.</li><li>The exhaust fan was not effective at all. First, my choice of 25mm was too large for the case. As I described, I tilted the fan and managed to stuff it into the case. But once I turned on the fan, it made large rattling noise because it hit the cover and cables. And even if I ignored the noise and kept it running, measured temperature didn’t decrease at all. Probably because placing the fan on top vents to suck the air out of the case was not a good idea, attaching the fan only with hot glue was not a good idea, and the fan was too large for the size of the case but was too small for this purpose. I probably might better expose a temperature and humidity sensor to the air outside the case through a hole or something rather than trying to take the warm air out of the case.</li><li>The LED strip worked. But when I placed a sensor node on the wall, it was not enough for even indirect lighting. I need more gap and angle between the LED and the wall. Attaching the LED strip on the back plate was not just a good idea.</li><li>Although BH1750 can measure the ambient light and ICS-43434 can measure the ambient noise, holes on the cover were not well aligned with them. I might need to think about a better approach.</li></ul><h3>Final Thoughts</h3><p>Although the result was completely failure and disappointing, I learned a lot this time. Especially, it was the first time for me to design a PCB board and it was a very great experience. Since the PCB board worked as expected, it gave me some level of confidence. Based on the failure this time, I’ll try to make improvements in the next version.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e65dd4e96cf2" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Comparing Enclosures for Smart Home Projects]]></title>
            <link>https://medium.com/@jj.underwood/comparing-enclosures-for-smart-home-projects-2e80e82ebee6?source=rss-53516c79bb78------2</link>
            <guid isPermaLink="false">https://medium.com/p/2e80e82ebee6</guid>
            <category><![CDATA[smart-home]]></category>
            <category><![CDATA[esp8266]]></category>
            <category><![CDATA[diy]]></category>
            <category><![CDATA[esp32]]></category>
            <category><![CDATA[iot]]></category>
            <dc:creator><![CDATA[JJ Underwood]]></dc:creator>
            <pubDate>Fri, 10 Jan 2025 19:12:12 GMT</pubDate>
            <atom:updated>2025-01-10T19:19:23.247Z</atom:updated>
            <content:encoded><![CDATA[<p>For a casual DIYer like me, an enclosure is always challenging and important in a weekend DIY smart home project. We don’t want to end up in a prototype made with a bare microcontroller and sensors connected with a breadboard and jumper wires. We want to put them in an enclosure and actually deploy our project achievements in our houses. 3D printing an original enclosure is an ideal solution, but it is still a bit too much at least for me. As described in <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">another post</a>, it requires some efforts and costs. So, affordable and (almost) ready-to-use enclosures, that are typically called “project box”, are very helpful. We can find a wide variety of project boxes on the Internet. In this post, I’m going to compare some of them I found on the Internet especially for an ESP8266/ESP32 microcontroller and sensors.</p><p><strong>Disclaimer</strong>: I’m not affiliated with manufacturers, sellers and businesses of enclosures which I refer to in this post. I’m just a user and a hobbyist.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oYHapWuO5EwK6v_6KdWYng.jpeg" /><figcaption>Enclosures (with Lolin D1 mini and Lolin S3)</figcaption></figure><h3>Requirements</h3><ul><li><strong>Affordable price</strong>: In my case, I cannot afford to spend much on a project box. For example, when I created my sensor nodes in <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">DIY environment monitor</a> project, I needed 9 project boxes. And some I2C sensors were relatively expensive. I had to figure out the way to reduce the total cost to measure environment metrics all over the house. It’s too much for me to spend tens or hundreds of dollars on a project box. I prefer to pick up an affordable project box which is under, let’s say, 5 USD (including shipping fee). The cost requirement depends on each project and each person’s budget though.</li><li><strong>Appearance</strong>: This depends on our preferences. At least I prefer to have simple and subtle appearance of a project box when I place it somewhere in a room, like a wall of a room. It doesn’t have to be either gadget-y with shiny LEDs or heavy-duty.</li><li><strong>Size</strong>: We need to make sure if a project box has enough space to fit all the necessary components — a microcontroller, sensors and wires.</li><li><strong>Mounting mechanism</strong>: We need to think about how to mount a project box somewhere in a house, like a wall. We don’t want to just place it on a desk like a prototyping. An adhesive tape could work in some cases. But in case a box and components have some weight, or they get warmed up and could weaken an adhesive tape, we might need at least a hole to hook it on a wall or a hole to screw it on a wall.</li><li><strong>Vents</strong>: This is not always the case, but we might need vents or holes for air inlets/outlets or lighting depending on what we want to measure with sensors in a box. If you need to have waterproof or weatherproof, you might prefer to have better sealing rather than vents or holes though.</li><li><strong>Cabling mechanism</strong>: We might need to think about cabling as well. Especially when we connect a project box to a certain power source, it needs to have a hole for cabling.</li><li><strong>Durability</strong>: Although a project box does not have to be heavy-duty since we are talking about our DIY smart home projects, it cannot be too fragile and easily broken. We need to balance between the durability and the cost.</li><li><strong>Waterproof/Weatherproof</strong>: In some use cases like some monitoring in an outdoor area, we might need waterproof/weatherproof. I myself don’t have many use cases though.</li></ul><h3>Options</h3><p>We can find several plastic project boxes which have holes and/or vents on the Internet. We can even see almost or exact the same boxes from different sellers. I’m not sure which one is the original. And I’m not sure what purposes these boxes were originally designed for.</p><p><strong>Option 1</strong>: This is my favorite. So far, I have found 3 sizes (small, medium and large) and 1 variant of a large one. I used them in <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">DIY environment monitor</a> and a variant in <a href="https://medium.com/@jj.underwood/diy-smart-button-43d4b461ee5d">DIY smart button</a>. We can find them from different sellers on AliExpress or other online shops (like <a href="https://www.aliexpress.com/item/3256803466337955.html">this</a>, <a href="https://www.aliexpress.com/item/3256805757021386.html">this</a>, <a href="https://www.aliexpress.com/item/3256805431668233.html">this</a> and <a href="https://www.aliexpress.com/item/3256801663357671.html">this</a>).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bl0O6mYHnfjPzt-nOf3m6g.jpeg" /><figcaption>Option 1 in 3 sizes and a variant</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1Xk2nDKzdA-L9jH4khv7tg.jpeg" /><figcaption>Option 1 (Small size with Lolin D1 mini and Lolin S3)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9c9BzsOGo-g4dSqXCaXk4A.jpeg" /><figcaption>Option 1 (Medium size with Lolin D1 mini and Lolin S3)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZUNApvNTQlteS2RBBok5sA.jpeg" /><figcaption>Option 1 (Large size with Lolin D1 mini and Lolin S3)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V6wjHWOU-U8NGh2aVVjyOw.jpeg" /><figcaption>Option 1 (A variant of large size with Lolin D1 mini and Lolin S3)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*qTt4j7l3NmJ_hBEnkkbGTQ.jpeg" /><figcaption>Option 1 (Small size with Lolin D1 mini and USB-C cable)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*u-ZkyyQkcg1x9F5N4QUWOg.jpeg" /><figcaption>Option 1 (Large size with Lolin S3 and USB-C cable)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8stjLASHKKyhm31ZHOB1KA.jpeg" /><figcaption>Option 1 (USB-C cable fitting in a cabling hole)</figcaption></figure><ul><li><strong>Pros</strong>: They are very affordable and each cost just around 1 USD at the time of this writing. We can buy them from different sellers on AliExpress. Because they are likely to be “Free shipping over $10” on AliExpress, it won’t be difficult to get free shipping if we buy a microcontroller and sensors together with the box. They all look very simple and subtle. And they have vents on top and bottom, a cabling hole on bottom and a mounting hole on the back. They have even screw posts on corners of the back panel, which we can use to place a PCB board if we want. We don’t have to make them with some tools by ourselves. If we need to use several boxes, it will be helpful. Although they are not waterproof/weatherproof, they are relatively sturdy.</li><li><strong>Cons</strong>: If we use a large sensor like Sensirion SEN5x, even a large box is a bit small in height as I described in <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">another post</a>. And although a USB-C cable can fit in a cabling hole, the hole is a bit small for a USB-C connector as I describe in <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">another post</a> as well. Which means that we need to place a microcontroller in an upper area so that an entire USB-C connector can fit within a box. Otherwise, we cannot close a cover.</li></ul><p><strong>Option 2</strong>: I found this on AliExpress (like <a href="https://www.aliexpress.com/item/3256805764980139.html">this</a>). There are 3 sizes, and I purchased a medium one and a large one. I used a large one in <a href="https://medium.com/@jj.underwood/sending-car-data-from-esp32-to-aws-iot-and-home-assistant-aabf542bf5f9">another project</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*C1-OV1ss0cP_N8eseyb6Iw.jpeg" /><figcaption>Option 2 (Medium size and large size)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*orpRxI1xUTcps_AW_OGBkA.jpeg" /><figcaption>Option 2 (Medium size with Lolin D1 mini and Lolin S3)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xTyYWvb-NAStK7zrbgv8iw.jpeg" /><figcaption>Option 2 (Large size with Lolin D1 mini and Lolin S3)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*564DrobmmSbOuBD483KlzQ.jpeg" /><figcaption>Option 2 (Medium size with Lolin D1 mini and USB-C cable)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cwvQ80a9YpKRVRIsObSXwg.jpeg" /><figcaption>Option 2 (USB-C cable cannot fit in a cabling hole)</figcaption></figure><ul><li><strong>Pros</strong>: They are very affordable and a set of several pieces cost just around 1 USD at the time of this writing. They are likely to be “Free shipping over $10” on AliExpress. Appearance seems to be a bit gadget-y compared to Option 1, but it is still simple (just my opinion). And they have vents on a cover, a cabling hole on bottom and a mounting hole on two corners of a cover.</li><li><strong>Cons</strong>: They are basically too small. It could work if we put only a microcontroller without any sensors and wires. We need to widen a cabling hole because it is too small even for a USB-C cable. And they are relatively soft and a bit fragile and even bent a bit from the first. Considering the price, it should not be a big problem though.</li></ul><p><strong>Option 3</strong>: I found this on AliExpress (like <a href="https://www.aliexpress.com/item/3256804108487342.html">this</a>). I have purchased it to see how it actually looks like. But I have not yet used it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VDTM66x5VDLQzTarfe3ZbA.jpeg" /><figcaption>Option 3</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*D6-IP9KeJA9F3KVCaNnwTQ.jpeg" /><figcaption>Option 3 (with Lolin D1 mini and Lolin S3)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3MgQmMm33OgB7YeWqfP00w.jpeg" /><figcaption>Option 3 (USB-C cable going through a hole)</figcaption></figure><ul><li><strong>Pros</strong>: It is a bit expensive compared to Option 1 and 2, but still affordable and under 5 USD at the time of this writing. It’s also likely to be “Free shipping over $10” on AliExpress. It looks simple and subtle. It has vents and 4 mounting holes around a box. It also has a rectangle hole for some reasons that I don’t know.</li><li><strong>Cons</strong>: The size is between a medium one and a large one of Option 1. Which means that it will be too small for a large sensor. And there is no cabling hole. A USB-C cable can run through the rectangle hole, but it will not be practical as you can see a picture above. We might need to make a cabling hole if we need.</li></ul><p><strong>Option 4</strong>: There are other simple plastic project boxes with vents and/or holes which are similar to Option 1–3 (like <a href="https://www.aliexpress.com/item/3256805318993659.html">this</a>, <a href="https://www.aliexpress.com/item/3256807129783465.html">this</a>, <a href="https://www.aliexpress.com/item/3256807515106411.html">this</a> and <a href="https://www.aliexpress.com/item/2251832721092840.html">this</a>). Although they themselves are under 5 USD, the total cost exceeds 5 USD including shipping fees because they are not “Free shipping over $10” on AliExpress at the time of this writing. So, I’m going to separate them from Option 1–3. I have purchased <a href="https://www.aliexpress.com/item/3256805318993659.html">one of them</a> to see how it looks like. But I have not yet used it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dIX9JzQW5hFdhxvumAdbXA.jpeg" /><figcaption>Option 4</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-MfzW0KFumW9IIYW-bVf1g.jpeg" /><figcaption>Option 4 (with Lolin D1 mini and Lolin S3)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V7Q6yKBGhRBCFN1Uwt2VTw.jpeg" /><figcaption>Mounting mechanism of Option 4</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SbAnOCODxV0A4T5gO_gG3A.jpeg" /><figcaption>Option 4 (with SEN5x holder and USB-C cable)</figcaption></figure><ul><li><strong>Pros</strong>: It is larger than a large one of Option 1. Although it’s a bit noticeable in its size, it still looks simple and relatively subtle. Because of its intended usage “Text Display”, which I’m not exactly sure, it could be possible to put something on the cover. It has vents on top and bottom, and mounting holes on top and bottom as well. But I’m still not sure how I use this mounting mechanism though.</li><li><strong>Cons</strong>: It is a bit expensive compared to Option 1 and 2. Although the box itself is still under 5 USD, the total cost including the shipping fee will be over 5 USD. Although it’s a large enclosure, it is a bit thinner. Which means that it is still a bit small for a large sensor. If we use a SEN5x official holder, we cannot close a cover. A thinner holder, which I created in <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">another post</a>, could fit. But I have not yet tried. And there is on cabling hole. A USB-C cable cannot go through a mounting hole. We need to make a cabling hole somewhere.</li></ul><p><strong>Option 5</strong>: I found this on AliExpress (like <a href="https://www.aliexpress.com/item/3256804616576892.html">this</a>). It looks a bit different from Option 1–4 because it is designed for a specific use case like an environment monitor or a smoke detector. There are many variations from the same seller (like <a href="https://www.aliexpress.com/item/3256804604798791.html">this</a>, <a href="https://www.aliexpress.com/item/3256804629889804.html">this</a> and <a href="https://www.aliexpress.com/item/3256804610591024.html">this</a>), and I purchased one of them to see how it actually looks like. I have not yet used it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*f9NEDJFZP394CQlMxztuiw.jpeg" /><figcaption>Option 5</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Q53qesDmNE9M4PyOxbRL_w.jpeg" /><figcaption>Option 5 (with Lolin D1 mini and Lolin S3)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aUme6Tx070oyKFmxloNsmw.jpeg" /><figcaption>Option 5 (with USB-C cable and Lolin D1 mini)</figcaption></figure><ul><li><strong>Pros</strong>: It is designed very well. It will look like a nice smoke detector or something. It has small holes on a cover which can be used as vents, a cabling hole on bottom and a nice mounting bracket (not just a hole). It also has a tiny hole on bottom, which we often see in some products for factory reset.</li><li><strong>Cons</strong>: Although it is under 5 USD, it is not “Free shipping over $10” on AliExpress and the shipping cost is more expensive than the price of the box at the time of this writing. We need shipping fee for each box. The total cost will exceed 5 USD. The size of the box is between a medium one and a large one of Option 1. But it is thinner. The biggest problem is the size of the cabling hole. Only a tip of USB-C connector can go through the hole. It is a bit short for plugging into a microcontroller through this hole. Probably it is designed to fit a custom PCB board.</li></ul><p><strong>Option 6</strong>: When we search “project box” on the Internet, we can find many general-purpose boxes in different sizes (like <a href="https://www.aliexpress.com/item/3256805294655445.html">this</a> or <a href="https://www.aliexpress.com/item/3256801704572496.html">this</a>). They are made with either metal or plastic. A plastic one will be easier to use if we think about making vents or holes. And a plastic one will be suitable if we want to use some sensors like mmWave presence sensor or need to have better Wi-fi or BLE signal.</p><ul><li><strong>Pros</strong>: They tend to be cheap. If you buy them from AliExpress, they cost only a couple of USD or even less than 1 USD. They are likely to be “Free shipping over $10” on AliExpress. From appearance perspective, they look simple and subtle because they are just bare boxes. Because there are a wide variety of sizes, it will be easy to find a size that fits our needs. They tend to have some level of durability as well.</li><li><strong>Cons</strong>: We need to create necessary vents or holes with some tools by ourselves. Although they tend to claim waterproof/weatherproof, it does not mean it can actually have waterproof/weatherproof after you create vents or holes.</li></ul><p><strong>Option 7</strong>: When we search “project box” on the Internet, we can find many junction boxes, electrical boxes, outlet boxes and switch boxes as well (like <a href="https://www.amazon.com/s?k=junction+box">this</a> and <a href="https://www.homedepot.com/b/Electrical-Electrical-Boxes-Conduit-Fittings-Boxes-Brackets/N-5yc1vZbohn">this</a>). They are intended for professional usage like housing electrical components or wires. But it could be possible to use them for DIY smart home projects if they fit our needs. Some of them seem to be for more like IoT projects (like <a href="https://www.aliexpress.com/item/3256805655155482.html">this</a>). They are made with either metal or plastic</p><ul><li><strong>Pros</strong>: The price varies, but there could be ones under 5 USD. They tend to have some holes and bushing or glands for cabling. If you are looking for waterproof/weatherproof, they will be helpful. They tend to be heavy-duty.</li><li><strong>Cons</strong>: Since it is intended for professional work, it definitely fits in a circuit box area, a garage, an outside area and even inside a wall in a house. But it does not necessarily look like something you place on a wall of living room. We can find some of them with vents, but they usually don’t have vents.</li></ul><p><strong>Option 8</strong>: I found a Stevenson screen type of enclosure on AliExpress (like <a href="https://www.aliexpress.com/item/3256806262434472.html">this</a>). It is not just a project box though. It seems that it is originally created for a weather station product but sold separately on its own as spare parts. I have not purchased it.</p><ul><li><strong>Pros</strong>: It will be good if you specifically need a Stevenson screen type of enclosure for your outdoor environment monitoring.</li><li><strong>Cons</strong>: It will be a bit too much for indoor usage.</li></ul><p><strong>Option 9</strong>: We could use a Raspberry Pi case as a project box. There are many choices from <a href="https://www.raspberrypi.com/products/raspberry-pi-5-case/">the official store</a> and other brands. They are made with either plastic or metal.</p><ul><li><strong>Pros</strong>: Although they are computer cases, some of them could look simple and subtle. They have holes for interface ports of Raspberry Pi which we can use as vents and a cabling hole.</li><li><strong>Cons</strong>: They are likely to be over 5 USD.</li></ul><p><strong>Option 10</strong>: I found <a href="https://www.aliexpress.com/item/3256805898101140.html">this</a> on AliExpress. It is a smoke detector “product”. I have purchased it to see if its enclosure can be re-purposed as a project box.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rXm8hJpwOICAkxw-DzNkfA.jpeg" /></figure><ul><li><strong>Pros</strong>: It is under 5 USD and even cheaper than Option 5. Because it is an actual product, it will have a better quality and fit in a wall. It has vents and nice mounting bracket.</li><li><strong>Cons</strong>: Taking guts out of it will need some work. And it does not have a cabling hole because it is intended to be powered by a battery. Although the size is between a medium one and a large one of Option 1, it is a bit small for DIY projects.</li></ul><h3>Final Thoughts</h3><p>At the time of this writing, I think Option 1 is still the best and easiest to use. But I wish I could have a bit larger one. Considering their prices, it is hard to ask for more though. Let me know if you find a better option.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2e80e82ebee6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Sending Car Data from ESP32 to AWS IoT and Home Assistant]]></title>
            <link>https://medium.com/@jj.underwood/sending-car-data-from-esp32-to-aws-iot-and-home-assistant-aabf542bf5f9?source=rss-53516c79bb78------2</link>
            <guid isPermaLink="false">https://medium.com/p/aabf542bf5f9</guid>
            <category><![CDATA[iot]]></category>
            <category><![CDATA[esphome]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[home-assistant]]></category>
            <category><![CDATA[esp32]]></category>
            <dc:creator><![CDATA[JJ Underwood]]></dc:creator>
            <pubDate>Wed, 18 Dec 2024 21:53:00 GMT</pubDate>
            <atom:updated>2024-12-18T22:25:32.006Z</atom:updated>
            <content:encoded><![CDATA[<p>In <a href="https://medium.com/@jj.underwood/collecting-data-from-car-with-esphome-and-home-assistant-4e36b48fb24e">the previous post</a>, I created a BLE Node and an external component of ESPHome to automatically collect data from a car with an affordable OBD2 adapter. I placed the BLE Node in my garage so that it can connect to the OBD2 adapter when my car is within BLE signal range. Although it was technically successful, I got mixed results. One reason was time limitation where the BLE Node finds and connects to the OBD2 adapter while the adapter is within BLE signal range, and the engine is running. So, I decided to try a different approach this time, which is placing the BLE Node inside the car and collecting data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5Ex0uZ0V1dcad7GjCTvzmA.jpeg" /></figure><h3>Architecture</h3><p>There are two options. The one is collecting data within the car and sending them directly to Home Assistant (“Direct” approach). Another one is collecting data within the car, sending them to Cloud and then collecting them from Cloud to Home Assistant (“Indirect” approach).</p><ul><li><strong>“Direct” approach</strong>: In this approach, we need to send data from a car to Home Assistant at home over either Wi-fi at home or cellular network. If we use Wi-fi at home, we can use either ESPHome native API with Home Assistant or MQTT. In either case, there is still time limitation problem where a car is within Wi-fi signal range. And BLE Node will face another problem outside Wi-fi signal range because ESPHome relies on Wi-fi connectivity. If we use cellular network, we can use MQTT. But we need to set up MQTT broker at home and open a port for MQTT broker on an internet router. And of course, we need to have cellular network capability somehow.</li><li><strong>“Indirect” approach</strong>: In this approach, we send data from a car to Cloud first and collect data from Cloud to Home Assistant at home. We can still use Wi-fi at home when a car is within Wi-fi signal range but will have time limitation problem. If we use cellular network, we can use MQTT broker on Cloud, publish data from a car and subscribe data from Home Assistant at home. Although we need to prepare cellular network capability somehow, we can collect data anytime at anywhere while a car is running.</li></ul><p>I decided to use “Indirect” approach this time and use cellular network. As I described in the previous post, there are already many commercial solutions (like <a href="https://www.bouncie.com/">this</a>, <a href="https://tracki.com/">this</a> and <a href="https://www.autopi.io/">this</a>). But I prefer to DIY. So, I’m going to create an affordable version — I place a BLE Node in a car, collecting data with an OBD2 adapter over BLE, publishing MQTT data to AWS IoT Core over cellular network, and subscribing MQTT data from Home Assistant at home. In fact, there is already <a href="https://aws.amazon.com/blogs/compute/building-an-aws-iot-core-device-using-aws-serverless-and-an-esp32/">a best practice published from AWS, which features Arduino</a> for connecting ESP32 to AWS IoT. In this project, I’m going to use ESPHome.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oT0Ldfc6tVdeZaw5HLOSEw.png" /><figcaption>Overview</figcaption></figure><h3>Material</h3><p><strong>BLE Node</strong>: Like the previous post, it’s just an ESP32 controller in an enclosure. I used a <a href="https://www.wemos.cc/en/latest/s3/s3_mini.html">Lolin S3 mini</a> in <a href="https://medium.com/@jj.underwood/collecting-data-from-car-with-esphome-and-home-assistant-4e36b48fb24e">the previous post</a>. Since I need to use not only BLE but MQTT this time, the size of the program becomes larger and S3 mini had limitation. So, I use a <a href="https://www.wemos.cc/en/latest/s3/s3.html">Lolin S3</a> this time. I bought it from <a href="https://www.aliexpress.com/item/1005004643475363.html">the official store on AliExpress</a>. Regarding an enclosure, I have been using the same simple and affordable case in my projects (like <a href="https://www.aliexpress.us/item/3256806039221720.html">this</a>, <a href="https://www.aliexpress.us/item/3256806253780960.html">this</a>, <a href="https://www.aliexpress.us/item/3256803466337955.html">this</a> and <a href="https://www.aliexpress.us/item/3256805757021386.html">this</a>). But this time, I tried <a href="https://www.aliexpress.com/item/3256805764980139.html">another simple and affordable case</a>, which I also found on AliExpress. I bought two sizes for just in case (70x35x17mm and 88x40x18mm). Each of them came with 5 pieces. I don’t need many though. One piece would cost under 0.5 USD. But it turned out that Lolin S3 would not fit in a smaller one. And even a larger one didn’t have enough space for a USB type-C connector of a cable. I had to cut a case to widen a hole. Since it was a cheap and even a bit soft plastic case, a box cutter was enough to widen a hole. I need to find a better case for future improvement. And as always, I attached Lolin S3 to the backplate of the case with hot glue.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*CA7nwTyXdzh5gnDvlcygVA.jpeg" /><figcaption>Cases in two sizes</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i-4Nc4vu25fN6la3_cX3lg.jpeg" /><figcaption>Original hole (left) and widen hole (right)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ip-FJIfxNUZnlt6l6ImIIQ.jpeg" /><figcaption>Lolin S3 in a larger case</figcaption></figure><p><strong>Cellular Network</strong>: I always bring my iPhone to use CarPlay when I use my car. It’s reasonable for me to rely on cellular connectivity of my iPhone rather than buying another mobile router or adding cellular capability to ESP32 somehow. So, I’m going to use Personal Hotspot of my iPhone.</p><p>The additional cost of this project including a Lolin S3 and a case was 8 USD at the time of this writing. Since I use my iPhone for cellular connectivity, I can take care of its cost in my existing data plan.</p><h3>Implementation</h3><p>I already created an external component of ESPHome called “obd2_ble” that collects data from a car with an OBD2 adapter over BLE in <a href="https://medium.com/@jj.underwood/collecting-data-from-car-with-esphome-and-home-assistant-4e36b48fb24e">the previous post</a>. I used ESPHome native API to publish data to Home Assistant. This time, I’m going to use MQTT to publish data to <a href="https://aws.amazon.com/iot-core/">AWS IoT Core</a>. Thankfully, ESPHome already has <a href="https://esphome.io/components/mqtt.html">MQTT Client Component</a>. Like I used <a href="https://esphome.io/components/ble_client.html">BLE Client</a> of ESPHome for connecting the BLE Node to an OBD2 adapter, I can use <a href="https://esphome.io/components/mqtt.html">MQTT Client Component</a> for connecting the BLE Node to a MQTT broker on AWS IoT Core. Regarding Home Assistant, it has <a href="https://www.home-assistant.io/integrations/mqtt/">MQTT integration</a> for connecting it to a MQTT broker as I explained in <a href="https://medium.com/@jj.underwood/sending-environment-data-from-home-assistant-to-aws-3d9a793974b4">another post</a>. Let’s go into each of them.</p><p><strong>AWS IoT Core</strong>: As I described in <a href="https://medium.com/@jj.underwood/sending-environment-data-from-home-assistant-to-aws-3d9a793974b4">another post</a> about how to send data from Home Assistant to AWS IoT Core, we can define a Thing for connecting a device like the BLE Node to a MQTT broker on AWS IoT Core and define a Device Shadow to maintain state of it. Although a Device Shadow is not mandatory if we need only MQTT capability, it might help us manage IoT data. When I connected Home Assistant to AWS IoT Core in another post, I defined Home Assistant as a Thing and created multiple Named Shadows for my <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">DIY Environment Monitors</a> to manage data, which means that Home Assistant was only one MQTT client device directly connecting to AWS IoT Core. In this case, one option would be just adding another Named Shadow for the BLE Node under the existing Thing. Another option would be defining a new Thing (and defining a new Device Shadow as well). Since AWS IoT Core allows us to separate credentials among Things, it would be ideal if we define different Things for different devices with different credentials from security perspective. So, I decided to define a new Thing that represents my car and define a Named Shadow for the BLE Node because Home Assistant and the BLE Node are two different clients from AWS IoT Core perspective. When we define a Thing, we can let AWS IoT Core autogenerate certificates as below. We need “Device certificate” (…-certificate.pem.crt), “Private key file” (…-private.pem.key) and one of “Root CA certificates” (AmazonRootCA1.pem) later. And we also need to make sure AWS IoT policy attached to the existing certificates for the existing Thing allows access to subscribing the new MQTT topics of the new Thing. If it’s not allowed, Home Assistant cannot subscribe the MQTT topics for the BLE Node later.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wVql5zpLkGTNTnmcK7S57w.png" /><figcaption>Thing and Named Shadow</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jjaplg21TZwDl6aRJyY0lg.png" /><figcaption>AWS IoT certificates</figcaption></figure><p><strong>BLE Node</strong>: Regarding connection to AWS IoT Core, we can just use <a href="https://esphome.io/components/mqtt.html">MQTT Client Component</a> and certificates autogenerated by AWS IoT Core. We already downloaded certificates from AWS IoT Core as above. All we need is just to copy and paste contents of those certificates on ESPHome yaml configuration — the contents of AmazonRootCA1.pem go to “certificate_authority”, the contents of Device certificate go to “client_certificate” and the contents of Private key file go to “client_certificate_key”. Just make sure you put indent as below. At the time of this writing, I also had to disable discovery features as follows. If I didn’t disable them, the BLE Node got disconnected from AWS IoT Core after connecting to it. As you can see below, we still see “api” and “ota” sections, and “wifi” section still expects Wi-fi information at home because we assume we use Wi-fi at home during try-and-error process. But once we make sure it will work, we remove “api” and “ota” sections and change information in “wifi” section from Wi-fi at home to iPhone Personal Hotspot.</p><pre>esphome:<br>  name: s3<br>  friendly_name: s3<br>  platformio_options:<br>    board_build.flash_mode: dio<br><br>esp32:<br>  board: lolin_s3<br>  variant: esp32s3<br>  framework:<br>    type: esp-idf<br><br>logger:<br>  level: DEBUG<br><br>api:<br>  encryption:<br>    key: !secret api_key<br><br>ota:<br>  - platform: esphome<br>    password: !secret ota_password<br><br>wifi:<br>  ssid: !secret wifi_ssid<br>  password: !secret wifi_password<br><br>mqtt:<br>  broker: xxxxxxxxxxxxxx-ats.iot.us-east-1.amazonaws.com<br>  port: 8883<br>  certificate_authority: |<br>    -----BEGIN CERTIFICATE-----<br>    ...<br>    ...<br>    -----END CERTIFICATE-----<br>  client_certificate: |<br>    -----BEGIN CERTIFICATE-----<br>    ...<br>    ...<br>    -----END CERTIFICATE-----<br>  client_certificate_key: |<br>    -----BEGIN CERTIFICATE-----<br>    ...<br>    ...<br>    -----END CERTIFICATE-----<br>  discover_ip: False<br>  discovery: False<br>  discovery_retain: False<br>  log_topic: null<br>  birth_message: <br>    topic:<br>    payload:<br>  will_message: <br>    topic:<br>    payload:</pre><p>When it comes to publishing data with MQTT from the BLE Node, there seem to be several options. Since I’m creating an external component, I’d like to use something already developed in <a href="https://github.com/esphome/esphome/tree/dev/esphome/components/mqtt">esphome/components/mqtt</a>. One option would be <a href="https://github.com/esphome/esphome/blob/dev/esphome/components/mqtt/mqtt_client.h">MQTTClientComponent</a>. But it looks like a foundation class of MQTT connection handling and too much for me trying to just publish data. Another option would be <a href="https://github.com/esphome/esphome/blob/dev/esphome/components/mqtt/mqtt_component.h">MQTTComponent</a>. But it also looks too much for me. I would still need to override many functions. One more another option would be <a href="https://github.com/esphome/esphome/blob/dev/esphome/components/mqtt/custom_mqtt_device.h">CustomMQTTDevice</a>, which allows me to use just a few helper functions including publishing data. I decided to use CustomMQTTDevice this time. As I described in the previous post, my OBD2BLEClient class inherits BLEClientNode class and Component class. I added CustomMQTTDevice class as well. I had to implement sending and receiving OBD2 commands and responses over BLE communication in the previous post. But this time, I just check MQTT connection and publish JSON data by using is_connected and publish_json method of CustomMQTTDevice because MQTT Client Component already takes care of MQTT connection.</p><p>My example code is <a href="https://github.com/jj-underwood/obd2_ble_mqtt">here</a>.</p><p><strong>iPhone</strong>: I don’t want to manually turn on Personal Hotspot on iPhone every time I use my car, and I don’t need to always turn on Personal Hotspot. So, I’m going to use Automation feature of iOS. I use CarPlay connection/disconnection as a trigger to automatically turn on and off Personal Hotspot. At first, I created shortcuts of turning on and off Personal Hotspot based on Set Personal Hotspot. And then, I created automations that trigger CarPlay connection/disconnection and run shortcuts. I’m still not sure but I might need to add disabling Auto-Lock as well.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MljixndoA216wrwYa3mt1Q.png" /><figcaption>Automation of Personal Hotspot</figcaption></figure><p><strong>Home Assistant</strong>: Like another post, I’m going to use <a href="https://www.home-assistant.io/integrations/mqtt/">MQTT integration</a>, not Mosquitto add-on. In my case, I have already set up MQTT integration to send environment data from <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">DIY Environment Monitor</a>. Basically, I don’t need to add anything regarding MQTT connection to AWS IoT. Because MQTT integration connects to just one MQTT broker, I cannot add another MQTT connection anyway. If we need more than two MQTT brokers, we need MQTT bridge like Mosquitto add-on. Although I can use the existing MQTT connection, I just disabled discovery feature of MQTT integration (See the update of <a href="https://medium.com/@jj.underwood/sending-environment-data-from-home-assistant-to-aws-3d9a793974b4">another post</a>) at the time of this writing. When it comes to using data receiving from MQTT topics, there are many options on Home Assistant, like MQTT triggered Automation, MQTT sensor, and so forth. I created MQTT sensors this time like below in configuration.yaml of Home Assistant.</p><pre>mqtt:<br>  sensor:<br>    - name: &quot;atrv&quot;<br>      state_topic: &quot;$aws/things/car-20241210/shadow/name/s3-01/update&quot;<br>      value_template: &quot;{{ value_json.state.reported.atrv | round(2) }}&quot;<br>      unit_of_measurement: &quot;V&quot;<br>    - name: &quot;engine_coolant_temperature&quot;<br>      state_topic: &quot;$aws/things/car-20241210/shadow/name/s3-01/update&quot;<br>      value_template: &quot;{{ value_json.state.reported.engine_coolant_temperature | round(2) }}&quot;<br>      unit_of_measurement: &quot;°C&quot;<br>    - name: &quot;stft&quot;<br>      state_topic: &quot;$aws/things/car-20241210/shadow/name/s3-01/update&quot;<br>      value_template: &quot;{{ value_json.state.reported.stft | round(2) }}&quot;<br>      unit_of_measurement: &quot;%&quot;<br>    - name: &quot;ltft&quot;<br>      state_topic: &quot;$aws/things/car-20241210/shadow/name/s3-01/update&quot;<br>      value_template: &quot;{{ value_json.state.reported.ltft | round(2) }}&quot;<br>      unit_of_measurement: &quot;%&quot;<br>    - name: &quot;fuel_tank_level_input&quot;<br>      state_topic: &quot;$aws/things/car-20241210/shadow/name/s3-01/update&quot;<br>      value_template: &quot;{{ value_json.state.reported.fuel_tank_level_input | round(2) }}&quot;<br>      unit_of_measurement: &quot;%&quot;<br>    - name: &quot;absolute_barometric_pressure&quot;<br>      state_topic: &quot;$aws/things/car-20241210/shadow/name/s3-01/update&quot;<br>      value_template: &quot;{{ value_json.state.reported.absolute_barometric_pressure | round(2) }}&quot;<br>      unit_of_measurement: &quot;kPa&quot;<br>    - name: &quot;control_module_voltage&quot;<br>      state_topic: &quot;$aws/things/car-20241210/shadow/name/s3-01/update&quot;<br>      value_template: &quot;{{ value_json.state.reported.control_module_voltage | round(2) }}&quot;<br>      unit_of_measurement: &quot;V&quot;<br>    - name: &quot;ambient_air_temperature&quot;<br>      state_topic: &quot;$aws/things/car-20241210/shadow/name/s3-01/update&quot;<br>      value_template: &quot;{{ value_json.state.reported.ambient_air_temperature | round(2) }}&quot;<br>      unit_of_measurement: &quot;°C&quot;<br>    - name: &quot;engine_oil_temperature&quot;<br>      state_topic: &quot;$aws/things/car-20241210/shadow/name/s3-01/update&quot;<br>      value_template: &quot;{{ value_json.state.reported.engine_oil_temperature | round(2) }}&quot;<br>      unit_of_measurement: &quot;°C&quot;</pre><h3>Test</h3><p>After many try-and-error at home, I’ve finally got the BLE Node working. I commented out “api” and “ota” sections and changed “wifi” section so that the BLE Node connects to iPhone.</p><pre>#api:<br>#  encryption:<br>#    key: !secret api_key<br><br>#ota:<br>#  - platform: esphome<br>#    password: !secret ota_password<br><br>wifi:<br>  ssid: !secret personal_hotspot_ssid<br>  password: !secret personal_hotspot_password</pre><p>I plugged the BLE Node to a USB port in my car. Once I start an engine, it’s powered up and starts searching Wi-fi. When I connect my iPhone to another USB port to use CarPlay, my iPhone automatically turn on Personal Hotspot and the BLE Node connected to Wi-fi and AWS IoT Core. The BLE Node also finds the OBD2 adapter in my car over BLE and starts collecting data from my car and publishing them to AWS IoT Core. While I’m driving my car, the BLE Node continued to collect and publish data. And when I stopped the engine, my iPhone turned off Personal Hotspot and the BLE Node powered down. It was successful.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/549/1*qkCapclGh7Yjn9z-QU7k6g.png" /><figcaption>Device Shadow</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/493/1*ZlUOT0N9tsDrs_kz4wcCtA.png" /><figcaption>Home Assistant dashboard</figcaption></figure><p>Although it was successful, I found some issues as well.</p><ul><li>When I tried to put all the supported PIDs as sensors, text sensors or binary sensors as I described in the previous post, MQTT connection could not be established. Probably because BLE and MQTT handling are memory intensive. When I reduced the number of those sensors, it worked as expected. This will be future improvement.</li><li>It seems that MQTT over cellular network with iPhone Personal Hotspot does not always work. Especially when I start my car in an area where cell phone reception is weak, MQTT communication ends up with no success even if cell phone reception becomes better in the middle of the drive. But when I start my car in strong cell phone reception area, MQTT communication becomes successful. Probably because iPhone Personal Hotspot stops working due to network problem even when display is ON.</li></ul><h3>Result</h3><p>Unlike the previous post, now I don’t need to worry about time limitation anymore. The BLE Node keeps collecting and publishing data while my car is running, and Home Assistant at home collects the latest data through Device Shadow. As I described above, it does not always work. But it is enough in my use case. It works fines at least once sometime during the day.</p><h3>Final Thoughts</h3><p>I successfully collected data from a car to the ESPHome based BLE Node with an OBD2 adapter, sent data from the BLE Node to AWS IoT Core over cellular network and collect data from AWS IoT Core to Home Assistant at home. Although it’s just a DIY project and does not have commercial grade reliability and convenience, I’m satisfied with the result.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=aabf542bf5f9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Collecting Data from Car with ESPHome and Home Assistant]]></title>
            <link>https://medium.com/@jj.underwood/collecting-data-from-car-with-esphome-and-home-assistant-4e36b48fb24e?source=rss-53516c79bb78------2</link>
            <guid isPermaLink="false">https://medium.com/p/4e36b48fb24e</guid>
            <category><![CDATA[home-assistant]]></category>
            <category><![CDATA[esphome]]></category>
            <category><![CDATA[obd2]]></category>
            <category><![CDATA[automotive]]></category>
            <category><![CDATA[iot]]></category>
            <dc:creator><![CDATA[JJ Underwood]]></dc:creator>
            <pubDate>Mon, 25 Nov 2024 19:53:21 GMT</pubDate>
            <atom:updated>2024-12-05T21:21:47.890Z</atom:updated>
            <content:encoded><![CDATA[<h3>Collecting Data from Car with ESPHome, Home Assistant and OBD2 Adapter</h3><p>My car has a connectivity feature called <a href="https://www.subaru.com/vehicle-info/subaru-starlink.html">STARLINK</a> (not <a href="https://www.starlink.com/us">the satellite internet service</a>). If I pay subscription fee for the correspondent service, I can check some sensor data (e.g., tire pressure, mileage, etc.), diagnostic data and even remotely start my car on the smartphone app. And there is the Home Assistant <a href="https://github.com/G-Two/homeassistant-subaru">custom component</a> to integrate the service with Home Assistant. I actually paid and tried the service for one year. But after one year subscription, I didn’t renew it because I couldn’t get the most from it for the price in my environment. Luckily, I didn’t receive any notification about a problem in my car. Unfortunately, at the same time, it couldn’t find a <a href="https://www.subarubatterysettlement.com/">battery problem</a>, which was the biggest problem for me. And I didn’t have a use case to remotely start my car. After I stopped the service, I had a tire pressure warning on the car dashboard. But it didn’t give me any motivation to pay for the service again because the car let me know a problem on its dashboard. Although the service was not necessary for its price in my case, it was enjoyable to see data collected from my car on Home Assistant. Since then, I have been wondering if I could do cheaper somehow.</p><p>There are some comprehensive end-to-end car data logger and GPS tracking services that come with a device to connect to an OBD2 (On-Board Diagnostics) port in a car and collect data to Cloud service via cellular network (like <a href="https://www.bouncie.com/">this</a> and <a href="https://tracki.com/">this</a>). But they need subscription fee as well. There is also <a href="https://community.home-assistant.io/t/wican-esp32-based-open-source-obd2-obd-ii-vehicle-car-or-truck-can-bus-diagnostics-to-wifi-ble-usb-adapter-for-home-assistant-integration/456098">an advanced OBD2 scanner that incorporates an ESP32 board</a> and publish data with MQTT so that we can directly collect data from a car to Home Assistant. Although it’s a bit expensive, I want to give it a try at some point in the future. Eventually, I ended up with a simple and affordable <a href="https://www.amazon.com/obd2-adapter/s?k=obd2+adapter">OBD2 adapter</a> to collect data from a car. We can use BLE or Wi-Fi to communicate with the adapter to collect data from a car. Although there is a Home Assistant <a href="https://www.home-assistant.io/integrations/torque/">integration</a>, we basically need an app to communicate with the adapter (like <a href="https://play.google.com/store/apps/details?id=org.prowl.torque&amp;hl=en_US">this</a>). When it comes to more DIY approach, there is already <a href="https://github.com/PowerBroker2/ELMduino">complete implementation,</a> but it is for Arduino. After many searches, I decided to use my favorite ESPHome to automatically and directly communicate with an OBD2 adapter and collect data from my car to Home Assistant.</p><p><strong>Disclaimer</strong>: Before I dive into the details, let’s set some ground rules:</p><ol><li>Educational Purpose Only: This blog post serves as an educational resource. I’m sharing my experiences, but I’m just a hobbyist, not a professional. Always consult experts for specific advice.</li><li>Assumption of Risk: By following my instructions, you assume full responsibility for any consequences. DIY projects involve risks, so proceed with caution.</li><li>Safety First: Handle electronic components carefully. Use appropriate tools and safety gear. Be aware of electrical hazards. Follow manufacturer guidelines.</li></ol><h3>Objective</h3><p>An OBD2 adapter can collect lots of detailed data from a car for a diagnostic purpose. It is a tool for a car enthusiast or an expert. It is intended to use only when we need to diagnose a car. It is not intended to keep connecting to a car all the time. Although it is powered up even while the car is OFF (engine is stopped) by using a car battery, it basically collects data while the car is ON (engine is running).</p><p>I’m neither a car enthusiast nor an expert. I won’t be able to understand most of data. Collecting some potential alerts or basic data to Home Assistant at my home once a day would be enough for me. And of course, I want to do that automatically. Since I usually start my car in the morning and go somewhere and come back to my home in the evening and stop my car, only chances to collect data from my car to Home Assistant at my home are when my car is ON and an OBD2 adapter in my car is in BLE or Wi-Fi signal range in the morning and the evening. So, I’m going to keep connecting the OBD2 adapter to my car and automatically collect data from my car at home when I start my car in the morning or when I come back to the home in the evening.</p><p><strong>Disclaimer</strong>: This is not the intended usage. As I describe later, there might be some security and safety concerns.</p><h3>Architecture</h3><p>Although I’m not an expert and don’t fully understand <a href="https://www.csselectronics.com/pages/obd2-explained-simple-intro">OBD2</a>, the communication with OBD2 adapter looks like <a href="https://learn.sparkfun.com/tutorials/obd-ii-uart-hookup-guide/obd-commands">simple command (AT command) and response</a> on serial terminal. It shows a prompt (“&gt;”) waiting for an input. If we type in a command, it shows a response and a prompt again. So, we could just automate typing in a command on a prompt and parsing an output. That is the basic idea.</p><p>There are already some discussions about getting data from a car through an OBD2 adapter over BLE (like <a href="https://stackoverflow.com/questions/52075456/which-gatt-profile-and-services-are-used-by-obd-ble-adapters-like-lelink-automa">this</a> and <a href="https://stackoverflow.com/questions/53653733/obd2-ble-communication-how-to-send-at-commands-and-receive-data">this</a>). I also found <a href="https://community.home-assistant.io/t/custom-component-nissan-leaf-via-lelink-2-elm327-ble/561961">a prior work</a> trying to use ble_client of ESPHome only with YAML configuration. It seems that all I need is sending AT commands and receiving responses over BLE.</p><p>Since I’m familiar with ESPHome, I decided to use ESPHome BLE feature as well. I’m going to create a “BLE Node”, which is just an ESP32 board based on ESPHome, place it in the garage in my house, let it connects to the OBD2 adapter on my car with BLE, send some commands to my car through the OBD2 adapter over BLE connection, and get some data from my car. Regarding software to actually communicate with the OBD2 adapter and my car, I’m going to create an external component of ESPHome.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*e7fOv9zaHK3HIvwsqLff2w.png" /><figcaption>Overview</figcaption></figure><h3>Materials</h3><ul><li><strong>OBD2 Adapter</strong>: There are many OBD2 adapters with a wide variety of prices. Although there might be a <a href="https://www.carscanner.info/choosing-obdii-adapter/">quality issue</a>, I decided to buy an affordable one on Amazon. I picked up <a href="https://www.amazon.com/dp/B073XKQQQW">the one with BLE4.0</a>.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dLgajKjd78KB5tHQ6dmoTQ.jpeg" /></figure><ul><li><strong>ESP32 Board</strong>: Because I liked Lolin D1 mini (ESP8266) in my previous projects like <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">DIY Environment Monitor</a> and <a href="https://medium.com/@jj.underwood/diy-smart-button-43d4b461ee5d">DIY Smart Button</a>, I decided to use a ESP32 board from the same brand. I picked up a small one — <a href="https://www.wemos.cc/en/latest/s3/s3_mini.html">Lolin S3 mini</a>. I bought it from <a href="https://www.aliexpress.com/item/3256805262904443.html">the official store</a>.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5T7pYYEp207fRsZokx4RdA.jpeg" /><figcaption>Lolin S3 mini</figcaption></figure><ul><li><strong>Case</strong>: Like my <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">DIY Environment Monitor</a> and <a href="https://medium.com/@jj.underwood/diy-smart-button-43d4b461ee5d">DIY Smart Button</a>, I want to put an ESP32 board in a simple and affordable enclosure. I decided to use the small case that I used in my <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">DIY Environment Monitor</a> (like <a href="https://www.aliexpress.us/item/3256806039221720.html">this</a>, <a href="https://www.aliexpress.us/item/3256806253780960.html">this</a>, <a href="https://www.aliexpress.us/item/3256803466337955.html">this</a> and <a href="https://www.aliexpress.us/item/3256805757021386.html">this</a>).</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DF54HUA90I__eTxgULFhvw.jpeg" /><figcaption>Case</figcaption></figure><p>At the time of this writing, the total cost was $33.37 for the OBD2 adapter, Lolin S3 mini and the case without tax and shipping fee.</p><h3>Hardware Implementation</h3><ul><li><strong>OBD2 Adapter</strong>: There is nothing to do. I just plug the adapter into the OBD2 port in my car.</li><li><strong>BLE Node</strong>: This time, there is nothing difficult to do. I just attached the Lolin S3 mini onto the back panel of the case with hot glue.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aFDZy97dzfRBYgRQ61nSrg.jpeg" /><figcaption>Building BLE Node</figcaption></figure><h3>Software Implementation and Configuration</h3><p>If I just receive broadcasted data, esp32_ble_tracker would be enough. But this time, I need to let the BLE Node “connect” to the OBD2 adapter with BLE and send and receive data. So, I use ble_client. Connecting to the OBD2 adapter with ble_client is very easy. I just need MAC address of the OBD2 adapter. Although there is no MAC address information on the adapter, I could find it out by specifying esp32_ble_tracker in the YAML configuration, installing the firmware, and waiting for a while until the adapter showed up on logs.</p><pre>logger:<br>  level: VERBOSE<br><br>esp32_ble_tracker:</pre><pre>[11:29:57][D][esp32_ble_client:110]: [0] [8C:DE:52:DD:FC:19] Found device<br>[11:29:57][D][esp32_ble_tracker:671]: Found device XX:XX:XX:XX:XX:XX RSSI=-81<br>[11:29:57][D][esp32_ble_tracker:692]:   Address Type: PUBLIC<br>[11:29:57][D][esp32_ble_tracker:694]:   Name: &#39;VEEPEAK&#39;<br>[11:29:57][D][esp32_ble_tracker:697]:   TX Power: 3</pre><p>Once I get MAC address, I can specify it on ble_client configuration.</p><pre>logger:<br>  level: VERBOSE<br><br>esp32_ble_tracker:<br><br>ble_client:<br>  - mac_address: XX:XX:XX:XX:XX:XX<br>    id: veepeak</pre><p>To communicate with the BLE based adapter, we need Service UUID and Characteristic UUID. We can find them out by using some BLE scanner apps or searching on the Internet. It seems that <a href="https://stackoverflow.com/questions/52075456/which-gatt-profile-and-services-are-used-by-obd-ble-adapters-like-lelink-automa">typical OBD2 adapters have just one Service UUID and two Characteristic UUIDs</a> (one for “write” and one for “notify”). We can use “write” characteristic to send AT commands, and “notify” characteristic to get a notification for receiving data. I also found <a href="https://github.com/particle-iot/ble-gateway-library/blob/main/examples/veepeak/README.md">a good insight</a> about UUIDs of almost exact the same OBD2 adapter that I bought. My OBD2 adapter, Veepeak OBDCheck BLE, uses “0xFFF0” for Service UUID, “0xFFF2” for “write” Characteristic UUID, and “0xFFF1” for “notify” Characteristic UUID.</p><p>Regarding <a href="https://en.wikipedia.org/wiki/OBD-II_PIDs">OBD2</a>, we need to send a series of AT commands for initialization like “ATZ”, “ATE0”, and so forth to get expected behavior. After initialization, we can send an actual command with at least a Mode and a PID (Parameter ID) like “010C”, which means Mode 01 and PID 0C. In either initialization commands or other actual commands, what we do is the same — automating command sending and response parsing like the following example. We can parse a response based on “\r” (0x0D) and a prompt “&gt;” (0x3E). Responses are all <a href="https://simple.wikipedia.org/wiki/ASCII">hex ASCII characters</a>. For example, a response in the following example like “ELM327 v1.5\r\r&gt;” actually comes in a list of bytes “0x45 0x4C 0x4D 0x33 0x32 0x37 0x20 0x76 0x31 0x2E 0x35 0x0D 0x0D 0x3E”. We can just convert them to ASCII characters to understand them. And we need to be aware that OBD2 responses also include hex ASCII characters like the following example — “410C1CB2” in “7E804410C1CB2”. So, we need to handle a pair of ASCII characters as a hex like “0x41 0x0C 0x1C 0xB2”.</p><p>And we might get multiple responses for one command. In the following example, two responses are returned for one command. One of them starts with “7E8” while another one starts with “7E9”. They are CAN (Controller Area Network) IDs of ECUs (Electronic Control Unit). Because there are two ECUs in a car in the following example, which is actually my car by the way, both of them responds to one command. Even one response from one ECU could become multiple lines if the response is long. We need to concatenate them to get a full response.</p><pre>ATZ                &lt;--- Send a first initialization command with &quot;\r&quot;.<br>ATZ                &lt;--- Echo with &quot;\r&quot;<br>                   &lt;--- &quot;\r&quot;<br>ELM327 v1.5        &lt;--- Something like welcome message.<br>                   &lt;--- &quot;\r&quot;<br>&gt;ATE0              &lt;--- Prompt and then send another command with &quot;\r&quot;.<br>ATE0               &lt;--- Echo with &quot;\r&quot;<br>OK                 &lt;--- OK message<br>                   &lt;--- &quot;\r&quot;<br>&gt;ATL0              &lt;--- Prompt and then send another command with &quot;\r&quot;.<br>OK                 &lt;--- OK message<br>                   &lt;--- &quot;\r&quot;<br>&gt;ATS0              &lt;--- Prompt and then send another command with &quot;\r&quot;.<br>OK                 &lt;--- OK message<br>                   &lt;--- &quot;\r&quot;<br>&gt;ATH1              &lt;--- Prompt and then send another command with &quot;\r&quot;.<br>OK                 &lt;--- OK message<br>                   &lt;--- &quot;\r&quot;<br>&gt;ATSP0             &lt;--- Prompt and then send another command with &quot;\r&quot;.<br>OK                 &lt;--- OK message<br>                   &lt;--- &quot;\r&quot;<br>&gt;ATDPN             &lt;--- Prompt and then send another command with &quot;\r&quot;.<br>A0                 &lt;--- OK message<br>                   &lt;--- &quot;\r&quot;<br>&gt;010C              &lt;--- Prompt and then send Mode and PID with &quot;\r&quot;.<br>SEARCHING...       &lt;--- SEARCHING message<br>7E804410C1CB2      &lt;--- A response from a ECU<br>7E904410C1CB8      &lt;--- A response from another ECU<br>                   &lt;--- &quot;\r&quot;<br>&gt;                  &lt;--- Prompt</pre><p><a href="https://en.wikipedia.org/wiki/OBD-II_PIDs">There are 10 Modes and lots of PIDs in OBD2 standards</a>, depending on what kind of data we want. Since each car does not fully support them, there are even PIDs to check what PIDs are supported by a car. Based on my objective, I would want to use Mode 03, 07 or 0A for DTC (Diagnostic Trouble Codes) data to see if there is anything wrong in my car. I found <a href="https://www.autonationsubaruwest.com/service/obd-ii-trouble-codes.htm">a reference</a> of DTC codes for my car manufacturer. Because DTC codes are returned from my car if there are problems, Binary Sensor type will be suitable for DTC codes. I may want to use Mode 01 for some sensor data and Mode 05 and 06 for some test results if necessary. Because they are basically numeric values, Sensor type will be suitable for most of them. I may want to use Mode 09 for vehicle information as well. Text Sensor type will be OK for them. Because there are a wide variety of commands, Modes and PIDs, and I’m still not sure what are supported, I want to make them configurable on YAML like this.</p><pre>esphome:<br>  name: s3-mini<br>  friendly_name: s3-mini<br>  platformio_options:<br>    platform: https://github.com/platformio/platform-espressif32.git<br>    board: lolin_s3_mini<br>    framework: arduino<br>    board_build.flash_mode: dio<br><br>esp32:<br>  variant: esp32s3<br>  board: lolin_s3_mini<br>  framework: <br>    type: arduino<br>    version: latest<br><br>logger:<br>  level: DEBUG<br><br>api:<br>  encryption:<br>    key: !secret api_key<br><br>ota:<br>  - platform: esphome<br>    password: !secret ota_password<br><br>wifi:<br>  ssid: !secret wifi_ssid<br>  password: !secret wifi_password<br><br>  ap:<br>    ssid: &quot;Esp32-01 Fallback Hotspot&quot;<br>    password: !secret ap_password<br><br>captive_portal:<br><br>esp32_ble_tracker:<br><br>ble_client:<br>  - mac_address: XX:XX:XX:XX:XX:XX # MAC address of OBD2 adapter<br>    id: veepeak<br><br>external_components:<br>  - source: my_components<br><br>obd2_ble:<br>  id: mycar<br>  service_uuid: &quot;0xFFF0&quot;<br>  characteristic_uuid:<br>    write: &quot;0xFFF2&quot;<br>    notify: &quot;0xFFF1&quot;<br>  init_commands: &quot;ATZ ATE0 ATL0 ATH0 ATS0 ATSP0&quot; # Optional<br>  command_delay: 1000 # Optional<br>  command_wait: 5000 #Optional<br><br>sensor:<br>  - platform: obd2_ble<br>    obd2_ble_id: mycar<br>    name: &quot;Engine RPM&quot;<br>    can_id: &quot;7E8&quot;<br>    mode: &quot;01&quot;<br>    pid: &quot;0C&quot;<br>  - platform: obd2_ble<br>    obd2_ble_id: mycar<br>    name: &quot;Vehicle Speed&quot;<br>    can_id: &quot;7E8&quot;<br>    mode: &quot;01&quot;<br>    pid: &quot;0D&quot;<br><br>text_sensor:<br>  - platform: obd2_ble<br>    obd2_ble_id: mycar<br>    name: &quot;Diagnostic Trouble Codes&quot;<br>    mode: &quot;03&quot;<br>  - platform: obd2_ble<br>    obd2_ble_id: mycar<br>    name: &quot;Vehicle VIN&quot;<br>    can_id: &quot;7E8&quot;<br>    mode: &quot;09&quot;<br>    pid: &quot;02&quot;<br><br>binary_sensor:<br>  - platform: obd2_ble<br>    obd2_ble_id: mycar<br>    name: &quot;Stored PCM failure&quot;<br>    mode: &quot;03&quot;<br>    code: &quot;P0010&quot;<br>  - platform: obd2_ble<br>    obd2_ble_id: mycar<br>    name: &quot;Stored Camshaft variable timing solenoid failure&quot;<br>    mode: &quot;03&quot;<br>    code: &quot;P0011&quot;</pre><p>Now, I’m going to implement my external component called “obd2_ble” to achieve these communications and configurations. There is <a href="https://esphome.io/components/external_components.html">an official document</a> explaining how to implement an external component. And source codes of <a href="https://github.com/esphome/esphome/tree/dev/esphome/components">existing components</a>, especially which use ble_client, have a lot to learn. I create OBD2BLEClient class that inherits BLEClientNode class and Component class. BLEClientNode class corresponds to ble_client and takes care of BLE communication. By inheriting it, my OBD2BLEClient can do some actions when some BLE related events occur. And I decided to use Component class this time instead of PollingComponent class because BLE Node needs to connect to the OBD2 adapter as soon as it finds the adapter, and don’t have to repeat anything in a fixed interval. After many try and error and learning, I created 5 files as below and placed them in the directory structure like this.</p><pre>homeassistant<br>└── esphome<br>    └── my_components<br>        └── obd2_ble<br>            ├── __init__.py<br>            ├── binary_sensor.py<br>            ├── obd2_ble.cpp<br>            ├── obd2_ble.h<br>            ├── sensor.py<br>            └── text_sensor.py</pre><p>My example code is <a href="https://github.com/jj-underwood/obd2_ble">here</a>. I’m not a professional software engineer though. There will be better, more reliable and sophisticated way to implement the same thing. Although I tried to make my implementation generic and configurable, it might not just work in other environments because different OBD2 adapter has different behavior, and different car has different behavior.</p><ul><li><strong>__init__.py</strong>: This registers obd2_ble component. It takes UUIDs, a series of initial AT commands and other parameters from YAML and pass them to OBD2BLEClient class.</li><li><strong>sensor.py</strong>: This registers sensors of obd2_ble component. It takes a list of sensors specified in YAML and pass them to OBD2BLEClient class.</li><li><strong>text_sensor.py</strong>: This registers text sensors of obd2_ble component. It takes a list of text sensors specified in YAML and pass them to OBD2BLEClient class.</li><li><strong>binary_sensor.py</strong>: This registers binary sensors of obd2_ble component. It takes a list of binary sensors specified in YAML and pass them to OBD2BLEClient class.</li><li><strong>obd2_ble.h</strong> and <strong>obd2_ble.cpp</strong>: These implements OBD2BLEClient class. It inherits ble_client::BLEClientNode and Component classes. It uses UUIDs, a series of initial AT commands, Modes and PIDs specified in the YAML configuration, uses ble_client capabilities to handle BLE connection, sends commands, receives and parses data, and publish them to Home Assistant. I tried to separate BLE handling and OBD2 handling in my code. BLE handling is event driven, and gattc_event_handler() inherited from BLEClientNode takes care of events. OBD2 handling is a sequential command and response processing, and loop() inherited from Component takes care of those processing.</li></ul><h3>Installation and Test</h3><p>I placed the BLE Node in my garage. It is right in front of my car when the car is parked in the garage. And I plugged the OBD2 adapter to the OBD2 port of my car. In my case, the OBD2 port is located under the dashboard, beneath the steering wheel. It was not covered. I could easily plug the OBD2 adapter vertically.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ST11vJl_4bBdHXkt11glvg.jpeg" /><figcaption>BLE Node (left) placed in my garage along with DIY Environment Monitor (right)</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4qe_EaOJKQ9thdWYd3O8XA.jpeg" /><figcaption>OBD2 adapter plugged in my car.</figcaption></figure><p>Once I started the engine and the BLE Node found the OBD2 adapter, it connected to the adapter, sent commands, received responses and published data to Home Assistant. So, it was technically successful. It took just less than a minute to complete a series of commands.</p><p>I tried several commands to see what kind of data I could get from my car. In my case, my findings were as follows. I tried only standard OBD2 commands. There might be more than that, like proprietary ones. But I don’t have any information about them.</p><ul><li>My car supports 11-bit CAN ID. It might not support 29-bit CAN ID because it returned “NO DATA” for any command after initialization with ATSP7 command.</li><li>My car has two ECUs, one with CAN ID 7E8 (engine) and another one with CAN ID 7E9 (transmission).</li><li>Both ECUs support several PIDs in Mode 01 and 09. The total number of supported PIDs in Mode 01 and 09 was 56.</li><li>Both ECUs might not support any PID for Mode 05 because they returned “NO DATA” for inquiries to check supported PIDs. One of them supports some PIDs in Mode 06.</li><li>Because my car currently has no problem, I got empty responses in Mode 03 (Stored DTC), Mode 07 (Pending DTC) and Mode 0A (Permanent DTC). So, I still have not had any data for testing my obd2_ble component. Based on a reference of DTC codes from the manufacturer of my car, the number of DTC codes is 129. I’m not sure how many of them could be relevant to my car though.</li><li>Because there are many supported PIDs and potentially related DTC codes, the number of sensors, text sensors and binary sensors becomes a lot if I define all of them in YAML. When I actually put all of them in YAML, some problems like memory error or data communication error became more likely to occur (*1). Although it was not a good idea to define all the sensors, text sensors and binary sensors for all the supported PIDs and DTC codes, and I still don’t know how to decode all of them, I got data like below. I was overwhelmed by lots of data. I need to narrow them down to get only necessary data. I might need to avoid defining each binary sensor for each DTC code as well. I also need to improve reliability of my code.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ECYINoz3-z8u2s7zfw17xA.png" /><figcaption>Lots of data</figcaption></figure><ul><li>I have been leaving the OBD2 adapter plugged in all the time. Because I’m using Component class and loop() function, the BLE Node tries to connect to the OBD2 adapter as soon as it finds the adapter, which is expected and good for my use case. But I realized that the BLE Node connects to the OBD2 adapter even when the engine is OFF because the adapter is powered by the car battery. After it connects to the adapter, it can even send a series of initialization commands and receive responses. But when it tries to send an actual command with a Mode and a PID like “010C”, it gets a time out error and cannot receive any response. So, it repeats connecting to the adapter and sending initialization commands while my car is in my garage and the engine is OFF (e.g., during the night), which is just wasting electricity of my home and car battery. So far, I have not experienced a dead battery because of this. But it is not nice. To solve the issue, I initially thought changing from Component/loop() to PollingComponent/update() to reduce frequency of the connection between the BLE Node and the OBD2 adapter. But in my use case, the BLE Node needs to collect data from my car as soon as it finds the adapter in BLE signal range. If it tries to connection in a fixed interval, it might decrease a chance to find the adapter while the engine is ON. I also thought adding some timer capabilities while using Component/loop(). But it would be complicated. I eventually decided to use an affordable and simple smart plug and plug the BLE Node to the smart plug. By using the smart plug, I can just turn on the BLE Node only in a couple of hours in the morning and the evening (*2). I might want to have a better approach like combining presence detection for future improvements.</li></ul><h3>Results</h3><p>I’ve been using the BLE Node and the OBD2 adapter for a while. Although the BLE Node is working and it was technically successful, the result has been mixed for me so far.</p><ul><li>In my use case, I wanted to check if there is anything wrong in my car every day. But so far, I have not had any data, fortunately.</li><li>I also wanted to see basic data like tire pressure, mpg and so forth. But it turned out that there is no standard PIDs for them. I should have checked all the PIDs in advance. (*3)</li><li>Although I can see lots of Mode 01 or Mode 09 data, they are just too much for me at this moment. I might need to have knowledge to extract some insights from these data.</li><li>BLE device detection has not been reliable so far. In my use case, I want the BLE Node to find the OBD2 adapter as soon as I start the engine in the morning, or I come back to the garage in the evening. The time the OBD2 adapter is in BLE signal range, and the engine is running, is just a few minutes of time window. But I realized that it would sometimes take longer than I expected for the BLE Node to find the OBD2 adapter. Once it finds and connects to the adapter, sending commands and receiving responses take less than a minute though.</li></ul><h3>Security and Safety Consideration</h3><p>As I mentioned above, I’m leaving the OBD2 adapter plugged in all the time. Although the adapter manufacturer says it would be OK from battery consumption perspective, there might be some security and safety concerns. Since anyone within BLE signal range can connect to the adapter anytime, they can send some commands and get data from my car, which is not nice. And there might be some electronic interference as well. This post is just my personal experimental project. I won’t recommend anyone to do the same thing.</p><p>If there is any way or any OBD2 adapter that has a capability to restrict connection with configurable password or source address, that would be nice.</p><h3>Final Thoughts</h3><p>From a technical perspective, I successfully created my external component for ESPHome to communicate with an affordable BLE OBD2 adapter. It took me many try and error. Since I could test my implementation only in the limited time of the morning and the evening, it took me a longer period than I expected. Although there are still many to improve and consider, and I have not fully completed my use case, I’m satisfied with the result because I finally feel my car has become a part of my smart home.</p><p>Update (*1) [11/26/2024]: The problem was solved when I add a delay in publishing sensor states. It resulted in a longer time to complete collecting data though.</p><p>Update (*2) [11/26/2024]: I realized that all the sensor values would become “Unavailable” if I just turn off the BLE Node during the night. So, I added a “wait” time before the BLE Node disconnects from the OBD2 adapter when it cannot receive responses because the engine is OFF after all.</p><p>Update (*3) [12/02/2024]: I happened to know that there would be additional modes like <a href="http://twisting-roads.com/2017/11/custom-obd-ii-modes-in-a-terminal/">Mode 22</a> that contains a manufacturer or a vehicle specific data. Although the fuel type is different, it seems that the manufacturer of my car could <a href="https://github.com/giotec/ParsePID">support Mode 22</a>. In fact, I got some responses for some PIDs in Mode 22 and added them to my sample code. But I still don’t know how to decode them… I also learned that some responses could be used as a sort of <a href="https://stackoverflow.com/questions/54568402/how-to-get-the-battery-voltage-of-a-car-using-obd-2">battery voltage</a> and added them to my sample code as well.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ibUiyIWfj9DYlcJF2_Euiw.png" /><figcaption>Even more data which I don’t even know how to decode.</figcaption></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4e36b48fb24e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Receiving Statistics Report of DIY Smart Button by Email]]></title>
            <link>https://medium.com/@jj.underwood/receiving-statistics-report-of-diy-smart-button-by-email-2024b8b15458?source=rss-53516c79bb78------2</link>
            <guid isPermaLink="false">https://medium.com/p/2024b8b15458</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[esphome]]></category>
            <category><![CDATA[smart-home]]></category>
            <category><![CDATA[pandas]]></category>
            <category><![CDATA[home-assistant]]></category>
            <dc:creator><![CDATA[JJ Underwood]]></dc:creator>
            <pubDate>Tue, 29 Oct 2024 23:54:48 GMT</pubDate>
            <atom:updated>2024-10-29T23:54:48.265Z</atom:updated>
            <content:encoded><![CDATA[<p>I recently created <a href="https://medium.com/@jj.underwood/diy-smart-button-43d4b461ee5d">DIY smart button</a> device to count events related to housework and childcare in my house. It has 5 buttons on it, and it is based on affordable arcade style buttons, ESPHome, Home Assistant and AWS. My wife and I defined a role of each button. Every time my wife or I click a button, the click triggers Home Assistant automation and publishes a MQTT message to AWS IoT Core, and the click event is recorded in Amazon Timestream. Now, we want to keep aware of how many times those events have happened recently. I initially thought of creating a dashboard to show charts of click counts as I did for environment data. But I realized that more traditional approach like email report would work better for us in this case. Because we check email every day, email report would fit in our daily life. And we sometimes unexpectedly need to know something like “When was the last time of this?”, “How often do we have that recently?” and so forth, even when we are at somewhere outside of the house. We don’t want to open up the dashboard and go through authentication process. If we receive email reports regularly, we can just take a look at the latest report on email.</p><h3>Objective</h3><p>I’m going to create a simple serverless application that retrieve data from database, calculate statistics of yesterday, this week and last week, and this month and last month, and send a report to us by email every morning.</p><h3>Architecture</h3><p>This time, the architecture is very simple. As I already explained in <a href="https://medium.com/@jj.underwood/diy-smart-button-43d4b461ee5d">the previous post</a>, Home Assistant publishes MQTT messages to AWS IoT Core. When I created my DIY smart button, I initially used Amazon Timestream to record click events. But based on <a href="https://medium.com/@jj.underwood/using-dynamodb-and-timestream-together-for-environment-data-281a45b25959">the recent try and error</a>, I decided to switch from Timestream to DynamoDB to record click events. Since I don’t plan to store click events for a long term, DynamoDB would be enough and affordable. I define AWS IoT Core rule to route click event data to DynamoDB. And I create a lambda function to retrieve data from DynamoDB, calculate statistics and send email with Amazon SES. I create an Amazon EventBridge Schedule event (CloudWatch Event) to run the lambda function as a sort of cron job.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-z1_3oOZBMuQi2GPaTscmA.png" /><figcaption>Overview</figcaption></figure><h3>Implementation</h3><p>My example code is <a href="https://github.com/jj-underwood/diy_smart_button.git">here</a>.</p><p><strong>ESPHome and Home Assistant</strong>: There is no change from my previous post. I defined GPIO Binary Sensor and Template Binary Sensor on ESPHome, and created an automation on Home Assistant to publish click events to a MQTT topic of a Named Shadow on AWS IoT Core.</p><p><strong>AWS IoT Core</strong>: Because I defined a Named Shadow for my DIY smart button device, button click events are stored in the JSON document as click state like below. Each click state will be “on”, “off” or “unavailable” and they are all varchar data type. Every time one of button is clicked, a correspondent state change (“off” to “on” and “on” to “off”) is sent from Home Assistant to AWS IoT Core by MQTT and JSON document of the Named Shadow is updated.</p><pre># JSON document of button click<br>{<br>  &quot;state&quot;: {<br>    &quot;reported&quot;: {<br>      &quot;momentary_04_single_short_click&quot;: &quot;off&quot;,<br>      &quot;momentary_01_single_short_click&quot;: &quot;off&quot;,<br>      &quot;momentary_01_double_click&quot;: &quot;off&quot;,<br>      &quot;momentary_02_single_short_click&quot;: &quot;off&quot;,<br>      &quot;momentary_03_single_short_click&quot;: &quot;off&quot;,<br>      &quot;momentary_05_single_short_click&quot;: &quot;off&quot;,<br>      &quot;momentary_04_single_long_click&quot;: &quot;off&quot;,<br>      &quot;momentary_01_single_long_click&quot;: &quot;off&quot;,<br>      &quot;momentary_03_double_click&quot;: &quot;off&quot;,<br>      &quot;momentary_04_double_click&quot;: &quot;unavailable&quot;,<br>      &quot;momentary_03_single_long_click&quot;: &quot;unavailable&quot;,<br>      &quot;momentary_05_double_click&quot;: &quot;unavailable&quot;,<br>      &quot;momentary_05_single_long_click&quot;: &quot;unavailable&quot;,<br>      &quot;momentary_02_single_long_click&quot;: &quot;unavailable&quot;<br>    }<br>  }<br>}</pre><p>I defined the following AWS IoT Core Rule to subscribe the MQTT topic and route data to DynamoDB. As I did in <a href="https://medium.com/@jj.underwood/using-dynamodb-and-timestream-together-for-environment-data-281a45b25959">another post</a>, I added TTL so that old data will be automatically deleted from DynamoDB. I used SQL version 2016–03–23. I don’t have to worry about data type casting error because all the data are varchar this time.</p><pre># AWS IoT Core Rule<br>SELECT<br>  CAST(state.reported.momentary_01_single_short_click AS varchar) as momentary_01_single_short_click,<br>  CAST(state.reported.momentary_01_single_long_click AS varchar) as momentary_01_single_long_click,<br>  CAST(state.reported.momentary_01_double_click AS varchar) as momentary_01_double_click,<br>  CAST(state.reported.momentary_02_single_short_click AS varchar) as momentary_02_single_short_click,<br>  CAST(state.reported.momentary_02_single_long_click AS varchar) as momentary_02_single_long_click,<br>  CAST(state.reported.momentary_02_double_click AS varchar) as momentary_02_double_click,<br>  CAST(state.reported.momentary_03_single_short_click AS varchar) as momentary_03_single_short_click,<br>  CAST(state.reported.momentary_03_single_long_click AS varchar) as momentary_03_single_long_click,<br>  CAST(state.reported.momentary_03_double_click AS varchar) as momentary_03_double_click,<br>  CAST(state.reported.momentary_04_single_short_click AS varchar) as momentary_04_single_short_click,<br>  CAST(state.reported.momentary_04_single_long_click AS varchar) as momentary_04_single_long_click,<br>  CAST(state.reported.momentary_04_double_click AS varchar) as momentary_04_double_click,<br>  CAST(state.reported.momentary_05_single_short_click AS varchar) as momentary_05_single_short_click,<br>  CAST(state.reported.momentary_05_single_long_click AS varchar) as momentary_05_single_long_click,<br>  CAST(state.reported.momentary_05_double_click AS varchar) as momentary_05_double_click,<br>  (timestamp() / 1000) as ts,<br>  ((timestamp() / 1000) + (365 * 24 * 60 * 60)) as ttl<br>FROM &#39;$aws/things/home-assistant/shadow/name/d1-mini-10/update&#39;</pre><p>Because I want to retrieve data in a specific period of time, I decided to use the same approach with time series environment data in <a href="https://medium.com/@jj.underwood/using-dynamodb-and-timestream-together-for-environment-data-281a45b25959">another post</a> and defined the Partition Key and the Sort Key format like below.</p><pre># DynamoDB Partition Ley<br>${parse_time(&quot;yyyy-MM-dd&quot;, timestamp())}</pre><pre># DynamoDB Sort Key<br>${concat(parse_time(&quot;HH:mm:ss.SSS&quot;, timestamp()), &#39;#&#39;, topic(6))}</pre><p><strong>Amazon DynamoDB</strong>: I created a DynamoDB standard table with On-demand capacity mode. Once I defined the IoT Core Rule above, DynamoDB started to store button click events as you can see in the image below. Since I used date information for the Partition Key and time information (and device name) for the Sort Key, I can easily retrieve data in the specific time period. Here, date and time are UTC.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/965/1*7Sl0W2rMXRNAQ0OqsaGTOQ.png" /><figcaption>Data in DynamoDB</figcaption></figure><p>Each record has the following data format. This example is a record that I single-short-clicked a button 01.</p><pre>{<br>  &quot;pk&quot;: {<br>    &quot;S&quot;: &quot;2024-10-19&quot;<br>  },<br>  &quot;sk&quot;: {<br>    &quot;S&quot;: &quot;19:59:04.104#d1-mini-10&quot;<br>  },<br>  &quot;payload&quot;: {<br>    &quot;M&quot;: {<br>      &quot;momentary_01_single_short_click&quot;: {<br>        &quot;S&quot;: &quot;on&quot;<br>      },<br>      &quot;ts&quot;: {<br>        &quot;N&quot;: &quot;1729367944&quot;<br>      },<br>      &quot;ttl&quot;: {<br>        &quot;N&quot;: &quot;1760903944&quot;<br>      }<br>    }<br>  }<br>}</pre><p><strong>AWS Lambda</strong>: I created a python script to retrieve data from DynamoDB, calculate statistics with Pandas and send a report with email by Amazon SES. It was basically straightforward. Once I convert data from DynamoDB to Pandas datafame, I can calculate statistics just by filtering, grouping and aggregating data. But handling timestamp confused me a lot. Especially because I decided to calculate statistics in local timezone (PST/PDT in my case) and defined a day starting 6am, not 12am. Since we wanted to count events related to housework and childcare, events in the last night (even after 12am and until early in this morning) needed to be counted as yesterday’s events. So, I decided to count events from 6:00am to next day 5:59am as 1-day events. It added a bit of complexity. We also wanted to know average time of each event in local timezone. I needed to extract only time information (without date) in local timezone, convert it to seconds, calculate average with Pandas and convert it back to time.</p><p><strong>Amazon SES</strong>: I just used gmail. Outlook mail didn’t work on SES for some reason.</p><p><strong>Amazon EventBridge</strong>: I set a schedule event like a cron job so that the lambda function is called every 6am.</p><h3>Results</h3><p>Now, we receive email report every morning like below. The report contains statistics of yesterday, this week (not in Monday report), last week, this month (not in the 1st day of the month), last month. It’s easy for us to glance at how things go recently on email. So far, I don’t find any cost concern.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZhJvo97tXswyUHkgXx91Vg.png" /></figure><h3>Final Thoughts</h3><p>I successfully created statistics reporting feature for my DIY smart button. My wife and I can keep aware of how things go in our housework and childcare. Although creating these features from scratch are likely to be inefficient, I could focus on core function scripting thanks to ESPHome, Home Assistant and AWS.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2024b8b15458" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Using DynamoDB and Timestream Together for Environment Data]]></title>
            <link>https://medium.com/@jj.underwood/using-dynamodb-and-timestream-together-for-environment-data-281a45b25959?source=rss-53516c79bb78------2</link>
            <guid isPermaLink="false">https://medium.com/p/281a45b25959</guid>
            <category><![CDATA[data-visualization]]></category>
            <category><![CDATA[home-assistant]]></category>
            <category><![CDATA[esphome]]></category>
            <category><![CDATA[iot]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[JJ Underwood]]></dc:creator>
            <pubDate>Sat, 12 Oct 2024 00:25:35 GMT</pubDate>
            <atom:updated>2024-10-28T23:52:23.847Z</atom:updated>
            <content:encoded><![CDATA[<p>I recently created <a href="https://medium.com/@jj.underwood/visualizing-environment-data-in-amazon-timestream-5e6aa4d2bc35">my serverless dashboard on AWS to visualize environment data</a>, which is measured by <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">my ESPHome based environment monitor</a>, <a href="https://medium.com/@jj.underwood/sending-environment-data-from-home-assistant-to-aws-3d9a793974b4">collected to Home Assistant, sent to AWS IoT</a>, and <a href="https://medium.com/@jj.underwood/storing-environment-data-in-amazon-timestream-af4e18e450f7">stored in Amazon Timestream</a>. Because I already have Home Assistant dashboard to see the latest data, the purpose of my additional dashboard was seeing more than the latest data — days, weeks, months or even years before. But honestly, it had a mixed result for me. Although I successfully created the dashboard with chart.js to show trend of environment data over a certain period of time, the cost of Timestream was too much for my household usage. Especially, the cost of Timestream queries took a lot. I realized that I would not be able to put the dashboard on a wall and update periodically. Timestream is great service, and I still want to try it for a long term, but I need a workaround to reduce the cost. So, I decided to go back to the basic and combine DynamoDB with Timestream.</p><h3>Objective</h3><p>The objective of my dashboard has not been changed from <a href="https://medium.com/@jj.underwood/visualizing-environment-data-in-amazon-timestream-5e6aa4d2bc35">my previous project</a>. But this time, I reduce the cost of my dashboard. My dashboard shows daily trends of environment metrics in graphs as its default mode and keeps updating graphs. If I select a specific metrics or a specific room, it shows weekly trend and comparison over different weeks. And if I select a longer time range, it can show long term trends as well. In this use case, to keep updating daily trends cost a lot, and to show weekly trends sometimes costs a little bit. I need to reduce the cost of these short-term and mid-term data retrieval.</p><h3>Architecture</h3><p>Before Timestream, there seemed to be many practices of using DynamoDB for storing time series data. There are even best practices published from AWS (like <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-time-series.html">this</a> and <a href="https://aws.amazon.com/blogs/database/design-patterns-for-high-volume-time-series-data-in-amazon-dynamodb/">this</a>). And now, we have Timestream, and we can find articles or discussion about “DynamoDB vs Timestream”. But this time, I’m going to use both of them — DynamoDB for short-term and mid-term data (a day to a week) and Timestream for long-term data (weeks, months or years).</p><p>The overall architecture and flows have not been changed much. I just added DynamoDB in front of Timestream. It’s not just cache. It’s more like a tiered architecture. But I don’t move data from DynamoDB to Timestream because it’s complicated. Although there is data redundancy, it’s acceptable for me because majority of cost comes from queries — reading data in my case. Instead, I store data in both. And I set TTL (Time-To-Live) on data stored in DynamoDB so that it automatically keeps only short-term and mid-term data. Since one of lambda function (Lambda #4 in the picture below) retrieves data from Timestream before, I modified the lambda function to retrieve either DynamoDB or Timestream based on the time period specified as parameters.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Wr72dJurHVQgoP7Qozt12g.png" /><figcaption>Overview of Dashboard</figcaption></figure><h3>Implementation</h3><p>My example code is <a href="https://github.com/jj-underwood/diy_environment_dashboard.git">here</a>.</p><p><strong>DynamoDB</strong>: I created a table with a Partition key named “pk” (String type) and a Sort key named “sk” (String type). The table class is DynamoDB Standard and Capacity mode is “On-demand”. When we store time series data on DynamoDB, there have been some best practices and discussions. In the past, separating tables was one of best practices (like <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-time-series.html">this</a> and <a href="https://aws.amazon.com/blogs/database/design-patterns-for-high-volume-time-series-data-in-amazon-dynamodb/">this</a>), but it does not necessarily seem to be always the case now. <a href="https://stackoverflow.com/questions/57762832/is-dynamodb-good-for-time-series-data-market-stock-price-example">Some discussion suggests</a> using location and metrics for a Partition key and timestamp for a Sort key. Because we can use a partial match for a Sort key, using timestamp for a Sort key makes data retrieval easier. In my case, my dashboard mostly needs to retrieve all the metrics of all the location (device) during the specific period of times. And I retrieve old data less often. So, I decided to use date information (yyyy-MM-dd) for a Partition key and a combination of time information (HH:mm:ss) and device information for a Sort key.</p><p><strong>IoT Core</strong>: I added a IoT Core Rule to route data to DynamoDB. I used a similar SQL with the one I used for Timestream. When I created the rule for Timestream, <a href="https://medium.com/@jj.underwood/storing-environment-data-in-amazon-timestream-af4e18e450f7">I needed to use SQL version 2015–10–08</a> because Timestream sometimes misunderstands data types between Double and Int especially when metrics data becomes like “24.0”. But in this case, I need to use SQL version 2016–03–23 because I want to use “<a href="https://docs.aws.amazon.com/iot/latest/developerguide/iot-sql-functions.html#iot-sql-function-parse-time">parse_time</a>” function, which is not supported in SQL version 2015–10–08, for DynamoDB Partition key and Sort key as below. But luckily, <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html">DynamoDB simply uses just Number type (Decimal) for all the number data</a>. So, I can use SQL version 2016–03–23 and “parse_time” function. Regarding SQL statement, I used similar SQL to the one I used for Timestream. The only difference is, I added a TTL value so that I can use it in DynamoDB.</p><pre>SELECT<br>    CAST(state.reported.temperature AS Double) as temperature,<br>    CAST(state.reported.humidity AS Double) as humidity,<br>    CAST(state.reported.illuminance AS Double) as illuminance,<br>    CAST(state.reported.presence AS varchar) as presence,<br>    CAST(state.reported.co2 AS Int) as co2,<br>    CAST(state.reported.voc AS Int) as voc,<br>    CAST(state.reported.nox AS Int) as nox,<br>    CAST(state.reported.pm1_0 AS Double) as pm1_0,<br>    CAST(state.reported.pm2_5 AS Double) as pm2_5,<br>    CAST(state.reported.pm4_0 AS Double) as pm4_0,<br>    CAST(state.reported.pm10_0 AS Double) as pm10_0,<br>    CAST(state.reported.pm2_5_avg_24h AS Double) as pm2_5_avg_24h,<br>    CAST(state.reported.pm10_0_avg_24h AS Double) as pm10_0_avg_24h,<br>    CAST(state.reported.pressure AS Double) as pressure,<br>    CAST(state.reported.uv AS Double) as uv,<br>    (timestamp() / 1000) as ts,<br>    ((timestamp() / 1000) + (10 * 24 * 60 * 60)) as ttl<br>FROM &#39;$aws/things/ThingName/shadow/name/+/update&#39;</pre><pre># DynamoDB Partition Key Value<br>${parse_time(&quot;yyyy-MM-dd&quot;, timestamp())}</pre><pre># DynamoDB Sort Key Value<br>${concat(parse_time(&quot;HH:mm:ss&quot;, timestamp()), &#39;#&#39;, topic(6))}</pre><p>Now, data started to be stored in DynamoDB as below. As I described above, the Partition key is a date string, and the Sort key is a combination of a time string and device name (I used “#” as a delimiter).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KIFxbSYMl7dSavEa-L5CLw.png" /><figcaption>Data Stored in DynamoDB</figcaption></figure><p><strong>Lambda</strong>: I modified the Lambda #4 function to retrieve data from either DynamoDB or Timestream. When a specified time period starts with a time before DynamoDB TTL, it retrieves data from Timestream. In case Timestream, I’m using “bin” function to aggregate data when a period of time is relatively long. Timestream has a wide variety of <a href="https://docs.aws.amazon.com/timestream/latest/developerguide/aggregate-functions.html">aggregation functions</a>, and these are one of the advantages of Timestream. But when I retrieve data from DynamoDB, I need to do similar aggregation by myself. Since I needed only simple time-bin based aggregation in this case, I could add the code to the Lambda #4 function. However, if I need more complex aggregation, I will need a different approach.</p><h3>Cost</h3><p>According to <a href="https://aws.amazon.com/dynamodb/pricing/on-demand/">DynamoDB On-demand pricing information</a>, the cost becomes something like the following.</p><p><strong>Read and write requests</strong>: Because I’m not using transactional writes and I’m using eventually consistent reads, DynamoDB charges one WRU (Write Request Unit) for each write (up to 1KB), and 0.5 RRU (Read Request Unit) for each eventually consistent read (up to 4KB). WRU price is $1.25 per million write request units and RRU price is $0.25 per million read request units. There are 9 sensor nodes measuring metrics in my house, and each of them sends data every 5 minutes. Each data has 4 to 15 metrics. The size of each data is under 1KB even it has 15 metrics. So, each write request consumes 1 WRU. And there are 12 x 24 x 9 = 2,592 write requests per day. The cost of writes will be (2,592/1,000,000) x 1.25 = $0.00324/day, $0.0972/month (30 days). Regarding read requests, my dashboard shows the last 24 hours data of all the metrics of all the sensor nodes, and update graphs periodically. Based on the actual size of data read from DynamoDB, the size of the last 24 hours data is about 20 KB (sometimes less than 20 KB and sometimes more than 20 KB). So, every time my dashboard updates graphs, it consumes (20 / 4) x 0.5 = 2.5 RRU. If I let my dashboard update every 15 minutes all day, it consumes 2.5 x 4 x 24 = 240 RRU per day. And if I show the last 7 days data sometimes (let’s assume twice a day), it consumes 2.5 x 7 x 2 = 35 RRU. The total is 275 RRU per day. The cost of reads will be (275/1,000,000) x 0.25 = $0.00006875/day, $0.0020625/month (30 days).</p><p><strong>Data storage</strong>: Because I’m using DynamoDB Standard table, first 25 GB stored per month is free. As I described above, my data will be within 25 GB limit for 10 days TTL.</p><p><strong>Data transfer</strong>: Because 100 GB of data transfer out to the internet free each month, my case will fit in this limit.</p><p>Compared to the cost of Timestream, the cost of DynamoDB is much cheaper in my use case. Especially, Timestream Queries costs 1.6576$/day, and 49.728$/month (30 days) for 15 minutes update. Now with DynamoDB, it becomes $0.00006875/day, $0.0020625/month (30 days) for the same 15 minutes update. I’m going to think about storing more data on DynamoDB.</p><h3>Final Thoughts</h3><p>I successfully reduced the cost of my dashboard by combining DynamoDB with Timestream. Based on this cost reduction, I will be able to hang my dashboard on a wall and let it keep updating graphs all day.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=281a45b25959" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[DIY Smart Button]]></title>
            <link>https://medium.com/@jj.underwood/diy-smart-button-43d4b461ee5d?source=rss-53516c79bb78------2</link>
            <guid isPermaLink="false">https://medium.com/p/43d4b461ee5d</guid>
            <category><![CDATA[esphome]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[smart-home]]></category>
            <category><![CDATA[home-assistant]]></category>
            <category><![CDATA[diy]]></category>
            <dc:creator><![CDATA[JJ Underwood]]></dc:creator>
            <pubDate>Fri, 04 Oct 2024 21:22:58 GMT</pubDate>
            <atom:updated>2024-10-04T21:22:58.769Z</atom:updated>
            <content:encoded><![CDATA[<p>We still need physical buttons or switches to do something in our daily life, like turning on/off lights, opening/closing a garage door and so forth. Although we can use some apps or even automate them, they might not always work for some family members. In my case, my wife and I wanted to just “count” some things related to housework and childcare. Simple physical buttons would be enough and work for both of us. We need at least 4 or more buttons in our use case. There are many nice smart button products on the market, but they are a little bit expensive for us. Eventually, I found some online videos or articles about ESPHome based buttons (like <a href="https://www.youtube.com/watch?v=o1iysHr1IQg">this</a> and <a href="https://ragingcomputer.com/2021/08/29/glowforge-esphome-home-assistant-light-color-controller/">this</a>). I decided to build my own DIY smart buttons.</p><p><strong>Disclaimer</strong>: Before I dive into the details, let’s set some ground rules:</p><ol><li>Educational Purpose Only: This blog post serves as an educational resource. I’m sharing my experiences, but I’m just a hobbyist, not a professional. Always consult experts for specific advice.</li><li>Assumption of Risk: By following my instructions, you assume full responsibility for any consequences. DIY projects involve risks, so proceed with caution.</li><li>Safety First: Handle electronic components carefully. Use appropriate tools and safety gear. Be aware of electrical hazards. Follow manufacturer guidelines for microcontrollers and sensors.</li></ol><h3>Objective</h3><p>Like my sensor node project, I’m going to build a smart button device that can be connected to an existing power outlet and Wi-Fi network. Because of my laziness and procrastination habit, changing batteries is challenging for me. And I’m going to let the device has multiple buttons — at least 4 and 5 for just in case.</p><h3>Materials</h3><ul><li><strong>Microcontroller</strong>: I have already decided to use <a href="https://www.wemos.cc/en/latest/d1/d1_mini.html">Wemos LOLIN D1 mini</a>, which is ESP8266 microcontroller board, because it’s cheap (<a href="https://www.aliexpress.us/item/2251832342786284.html">Official Store on AliExpress</a>) and I’ve used it in my sensor node project. It has many GPIO pins, but it seems that 5 of them are safe to use according to <a href="https://lastminuteengineers.com/wemos-d1-mini-pinout-reference/">an online article</a> — GPIO4, GPIO5, GPIO12, GPIO13 and GPIO14. I can connect 5 buttons to them.</li><li><strong>Enclosure</strong>: Like my sensor node project, I want to use a simple case. It does not have to be heavy duty or weatherproof because we use buttons inside our house. And I realized that I still had <a href="https://www.aliexpress.com/item/3256805431668233.html">a case</a> left over from my sensor node project. It has a flat surface to place some buttons. Depending on the size of buttons, I can place 5 buttons on its surface.</li><li><strong>Button</strong>: There are many types of buttons and switches on the market. Considering my objective, I need a simple momentary switch which has an easy-to-push size of button. So, an arcade style button will work for my use case. I found <a href="https://www.amazon.com/dp/B08SKJ6V7Z">a simple and affordable arcade-like button set</a> which comes with 24 pieces of buttons in an affordable price. I don’t need 24 buttons in this project though. They don’t have fancy LED lights and nice arcade grade microswitches. But each of them has just 2 pins and the height of buttons looks relatively short. The button size is 16mm, which is not so small and not so large. Based on the size of the case, I might be able to place 5 of them.</li><li><strong>Misc</strong>: I need to drill a 16mm hole on the cover of the case to place a button. Because I already have a power drill, I need to buy a step drill bit, or a hole saw drill bit. I decided to buy <a href="https://www.amazon.com/dp/B09MD2JDMH">a 16mm hole saw drill bit</a> this time. And like my sensor node project, I’m going to use a <a href="https://www.adafruit.com/product/4986">6-pin cable with connectors</a> so that wiring will be easier.</li></ul><h3>Building Hardware</h3><p>I thought it would be easy to drill a hole on the plastic cover of the case with the hole saw drill bit. But it turned out, it took a little bit try-and-error for me. Honestly, I ruined one of my cases. I should have practiced more. But eventually I managed to drill 5 holes and successfully place 5 buttons. From appearance perspective, I wanted to choose a white color for all the buttons because it matches the color of the case. But I discussed the role of each button with my wife, and we decided to choose different color for each button.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GEpEmrXPIt-31PZ0TDRccg.jpeg" /><figcaption>Buttons on the Case (Front).</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BPJ_fRnhQX4WOVB6hWhTMQ.jpeg" /><figcaption>Buttons on the Case (Back).</figcaption></figure><p>Because each button has 2 pins, I’m going to connect one of them to one of GPIO pins of D1 mini, and another one to GND pin of D1 mini. To reduce the number of wires to connect D1 mini, I daisy-chained pins for GND among buttons with jumper wires and solder. And I connected a 6-pin cable, which has a connector, to pins for GPIO and one of pins for GND.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7R7OOQ7lsz8zA4KoX2iUdQ.jpeg" /><figcaption>Wiring of Buttons.</figcaption></figure><p>I soldered a 6-pin cable, which has a matching connector, to GPIO4/5/12/13/14 and GND pins on D1 mini as well. Comparing my sensor node project, wiring is quite easy this time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QO6M3lD1xAJtkRO7j3i_bg.jpeg" /><figcaption>Wiring of Buttons and D1 mini.</figcaption></figure><p>The cost of materials is roughly $9 including D1 mini, case, three buttons, 6-pin cable. It’s affordable. If I count the cost of entire 24 pieces of buttons and a hole saw drill bit, it might be too expensive just for buttons though. Because I bought D1 mini and case from AliExpress, prices change frequently. Sometimes, you might get them much cheaper.</p><h3>Configuring Software</h3><p>Thanks to the official document of ESPHome and a great tutorial on the web, configuration was straightforward. I just need <a href="https://esphome.io/components/binary_sensor/gpio.html">GPIO Binary Sensor</a>, “<a href="https://esphome.io/components/binary_sensor/index.html#on-multi-click">on_multi_click</a>” automation of binary sensor and <a href="https://esphome.io/components/binary_sensor/template.html">Template Binary Sensor</a>. Each button requires the following YAML configuration. I defined 5 set of these configuration so that each button has “Single Short Click”, “Single Long Click” and “Double Click” capabilities.</p><pre>binary_sensor:<br>  - platform: gpio<br>    name: Momentary-01<br>    pin:<br>      number: GPIO04<br>      inverted: True<br>      mode:<br>        input: true<br>        pullup: true<br>    internal: True<br>    on_multi_click:<br>    - timing:<br>      - ON for at most 1s<br>      - OFF for at most 1s<br>      - ON for at most 1s<br>      - OFF for at least 0.2s<br>      then:<br>      - logger.log: &quot;Double Clicked&quot;<br>      - binary_sensor.template.publish:<br>          id: momentary_01_double_click<br>          state: ON<br>      - delay: 0.5s<br>      - binary_sensor.template.publish:<br>          id: momentary_01_double_click<br>          state: OFF<br>    - timing:<br>      - ON for 1s to 2s<br>      - OFF for at least 0.5s<br>      then:<br>      - logger.log: &quot;Single Long Clicked&quot;<br>      - binary_sensor.template.publish:<br>          id: momentary_01_single_long_click<br>          state: ON<br>      - delay: 0.5s<br>      - binary_sensor.template.publish:<br>          id: momentary_01_single_long_click<br>          state: OFF<br>    - timing:<br>      - ON for at most 1s<br>      - OFF for at least 0.5s<br>      then:<br>      - logger.log: &quot;Single Short Clicked&quot;<br>      - binary_sensor.template.publish:<br>          id: momentary_01_single_short_click<br>          state: ON<br>      - delay: 0.5s<br>      - binary_sensor.template.publish:<br>          id: momentary_01_single_short_click<br>          state: OFF<br>  - platform: template<br>    id: momentary_01_double_click<br>    name: Momentary-01 Double Click<br>    publish_initial_state: True<br>  - platform: template<br>    id: momentary_01_single_long_click<br>    name: Momentary-01 Single Long Click<br>    publish_initial_state: True<br>  - platform: template<br>    id: momentary_01_single_short_click<br>    name: Momentary-01 Single Short Click<br>    publish_initial_state: True</pre><p>Once I configured ESPHome, all the binary sensors showed up on Home Assistant. Because I have 5 buttons and each of them has 3 types of clicks, there are 15 binary sensors. Each of them has “on” or “off” state.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/334/1*_0988LS8QU-Y1GH2pNwkiQ.png" /></figure><p>Now I define Automation. Depending on a use case, I can trigger whatever I want by using click events of these buttons. You can do anything to make them actually smart.</p><p>In my case, since my objective is just counting some things related to housework and childcare over some period of times and understand statistics, I don’t necessarily need to have automations this time. I can either use related features of Home Assistant (like History Stats, Statistics, etc.) or create my custom application on Cloud.</p><p>Because I have been using AWS in my different project, I decided to record click events on Amazon Timestream for now. I defined a Named Shadow for the button device on AWS IoT Core and defined Automation as follows on Home Assistant to send click events data to the Named Shadow’s MQTT topic.</p><pre>alias: button-device<br>description: &quot;&quot;<br>trigger:<br>  - platform: state<br>    entity_id:<br>      - binary_sensor.button_device_momentary_01_single_short_click<br>      - binary_sensor.button_device_momentary_01_single_long_click<br>      - binary_sensor.button_device_momentary_01_double_click<br>      - binary_sensor.button_device_momentary_02_single_short_click<br>      - binary_sensor.button_device_momentary_02_single_long_click<br>      - binary_sensor.button_device_momentary_02_double_click<br>      - binary_sensor.button_device_momentary_03_single_short_click<br>      - binary_sensor.button_device_momentary_03_single_long_click<br>      - binary_sensor.button_device_momentary_03_double_click<br>      - binary_sensor.button_device_momentary_04_single_short_click<br>      - binary_sensor.button_device_momentary_04_single_long_click<br>      - binary_sensor.button_device_momentary_04_double_click<br>      - binary_sensor.button_device_momentary_05_single_short_click<br>      - binary_sensor.button_device_momentary_05_single_long_click<br>      - binary_sensor.button_device_momentary_05_double_click<br>condition: []<br>action:<br>  - metadata: {}<br>    data:<br>      qos: &quot;0&quot;<br>      retain: false<br>      topic: |<br>        {% if &#39;button_device&#39; in trigger.to_state.entity_id %}<br>          $aws/things/home-assistant/shadow/name/button-device/update<br>        {% endif %}<br>      payload: |<br>        {<br>          &quot;state&quot;: {<br>            &quot;reported&quot;: {<br>              {% if &#39;momentary_01_single_short_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_01_single_short_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_01_single_long_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_01_single_long_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_01_double_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_01_double_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_02_single_short_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_02_single_short_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_02_single_long_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_02_single_long_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_02_double_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_02_double_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_03_single_short_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_03_single_short_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_03_single_long_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_03_single_long_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_03_double_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_03_double_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_04_single_short_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_04_single_short_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_04_single_long_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_04_single_long_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_04_double_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_04_double_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_05_single_short_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_05_single_short_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_05_single_long_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_05_single_long_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% elif &#39;momentary_05_double_click&#39; in trigger.to_state.entity_id %}<br>                &quot;momentary_05_double_click&quot;: {{ trigger.to_state.state | tojson }}<br>              {% endif %}<br>            }<br>          }<br>        }<br>    action: mqtt.publish<br>mode: single</pre><p>After I define AWS IoT Core Rule, click events began to be routed to Timestream. Now, I can retrieve “on” state of all the buttons and count them to calculate statistics.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*EQI2CnZzkJSpv33tJkNiiQ.png" /><figcaption>Data on Amazon Timestream</figcaption></figure><h3>Final Thoughts</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vFoeDYYz_GLneDbKONYtEQ.jpeg" /><figcaption>DIY Smart Buttons on a Wall.</figcaption></figure><p>I successfully created my DIY smart buttons. Because my objective was just counting some things, it was more like just Wi-Fi connected buttons rather than smart buttons though. I placed my button device on a wall in my house so that my wife and I can push buttons anytime we need. Because it is not battery powered and not mobile, we cannot bring them anywhere. But we don’t have to find it instead. We both know that where it is. And we don’t have to worry about changing battery. Although it does not have sophisticated appearance like commercial products, I’m very satisfied with my affordable DIY smart buttons.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=43d4b461ee5d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Visualizing Environment Data in Amazon Timestream]]></title>
            <link>https://medium.com/@jj.underwood/visualizing-environment-data-in-amazon-timestream-5e6aa4d2bc35?source=rss-53516c79bb78------2</link>
            <guid isPermaLink="false">https://medium.com/p/5e6aa4d2bc35</guid>
            <category><![CDATA[timestream]]></category>
            <category><![CDATA[esphome]]></category>
            <category><![CDATA[home-assistant]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[iot]]></category>
            <dc:creator><![CDATA[JJ Underwood]]></dc:creator>
            <pubDate>Thu, 03 Oct 2024 20:55:46 GMT</pubDate>
            <atom:updated>2024-10-03T20:55:46.754Z</atom:updated>
            <content:encoded><![CDATA[<p>This is a continuation of another post “<a href="https://medium.com/@jj.underwood/sending-environment-data-from-home-assistant-to-aws-3d9a793974b4">Sending Environment Data from Home Assistant to AWS</a>” and “<a href="https://medium.com/@jj.underwood/storing-environment-data-in-amazon-timestream-af4e18e450f7">Storing Environment Data in Amazon Timestream</a>“. I used MQTT and Automation feature of Home Assistant to send environment metrics data measured by <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">my ESPHome based sensor nodes</a> to AWS IoT Core. And I used Amazon Timestream to store long term data. The final step is retrieving and using data in Amazon Timestream.</p><h3>Objective</h3><p>I’m going to create a simple dashboard to visualize data. Since I already have Home Assistant dashboard and History integration feature to see the latest environment data, I want to use this additional dashboard to see trends of metrics over a relatively longer period of time in graphs, and even compare them over multiple periods of time.</p><h3>Architecture</h3><p>Because I’m not a professional software engineer, I consulted Generative AI on how to achieve the goal. As a result, I decided the architecture and the flow of my dashboard as a picture below. In the picture, I already covered “Data Ingestion” part in another posts. I have 9 of ESPHome based sensor nodes to measure metrics data, and those data are once collected to Home Assistant, and Home Assistant publishes those data to AWS IoT Core, and they are routed to Timestream. This time, I create “Login” and “Data Retrieval” parts. It is a typical serverless architecture that requires several frontend HTML and JavaScript, backend Lambda functions and some additional AWS services. I store frontend HTML and JavaScript on S3 and access them from a browser through CloudFront. When it comes to visualizing data in graphs, I use chart.js to have nice graphs which are similar to Home Assistant History integration. And those JavaScript access backend Lambda functions through API Gateway. And then, backend Lambda functions access data. I protect my dashboard with a very simple email authentication to have a minimum level of security. I register users’ email addresses in DynamoDB in advance, a user inputs their email address and requests a one-time code, the dashboard sends the code with Amazon SES, and the user inputs the code to get authorized. I also protect data retrieval through API Gateway with JWT token. Once a user gets authorized, the dashboard returns a token, frontend JavaScript sends data retrieval requests with the token, and API Gateway verifies the token before transferring those requests.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*y0fxCvozWlK04Ko8-IcmOQ.png" /><figcaption>Overview of Dashboard</figcaption></figure><h3>Implementation</h3><p>All of my example codes are <a href="https://github.com/jj-underwood/diy_environment_dashboard.git">here</a>.</p><p><strong>IoT Core and Timestream</strong>: As I explained in another post, I have been using IoT Core Rule to route data, which arrive at Named Shadows’ MQTT topics, to Timestream for LiveAnalytics. My SQL statement to route data is as follows. And I set “1 day” for Memory store retention and “10 years” for Magnetic store retention for now.</p><pre>SELECT <br>  CAST(state.reported.temperature AS Double) as temperature,<br>  CAST(state.reported.humidity AS Double) as humidity,<br>  CAST(state.reported.illuminance AS Double) as illuminance,<br>  CAST(state.reported.presence AS varchar) as presence,<br>  CAST(state.reported.co2 AS Int) as co2,<br>  CAST(state.reported.voc AS Int) as voc,<br>  CAST(state.reported.nox AS Int) as nox,<br>  CAST(state.reported.pm1_0 AS Double) as pm1_0,<br>  CAST(state.reported.pm2_5 AS Double) as pm2_5,<br>  CAST(state.reported.pm4_0 AS Double) as pm4_0,<br>  CAST(state.reported.pm10_0 AS Double) as pm10_0,<br>  CAST(state.reported.pm2_5_avg_24h AS Double) as pm2_5_avg_24h,<br>  CAST(state.reported.pm10_0_avg_24h AS Double) as pm10_0_avg_24h,<br>  CAST(state.reported.pressure AS Double) as pressure,<br>  CAST(state.reported.uv AS Double) as uv <br>FROM &#39;$aws/things/MyThingName/shadow/name/+/update&#39;</pre><p><strong>CloudFront and S3</strong>: I stored following HTML and JavaScript on my S3 bucket. And I didn’t allow public access to the bucket. I allowed only my CloudFront distribution to access the bucket. I also set my CloudFront distribution URL in “AllowedOrigins” in CORS setting.</p><ul><li>login.html and login.js: These show the login page where a user submits their email address to receive one-time code and redirect a browser to the verify page.</li><li>verify.html and verify.js: These show the verify page where a user submits their email address and received one-time code, get a JWT token, store the token in a local storage and redirect a browser to the dashboard page.</li><li>dashboard.html and dashboard.js: These show the dashboard page where a user can see graphs of environment metrics. For now, I simply implemented three modes — the glance mode (default), the metrics mode and the device mode. In the default glance mode, the dashboard shows all the metrics data of all the devices during the last 24 hours at a glance and keeps updating graphs every 15 minutes. In the metrics mode, the dashboard shows only one metrics data of all the devices during two different time periods (the last 7 days and the previous week for now) in side-by-side. In the device mode, the dashboard shows all the metrics data of one device, which means one location, during two different time periods for each metrics. Regarding graphs, I used chart.js and tried to make them look like History integration feature of Home Assistant. I used a line chart for almost all the metrics and used a horizontal floating bar chart (like discussed <a href="https://github.com/chartjs/Chart.js/issues/9671">here</a>) only for human presence.</li><li>styles.css and config.json: These are just CSS and the configuration file for the dashboard.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TtFwjFiN-h-y95sUn85llg.png" /><figcaption>Glance Mode</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0EkuSafsH1eSpg48cp17Cw.png" /><figcaption>Metrics Mode</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VrkpfnyxO9IA28CyxQM2vQ.png" /><figcaption>Device Mode</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/790/1*YrjeXkXGQkujXrIwaaOVNw.png" /><figcaption>Line Chart Example 1</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/791/1*ToKSnM0d78mFS4paSuf8zg.png" /><figcaption>Line Chart Example 2</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/798/1*VXBJPfAtb9laXbpuOxqcaA.png" /><figcaption>Horizontal Floating Bar Chart for Human Presence</figcaption></figure><p><strong>API Gateway</strong>: I created the following three resources on my API Gateway.</p><ul><li>“/send-code” (POST and OPTIONS): This has Lambda proxy integration with Lambda function #1 to send one-time code based on a request from login.js above.</li><li>“/authorize” (POST and OPTIONS): This has Lambda proxy integration with Lambda function #2 to verify one-time code based on a request from verify.js above and return a JWT token back to verify.js.</li><li>“/data” (GET and OPTIONS): This has Lambda proxy integration with Lambda function #4 to fetch data from Timestream based on a request from dashboard.js above. It is also associated with Lambda Authorizer based on Lambda function #3 in its Method request setting to authorize the JWT token attached in the Authorization header of a request from dashboard.js.</li></ul><p><strong>Lambda</strong>: I created the following four Lambda python functions. Because I used Lambda proxy integration as described above, lambda functions basically need to return CORS headers.</p><ul><li>#1 “SendCode”: This is a simple python script to check if a user’s email address exists in DynamoDB, generate a random one-time code, store the code in DynamoDB and send the code to the email address by using Amazon SES.</li><li>#2 “VerifyCode”: This is also a simple python script to verify a one-time code by comparing it with a code stored in DynamoDB, generate a JWT token and return the token back to a browser. I needed to add a layer to the lambda function to use the PyJWT module.</li><li>#3 “AuthorizeDataAccess”: This is another simple python script to verify a JWT token. I added a layer as well.</li><li>#4 “RetrieveData”: This is a python script to fetch data from Timestream based on request parameters like metrics, devices and a time period which are selected on the dashboard. I decided to use a Timestream query like the following example so that I can retrieve data in a table like format where each row contains all the metrics columns. If a requested time period becomes long, the amount of data becomes large. Since my sensor nodes send data every 5 minutes and every time human presence state changes, the amount of all the metrics data from all the devices in 24 hours becomes roughly 3,000 rows. I used aggregation in Timestream query like the following example to keep the amount of data around 3,000 even in case a requested time period becomes longer than 24 hours.</li></ul><pre>-- Timestream query without aggregation<br>SELECT<br>  time,<br>  DEVICE_NAME,<br>  MAX(CASE WHEN measure_name = &#39;temperature&#39; THEN measure_value::double END) AS temperature,<br>  MAX(CASE WHEN measure_name = &#39;humidity&#39; THEN measure_value::double END) AS humidity,<br>  MAX(CASE WHEN measure_name = &#39;illuminance&#39; THEN measure_value::double END) AS illuminance,<br>  MAX(CASE WHEN measure_name = &#39;presence&#39; THEN measure_value::varchar END) AS presence,<br>  MAX(CASE WHEN measure_name = &#39;co2&#39; THEN measure_value::bigint END) AS co2,<br>  MAX(CASE WHEN measure_name = &#39;voc&#39; THEN measure_value::bigint END) AS voc,<br>  MAX(CASE WHEN measure_name = &#39;nox&#39; THEN measure_value::bigint END) AS nox,<br>  MAX(CASE WHEN measure_name = &#39;pm1_0&#39; THEN measure_value::double END) AS pm1_0,<br>  MAX(CASE WHEN measure_name = &#39;pm2_5&#39; THEN measure_value::double END) AS pm2_5,<br>  MAX(CASE WHEN measure_name = &#39;pm4_0&#39; THEN measure_value::double END) AS pm4_0,<br>  MAX(CASE WHEN measure_name = &#39;pm10_0&#39; THEN measure_value::double END) AS pm10_0,<br>  MAX(CASE WHEN measure_name = &#39;pm2_5_avg_24h&#39; THEN measure_value::double END) AS pm2_5_avg_24h,<br>  MAX(CASE WHEN measure_name = &#39;pm10_0_avg_24h&#39; THEN measure_value::double END) AS pm10_0_avg_24h,<br>  MAX(CASE WHEN measure_name = &#39;pressure&#39; THEN measure_value::double END) AS pressure,<br>  MAX(CASE WHEN measure_name = &#39;uv&#39; THEN measure_value::double END) AS uv<br>FROM &quot;MyTimestreamDbName&quot;.&quot;MyTimestreamTableName&quot;<br>WHERE<br>  DEVICE_NAME IN (&#39;d1-mini-01&#39;, &#39;d1-mini-02&#39;, &#39;d1-mini-03&#39;, &#39;d1-mini-04&#39;, &#39;d1-mini-05&#39;, &#39;d1-mini-06&#39;, &#39;d1-mini-07&#39;, &#39;d1-mini-08&#39;, &#39;d1-mini-09&#39;)<br>  AND measure_name IN (&#39;temperature&#39;, &#39;humidity&#39;, &#39;illuminance&#39;, &#39;presence&#39;, &#39;co2&#39;, &#39;voc&#39;, &#39;nox&#39;, &#39;pm1_0&#39;, &#39;pm2_5&#39;, &#39;pm4_0&#39;, &#39;pm10_0&#39;, &#39;pm2_5_avg_24h&#39;, &#39;pm10_0_avg_24h&#39;, &#39;pressure&#39;, &#39;uv&#39;)<br>  AND time BETWEEN &#39;2024-MM-DD HH:mm:ss&#39; AND &#39;2024-MM-DD HH:mm:ss&#39;<br>GROUP BY time, DEVICE_NAME<br>ORDER BY time</pre><pre>-- Timestream query with aggregation<br>SELECT<br>  BIN(time, XXXXs) AS binned_time,<br>  DEVICE_NAME,<br>  MAX(CASE WHEN measure_name = &#39;temperature&#39; THEN measure_value::double END) AS temperature,<br>  MAX(CASE WHEN measure_name = &#39;humidity&#39; THEN measure_value::double END) AS humidity,<br>  MAX(CASE WHEN measure_name = &#39;illuminance&#39; THEN measure_value::double END) AS illuminance,<br>  MAX(CASE WHEN measure_name = &#39;presence&#39; THEN measure_value::varchar END) AS presence,<br>  MAX(CASE WHEN measure_name = &#39;co2&#39; THEN measure_value::bigint END) AS co2,<br>  MAX(CASE WHEN measure_name = &#39;voc&#39; THEN measure_value::bigint END) AS voc,<br>  MAX(CASE WHEN measure_name = &#39;nox&#39; THEN measure_value::bigint END) AS nox,<br>  MAX(CASE WHEN measure_name = &#39;pm1_0&#39; THEN measure_value::double END) AS pm1_0,<br>  MAX(CASE WHEN measure_name = &#39;pm2_5&#39; THEN measure_value::double END) AS pm2_5,<br>  MAX(CASE WHEN measure_name = &#39;pm4_0&#39; THEN measure_value::double END) AS pm4_0,<br>  MAX(CASE WHEN measure_name = &#39;pm10_0&#39; THEN measure_value::double END) AS pm10_0,<br>  MAX(CASE WHEN measure_name = &#39;pm2_5_avg_24h&#39; THEN measure_value::double END) AS pm2_5_avg_24h,<br>  MAX(CASE WHEN measure_name = &#39;pm10_0_avg_24h&#39; THEN measure_value::double END) AS pm10_0_avg_24h,<br>  MAX(CASE WHEN measure_name = &#39;pressure&#39; THEN measure_value::double END) AS pressure,<br>  MAX(CASE WHEN measure_name = &#39;uv&#39; THEN measure_value::double END) AS uv<br>FROM &quot;MyTimestreamDbName&quot;.&quot;MyTimestreamTableName&quot;<br>WHERE<br>  DEVICE_NAME IN (&#39;d1-mini-01&#39;, &#39;d1-mini-02&#39;, &#39;d1-mini-03&#39;, &#39;d1-mini-04&#39;, &#39;d1-mini-05&#39;, &#39;d1-mini-06&#39;, &#39;d1-mini-07&#39;, &#39;d1-mini-08&#39;, &#39;d1-mini-09&#39;)<br>  AND measure_name IN (&#39;temperature&#39;, &#39;humidity&#39;, &#39;illuminance&#39;, &#39;presence&#39;, &#39;co2&#39;, &#39;voc&#39;, &#39;nox&#39;, &#39;pm1_0&#39;, &#39;pm2_5&#39;, &#39;pm4_0&#39;, &#39;pm10_0&#39;, &#39;pm2_5_avg_24h&#39;, &#39;pm10_0_avg_24h&#39;, &#39;pressure&#39;, &#39;uv&#39;)<br>  AND time BETWEEN &#39;2024-MM-DD HH:mm:ss&#39; AND &#39;2024-MM-DD HH:mm:ss&#39;<br>GROUP BY BIN(time, XXXXs), DEVICE_NAME<br>ORDER BY binned_time</pre><p><strong>DynamoDB and SES</strong>: There is nothing special here. I just used DynamoDB to store user data and used SES to send email.</p><h3>Cost</h3><p>After I started storing data in Timestream and developing my dashboard, I realized that majority of the cost comes from Timestream usage. According to <a href="https://aws.amazon.com/timestream/pricing/">the official pricing document</a>, the cost of Timestream depends on “Writes”, “Queries”, “Memory store” and “Magnetic store”. In my case, I can estimate the cost as follows (correct me if I’m wrong).</p><ul><li><strong>Writes</strong>: Home Assistant publishes metrics data of each sensor node to the correspondent Named Shadow’s MQTT topic in every 5 minutes. The number of metrics is 4 at minimum (Temperature, Humidity, Illuminance, Presence) and 15 at maximum (Temperature, Humidity, Illuminance, Presence, CO2, VOC, NOx, PM1.0, PM2.5, PM4.0, PM10.0, PM2.5 in 24h average, PM10.0 in 24h average, Pressure, UV). The write data size will fit in 1 KB even at maximum (timestamp, dimension, 15 set of measure names and measure values). The number of write requests for each sensor node is 12 writes/hour, 288 writes/day, 8,640 writes/month (30 days). And Home Assistant publishes presence metrics data of each sensor node every time the presence state changes (from “on” to “off” or from “off” to “on”). Based on actual measurements in my house, the average number of write requests triggered by the presence state change of each sensor node is about 90 writes/day, so 2,700 writes/month (30 days). Total write requests for 9 sensor nodes will be 9 x (8,640 + 2,700) = 102,060 writes/month. The cost will be 102,060 x 0.50$/1,000,000 = 0.05$/month.</li><li><strong>Queries</strong>: The cost is calculated in TCU-hours. <a href="https://docs.aws.amazon.com/timestream/latest/developerguide/tcu.html">The official document</a> says, “Each TCU is billed on an hourly basis with per-second granularity and for a minimum of 30 seconds”. In my case, my Timestream query does not exceed 30 seconds. It always ends within 10 seconds. And my Max Query TCU is set to 4 (minimum value). If my dashboard in the glance mode updates graphs of all the metrics of all the devices every 15 minutes, query time will be 4 x 30 = 120 seconds/hour, 120 x 24 = 2,880 seconds/day, which is 0.8 hour/day. The cost will be 4 TCU x 0.8 hour/day x 0.518$/TCU-hour = 1.6576$/day, and 49.728$/month (30 days).</li><li><strong>Memory store</strong>: My memory store retention is set to 24 hours. Although the write data size in every 5 minutes is within 1 KB, each metrics is handled as 1 record in Timestream. So, 15 metrics data are considered to be 15 KB data. In my case, the total size becomes 65 KB for 9 sensor nodes. So, 65 KB x 12 x 24 = 18.72 MB/day. Regarding the presence state change for 9 sensor nodes, 1 KB x 90 x 9 = 0.81 MB/day. Total data size is 19.53 MB/day = 0.01953 GB/day. The cost will be 0.01953 GB x 24 hours x 0.036$/GB-hour = 0.0168739$/day, 0.5062176$/month (30 days).</li><li><strong>Magnetic store</strong>: My magnetic store retention is set to 10 years. Because the total data size is 0.01953 GB/day, the total data size will be 0.01953 x 365 = 7.12845 GB in 1 year and 7.12845 x 10 = 71.2845 GB in 10 years. As the total data size increases, the cost of magnetic store increases month by month. The cost will reach to 7.12845 GB x 0.03$ GB/month = 0.21$/month after 1 year and 2.1$/month after 10 years.</li></ul><p>As estimated above, the cost of queries takes a lot. I definitely don’t want to update graphs quite often. Even if I reduce the update cycle to every 30 minutes or even every 1 hour, the dashboard still costs 24.864$/month or 12.432$/month. It’s a lot for me just for showing graphs. It’s definitely not something that I can put on a wall to show graphs all the time.</p><h3>Final Thoughts</h3><p>I successfully created my dashboard on AWS to see trends of environment metrics measured by ESPHome and Home Assistant. Because I’m not good at maintaining my local IT infrastructure, I tried to store long term data on Timestream and visualize them on Cloud. But now, I realized that it would be costly for me. Although I’m still not sure if it would work for me, it was a good opportunity for me to learn dashboard implementation from Generative AI and revisit the balance between the cost of my local IT and Cloud.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5e6aa4d2bc35" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Storing Environment Data in Amazon Timestream]]></title>
            <link>https://medium.com/@jj.underwood/storing-environment-data-in-amazon-timestream-af4e18e450f7?source=rss-53516c79bb78------2</link>
            <guid isPermaLink="false">https://medium.com/p/af4e18e450f7</guid>
            <category><![CDATA[iot]]></category>
            <category><![CDATA[timestream]]></category>
            <category><![CDATA[home-assistant]]></category>
            <category><![CDATA[esphome]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[JJ Underwood]]></dc:creator>
            <pubDate>Fri, 30 Aug 2024 23:36:53 GMT</pubDate>
            <atom:updated>2024-08-30T23:37:35.685Z</atom:updated>
            <content:encoded><![CDATA[<p>This is a continuation of another post “<a href="https://medium.com/@jj.underwood/sending-environment-data-from-home-assistant-to-aws-3d9a793974b4">Sending Environment Data from Home Assistant to AWS</a>”. I used MQTT and Automation feature of Home Assistant to send environment metrics data measured by <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">my ESPHome based sensor nodes</a> to AWS IoT Core. Now, I’m going to use data. The first step is storing data in Amazon Timestream.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/880/1*oehAfA6uG_Vej8cBpfOJ_Q.jpeg" /></figure><h3>Objective</h3><p>Currently, <a href="https://medium.com/@jj.underwood/diy-environment-monitor-307f5a7fbd70">I’m measuring environment metrics like temperature, relative humidity, illuminance, human presence, CO2, VOC index, NOx index, PM, pressure and UV index with my sensor nodes placed in several areas in my house</a>. Those data are collected to Home Assistant with Native API. I can see environment metrics on Home Assistant dashboard, which is great. It is reasonable to see and use the latest data within local IT infrastructure in my house without relying on the Cloud infrastructure. But sometimes, I want to see long-term data. Storing data in the long term is challenging for me because of my laziness about reliability, management and maintenance of local IT infrastructure. So, I decided to use environment data sent to AWS IoT Core for long-term data visualization while continuing to use the latest and short-term data on Home Assistant at home.</p><h3>Architecture</h3><p><a href="https://medium.com/@jj.underwood/sending-environment-data-from-home-assistant-to-aws-3d9a793974b4">I already shared how to send data from Home Assistant to AWS IoT Core</a>. I’m using Named Shadow to manage data on AWS IoT Core. Each named shadow corresponds to each sensor node and each named shadow has each MQTT topic. Now, measured data from each sensor node are sent to each MQTT topic as follows.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/579/1*SPAWwwhaO_06YX4sDRWJhA.jpeg" /><figcaption>The list of Named Shadows</figcaption></figure><p>Because those environment metrics data are long-term time series data, I decided to store them in Amazon Timestream database. Thankfully, AWS IoT rules can route data arriving in MQTT topics to Amazon Timestream database.</p><p>Once I start storing data in Amazon Timestream, I can retrieve those data and use them on some applications, a dashboard in my case, which I’ll talk about later.</p><h3>Configuration of Amazon Timestream database</h3><p>It was very straightforward. I decided to use “Amazon Timestream for LiveAnalytics”. I created just “Standard database”, and a table with “Default partitioning”, 24 hours “Memory store retention” and 10 years “Magnetic store retention”.</p><h3>Configuration of AWS IoT rules</h3><p>This was a little bit tricky part. Routing data itself is very easy — opening up “Message routing” on AWS IoT, defining SQL statement to select data from MQTT topics, choosing “Timestream table” for “Rule actions”. Since each MQTT topic name includes correspondent sensor node name, I can extract sensor node name from MQTT topic name and use it as dimension for Timestream. Which means that I don’t have to send sensor node name in MQTT payload. And I don’t have to send timestamp as well since Timestream database adds timestamp. But when it comes to SQL statement, I initially used default SQL V2 (2016–03–23) and defined a simple SQL like this.</p><pre>SELECT state.reported.* FROM &#39;$aws/things/ha-20240731/shadow/name/+/update&#39;</pre><p>Because data are sent to named shadows, each data has the following structure. I can extract data by selecting “state.reported.*” like above SQL. And I can use a wild card “+” in FROM part to extract data from all the MQTT topics in one SQL statement. In my case “+” contains sensor node name as you can see in the above list of named shadows. I can extract an actual string in “+” as dimension by referring it with <a href="https://docs.aws.amazon.com/whitepapers/latest/designing-mqtt-topics-aws-iot-core/best-practices-for-using-mqtt-topics-in-the-aws-iot-rules-engine.html">substitution templates</a>. I defined “DEVICE_NAME” for dimension name and “${topic(6)}” for dimension value in my rule.</p><pre>{<br>  &quot;state&quot;: {<br>    &quot;reported&quot;: {<br>      &quot;temperature&quot;: 26.48,<br>      &quot;humidity&quot;: 53.3,<br>      &quot;pressure&quot;: 1012.4,<br>      &quot;co2&quot;: 609,<br>      &quot;voc&quot;: 405,<br>      &quot;nox&quot;: 100,<br>      &quot;pm1_0&quot;: 7.8,<br>      &quot;pm2_5&quot;: 8.1,<br>      &quot;pm4_0&quot;: 8.1,<br>      &quot;pm10_0&quot;: 8.1,<br>      &quot;illuminance&quot;: 140.4,<br>      &quot;uv&quot;: 0.0,<br>      &quot;pm2_5_avg_24h&quot;: 5.75,<br>      &quot;pm10_0_avg_24h&quot;: 5.78,<br>      &quot;presence&quot;: &quot;off&quot;<br>    }<br>  }<br>}</pre><p>At first, everything seemed to be good. But when I checked stored values on Timestream, some metrics were sometimes missing. And I realized that there were errors caused by data type. It seems that Amazon Timestream automatically choose data type best suited for data. In my case, temperature, relative humidity, illuminance, PM, UV index have accuracy decimals like “26.<strong>48</strong>” in case of temperature. They are likely to be handled as “measure_value::double” in Timestream. And CO2, VOC index, NOx index does not have decimals. They are likely to become “measure_value::bigint” in Timestream. And human presence is either “on”, “off” or “unknown”. It becomes “measure_value::varchar” in Timestream. And problem happened in measurement data which have decimals. When measurement data becomes like “26.<strong>0</strong>” or “26.<strong>00</strong>”, Timestream causes data type error because it converts those to integer like “26” and it is different from the data type of existing data. I need to explicitly cast data in SQL.</p><p>Before I change SQL statement, I need to create a new table if the existing table already has wrong data types. In my case, UV index had wrong data type (bigint) from the beginning because its measurement inside home was almost always “0.00000”. According to <a href="https://docs.aws.amazon.com/iot/latest/developerguide/timestream-rule-action.html">AWS official document</a>, I need to use SQL V1 (2015–10–08) to correctly cast data. I actually did several try-and-error until I found out this document. Finally, I used SQL V1, and defined SQL statement as follows. There might be more efficient way though.</p><pre>SELECT<br>    CAST(state.reported.temperature AS Double) as temperature,<br>    CAST(state.reported.humidity AS Double) as humidity,<br>    CAST(state.reported.illuminance AS Double) as illuminance,<br>    CAST(state.reported.presence AS varchar) as presence,<br>    CAST(state.reported.co2 AS Int) as co2,<br>    CAST(state.reported.voc AS Int) as voc,<br>    CAST(state.reported.nox AS Int) as nox,<br>    CAST(state.reported.pm1_0 AS Double) as pm1_0,<br>    CAST(state.reported.pm2_5 AS Double) as pm2_5,<br>    CAST(state.reported.pm4_0 AS Double) as pm4_0,<br>    CAST(state.reported.pm10_0 AS Double) as pm10_0,<br>    CAST(state.reported.pm2_5_avg_24h AS Double) as pm2_5_avg_24h,<br>    CAST(state.reported.pm10_0_avg_24h AS Double) as pm10_0_avg_24h,<br>    CAST(state.reported.pressure AS Double) as pressure,<br>    CAST(state.reported.uv AS Double) as uv<br>FROM &#39;$aws/things/ha-20240731/shadow/name/+/update&#39;</pre><h3>Query for Timestream</h3><p>Now, I can retrieve data from Timestream. I can use Query editor on Amazon Timestream to see how I can get data from Timestream. If I use a simple query as follows,</p><pre>SELECT<br> *<br>FROM &quot;timestream-20240819&quot;.&quot;environment-20240826&quot;<br>WHERE<br> time BETWEEN ago(5m) AND now()<br> AND DEVICE_NAME IN (&#39;d1-mini-01&#39;, &#39;d1-mini-02&#39;, &#39;d1-mini-03&#39;, &#39;d1-mini-04&#39;, &#39;d1-mini-05&#39;, &#39;d1-mini-06&#39;, &#39;d1-mini-07&#39;, &#39;d1-mini-08&#39;, &#39;d1-mini-09&#39;)<br>ORDER BY<br> time ASC</pre><p>I can get data as follows.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/916/1*W-nmCP8r1400O_LIPk2Sow.jpeg" /></figure><p>But considering my objective — data visualization on dashboard, I might want to use something like the following query so that I can get data in a sort of table structure.</p><pre>SELECT<br> time,<br>    DEVICE_NAME,<br>    MAX(CASE WHEN measure_name = &#39;temperature&#39; THEN measure_value::double END) AS temperature,<br>    MAX(CASE WHEN measure_name = &#39;humidity&#39; THEN measure_value::double END) AS humidity,<br>    MAX(CASE WHEN measure_name = &#39;illuminance&#39; THEN measure_value::double END) AS illuminance,<br>    MAX(CASE WHEN measure_name = &#39;presence&#39; THEN measure_value::varchar END) AS presence,<br>    MAX(CASE WHEN measure_name = &#39;co2&#39; THEN measure_value::bigint END) AS co2,<br>    MAX(CASE WHEN measure_name = &#39;voc&#39; THEN measure_value::bigint END) AS voc,<br>    MAX(CASE WHEN measure_name = &#39;nox&#39; THEN measure_value::bigint END) AS nox,<br>    MAX(CASE WHEN measure_name = &#39;pm1_0&#39; THEN measure_value::double END) AS pm1_0,<br>    MAX(CASE WHEN measure_name = &#39;pm2_5&#39; THEN measure_value::double END) AS pm2_5,<br>    MAX(CASE WHEN measure_name = &#39;pm4_0&#39; THEN measure_value::double END) AS pm4_0,<br>    MAX(CASE WHEN measure_name = &#39;pm10_0&#39; THEN measure_value::double END) AS pm10_0,<br>    MAX(CASE WHEN measure_name = &#39;pm2_5_avg_24h&#39; THEN measure_value::double END) AS pm2_5_avg_24h,<br>    MAX(CASE WHEN measure_name = &#39;pm10_0_avg_24h&#39; THEN measure_value::double END) AS pm10_0_avg_24h,<br>    MAX(CASE WHEN measure_name = &#39;pressure&#39; THEN measure_value::double END) AS pressure,<br>    MAX(CASE WHEN measure_name = &#39;uv&#39; THEN measure_value::double END) AS uv<br>FROM &quot;timestream-20240819&quot;.&quot;environment-20240826&quot;<br>WHERE<br> DEVICE_NAME IN (&#39;d1-mini-01&#39;, &#39;d1-mini-02&#39;, &#39;d1-mini-03&#39;, &#39;d1-mini-04&#39;, &#39;d1-mini-05&#39;, &#39;d1-mini-06&#39;, &#39;d1-mini-07&#39;, &#39;d1-mini-08&#39;, &#39;d1-mini-09&#39;)<br>    AND measure_name IN (&#39;temperature&#39;, &#39;humidity&#39;, &#39;illuminance&#39;, &#39;presence&#39;, &#39;co2&#39;, &#39;voc&#39;, &#39;nox&#39;, &#39;pm1_0&#39;, &#39;pm2_5&#39;, &#39;pm4_0&#39;, &#39;pm10_0&#39;, &#39;pm2_5_avg_24h&#39;, &#39;pm10_0_avg_24h&#39;, &#39;pressure&#39;, &#39;uv&#39;)<br>    AND time BETWEEN ago(5m) AND now()<br>GROUP BY time, DEVICE_NAME<br>ORDER BY time</pre><p>The result is as follows. Because data are grouped by time and DEVICE_NAME, one row contains all the metrics from one sensor node at one timestamp. Since all the sensor nodes do not measure all the metrics, unavailable metrics become NULL.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Vo8qCUOts3hJg5luj9M4Ug.jpeg" /></figure><h3>Final Thoughts</h3><p>After several try-and-error, I successfully start storing data on Timestream. Although it still relies on my local IT for sending data, I don’t have to care about management and maintenance of infrastructure for long-term data, which is great. I still need to observe cost for in my use case to decide if it’s really a good idea or not. Now, I’m trying to create my own dashboard to visualize long-term data. I’ll share details later.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=af4e18e450f7" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>