<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Joshua Olukotun</title>
    <description>The latest articles on DEV Community by Joshua Olukotun (@joshua_olukotun_e94af4236).</description>
    <link>https://dev.to/joshua_olukotun_e94af4236</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4230223%2F58707a27-c67b-491f-af3d-08e1f12d5f6b.jpg</url>
      <title>DEV Community: Joshua Olukotun</title>
      <link>https://dev.to/joshua_olukotun_e94af4236</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joshua_olukotun_e94af4236"/>
    <language>en</language>
    <item>
      <title>Shey Light Dey House? How I Built an ESP32 Power Monitor for My Home</title>
      <dc:creator>Joshua Olukotun</dc:creator>
      <pubDate>Fri, 06 Nov 2026 12:05:33 +0000</pubDate>
      <link>https://dev.to/joshua_olukotun_e94af4236/shey-light-dey-house-how-i-built-an-esp32-power-monitor-for-my-home-2l7c</link>
      <guid>https://dev.to/joshua_olukotun_e94af4236/shey-light-dey-house-how-i-built-an-esp32-power-monitor-for-my-home-2l7c</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://joshua.olukotun.ng/blog/home-electricity-monitor/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Imagine you’re at work, and you are deciding on whether to charge your devices before you leave office or to charge them when you get home, but then you wonder, "omo shey light dey house?". Well, I often find myself in situations where I want to know if I currently have light, when they took the light, or how long I've had light. So, I built a system that tells me exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;To solve this issue, I thought of building a simple device that communicates with my web server to notify me when I have power or not. The device works by sending a &lt;strong&gt;&lt;em&gt;beat&lt;/em&gt;&lt;/strong&gt; —an HTTP request every 30 seconds to my server. The device is plugged into a socket that only gets power from NEPA (the grid), not from the inverter. &lt;/p&gt;

&lt;p&gt;With this socket setup, I only receive beat requests whenever there is direct power supply from NEPA since the device is off otherwise. So all I have to do to determine if I have power at home is to check on the server side, when I last received a beat from the device. If the database says that the last beat was received more than 90 seconds ago, I can assume that there is no power at home.  &lt;/p&gt;

&lt;h2&gt;
  
  
  System Architecture
&lt;/h2&gt;

&lt;p&gt;An ESP32-C3 dev module is programmed to send a request every 30 seconds to a small VPS running a Rust (Axum) backend service and an SQLite database. &lt;/p&gt;

&lt;p&gt;When the server receives a request, it records it in the SQLite database. A simple dashboard built, with no extra libraries like React, displays analytics that answer my opening questions: whether power is ON/OFF, how long it has been ON/OFF, and how many hours of power there's been today and this week. The dashboard polls the server every 30 seconds and the server queries the SQLite database to get the relevant data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6gnmpk7zcssqmdu38hxv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6gnmpk7zcssqmdu38hxv.png" alt="System Architecture Overview" width="800" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The ESP32 Device
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How does the ESP32 device work?
&lt;/h3&gt;

&lt;p&gt;The ESP32-C3 is powered by a USB cable which is connected to a socket that only works when NEPA is available. &lt;/p&gt;

&lt;p&gt;When NEPA is available, the ESP32 turns on and begins sending &lt;strong&gt;&lt;em&gt;beat&lt;/em&gt;&lt;/strong&gt; requests to the server every 30 seconds. &lt;/p&gt;

&lt;p&gt;The ESP32 is attached to a 3D printed sleeve that slides over a USB-C phone charger. I used a spare phone charger I had lying around. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz12rzkiqgx6gmsrw5hvo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz12rzkiqgx6gmsrw5hvo.jpg" alt="The enclosure" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffike2hz3uyn42jdj8i4k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffike2hz3uyn42jdj8i4k.jpg" alt="The finished unit plugged into a wall socket" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a beat request made of?
&lt;/h3&gt;

&lt;p&gt;Each beat is a small JSON &lt;code&gt;POST&lt;/code&gt; to &lt;code&gt;/beat&lt;/code&gt;. Here is a sample request below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/beat&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer &amp;lt;API_KEY&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"device_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"homev1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"boot_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2435&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reset_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"power_on"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"uptime_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4120&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A beat request is made of 4 fields:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;device_id&lt;/strong&gt;: This lets the server tell devices apart, so one server could be used to monitor multiple devices. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;boot_id&lt;/strong&gt;: This is randomly generated every time the ESP32 powers on. It lets the server detect a reboot of the ESP32, which usually means they took the light and brought it back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;reset_reason&lt;/strong&gt;: This contains the reason why the ESP32 rebooted. The ESP32 could reboot either due to power outage, or due to random system crashes. A reset_reason of power_on or brownout is most likely because of a power outage, while a reset_reason of watchdog or panic means the ESP32 board crashed and there might not have been a power outage. See &lt;a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-reference/system/misc_system_api.html#_CPPv418esp_reset_reason_t" rel="noopener noreferrer"&gt;Espressif's full list of reset reasons&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;uptime_ms&lt;/strong&gt;: When the ESP32 boots up, it spends some time setting up the Wi-Fi connection before the first beat request is sent. This tracks how long the ESP32 has been on. The server subtracts uptime_ms from the current time to find the actual time when the power came back. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A beat request also contains an "Authorization" header carrying an API key, so only my device can send beats.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;boot_id&lt;/code&gt; and &lt;code&gt;reset_reason&lt;/code&gt; are set once when the board boots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;getReasonForReset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;esp_reset_reason_t&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;esp_reset_reason&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;esp_reset_reason_t&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ESP_RST_PANIC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"panic"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;esp_reset_reason_t&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ESP_RST_INT_WDT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;esp_reset_reason_t&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ESP_RST_TASK_WDT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;esp_reset_reason_t&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ESP_RST_WDT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"watchdog"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;default:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"unknown"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="c1"&gt;// Connect to Wi-Fi network&lt;/span&gt;
    &lt;span class="n"&gt;setupWifi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;boot_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;esp_random&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;reset_reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getReasonForReset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Web Server
&lt;/h2&gt;

&lt;p&gt;The backend code that handles all requests and communicates with the SQLite database is written in Rust. SQLx and Axum were the main libraries used for database communication and request handling respectively. &lt;/p&gt;

&lt;h3&gt;
  
  
  How is a beat request stored in the database?
&lt;/h3&gt;

&lt;p&gt;The server doesn't store beats. It stores power periods. A power period represents a continuous stretch of power, i.e., a period (duration) in which there is light.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;power_periods&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;AUTOINCREMENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;device_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;boot_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;reset_reason&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;started_at&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;last_seen_at&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;started_at&lt;/code&gt; column represents when the power came on, i.e., the time they brought light, and &lt;code&gt;last_seen_at&lt;/code&gt; is the time the latest beat for that period was received.&lt;/p&gt;

&lt;p&gt;When a beat request arrives, the Rust backend code first checks if the &lt;code&gt;boot_id&lt;/code&gt; of that beat is the same as the &lt;code&gt;boot_id&lt;/code&gt; in the last updated power period for that &lt;code&gt;device_id&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the boot_ids are the same, then we update the &lt;code&gt;last_seen_at&lt;/code&gt; column of the last updated power period to the current time. &lt;/li&gt;
&lt;li&gt;If the boot_ids are different or there are no power periods yet, we insert a new power period row into the database. The &lt;code&gt;started_at&lt;/code&gt; column of the new row is the current time minus &lt;code&gt;uptime_ms&lt;/code&gt;. A change in the &lt;code&gt;boot_id&lt;/code&gt; usually means that there was an outage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This method of using each beat to update or insert a new power period instead of storing each beat is advantageous because it dramatically reduces the growth rate of the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiacoeibjv0q2z7zohg5h.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiacoeibjv0q2z7zohg5h.gif" alt="A beat inserting a new power period, then the next beat updating it" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How does it know there is no light?
&lt;/h3&gt;

&lt;p&gt;Say the server receives a beat from the ESP32 device at 12:10 AM, and then receives no new beat from that device within a 90-second window. We can assume the ESP32 is not receiving any power, and therefore there is no light. This isn't 100% certain: if the Wi-Fi the ESP32 is connected to goes down, the beats stop too.&lt;/p&gt;

&lt;p&gt;Using the absence of a regular signal to detect a problem is the same idea as a &lt;a href="https://en.wikipedia.org/wiki/Dead_man%27s_switch" rel="noopener noreferrer"&gt;dead man's switch&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dashboard
&lt;/h2&gt;

&lt;p&gt;The dashboard shows if there is currently light, how long there has been light or how long there has been no light. It was written using plain HTML, CSS and vanilla JavaScript. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhwkrke0lfk3g6wl1fam4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhwkrke0lfk3g6wl1fam4.jpg" alt="The dashboard on a phone" width="800" height="1094"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dashboard refreshes every 30 seconds. The timeline chart shown in the dashboard is drawn as an SVG by hand, without a chart library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Improvements and Future Ideas
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;High and low voltage alerts&lt;/strong&gt;: Add a voltmeter to the ESP32 device and then send alerts to the users on the dashboard in the case of a low voltage or high voltage situation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telling power sources apart&lt;/strong&gt;: Modify the device to be able to distinguish between various power sources (inverter, NEPA, generator) based on variation in frequency of the power source. My guess is that each power source has a unique frequency variation. For instance, a generator's frequency might fluctuate a lot while that of an inverter might be very stable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predicting power&lt;/strong&gt;: Add a prediction mechanism that can predict if you'll have power and at what time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More homes&lt;/strong&gt;: The &lt;code&gt;device_id&lt;/code&gt; already lets one server track several devices. With devices in various homes, the dashboard could become a live map of which areas have light. &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Now, when I'm away from home wondering whether there's light, I don't have to call anyone. I just open the dashboard. &lt;/p&gt;

&lt;p&gt;The only thing I actually paid for was the ESP32-C3 (₦9,000) from &lt;a href="https://nerdshed.com.ng/product/esp32-c3-supermini-development-board-with-built-in-4m-flash-memory/" rel="noopener noreferrer"&gt;Nerdshed&lt;/a&gt;. Everything else I already had: a spare charger, a USB-C cable, my VPS, and my domain I use for other projects.&lt;/p&gt;

&lt;p&gt;The full code, firmware and server, is on &lt;a href="https://github.com/Josh596/electricity-monitor" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. If you build your own, or have ideas for any of the improvements above, I'd love to hear about it. &lt;/p&gt;

</description>
      <category>esp32</category>
      <category>rust</category>
      <category>iot</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
