<?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>Forem: The Refactored Road</title>
    <description>The latest articles on Forem by The Refactored Road (@ndygen).</description>
    <link>https://forem.com/ndygen</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1749369%2Fdc576384-ef56-4258-a0f1-06d76fc61940.png</url>
      <title>Forem: The Refactored Road</title>
      <link>https://forem.com/ndygen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ndygen"/>
    <language>en</language>
    <item>
      <title>I Built a $15 Smart Home Controller (and Why Phones Are Bad Dashboards)</title>
      <dc:creator>The Refactored Road</dc:creator>
      <pubDate>Sat, 04 Apr 2026 09:13:06 +0000</pubDate>
      <link>https://forem.com/ndygen/i-built-a-15-smart-home-controller-and-why-phones-are-bad-dashboards-2cff</link>
      <guid>https://forem.com/ndygen/i-built-a-15-smart-home-controller-and-why-phones-are-bad-dashboards-2cff</guid>
      <description>&lt;p&gt;In &lt;a href="https://refactoredroad.blogspot.com/2026/03/my-washing-machine-picks-its-own.html" rel="noopener noreferrer"&gt;my previous post&lt;/a&gt; I wrote about how my washing machine and dryer pick their own schedule based on energy prices. That post was about the concept — a Homey app that finds the cheapest window to run your appliances. What I didn't mention was the thing on the kitchen wall that makes it actually usable.&lt;/p&gt;

&lt;p&gt;Because here's the truth about smart home automation: if the only way to interact with it is through an app on your phone, it won't survive contact with your household.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Apps
&lt;/h2&gt;

&lt;p&gt;I call it the spouse test. If your partner needs to unlock their phone, find the right app, navigate to the right screen, and tap three buttons just to start the dryer at a cheap time — they're going to press the button on the dryer instead. And they'd be right to.&lt;/p&gt;

&lt;p&gt;A physical device on the wall changes that dynamic entirely. It's always on, always showing the current state, and requires exactly one tap to do the thing. No login, no loading spinner, no "update available" popup. It's the difference between a light switch and a lighting app.&lt;/p&gt;

&lt;p&gt;So I decided to build one. A small touch screen near the laundry area that shows energy prices, appliance status, and lets you schedule a run with a single tap.&lt;/p&gt;

&lt;h2&gt;
  
  
  $15 of Hardware, Infinite Ambition
&lt;/h2&gt;

&lt;p&gt;The ESP-2432S028R — affectionately known as the "Cheap Yellow Display" or CYD — is one of those products that shouldn't exist at its price point. For about $15, you get an ESP32 microcontroller, a 2.8-inch color TFT display with touch input, WiFi, and enough GPIO pins to feel dangerous.&lt;/p&gt;

&lt;p&gt;The screen is 320 by 240 pixels. That's not a lot. For context, the icon for your weather app is probably bigger than this entire display. But for a single-purpose device that shows two appliance cards and a price indicator, it's plenty.&lt;/p&gt;

&lt;p&gt;The ESP32 handles WiFi, MQTT communication with my Homey hub, NTP time sync, and over-the-air firmware updates. All on a chip that draws about half a watt. The whole thing runs off a USB-C cable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Pivot
&lt;/h2&gt;

&lt;p&gt;I didn't start with custom firmware. Like any reasonable person, I started with ESPHome — the YAML-based framework that lets you configure ESP32 devices without writing C++. Define your sensors, your display layout, your automations, and ESPHome generates the firmware for you.&lt;/p&gt;

&lt;p&gt;It worked. For about two hours.&lt;/p&gt;

&lt;p&gt;The problem was MQTT topic structure. My Homey app publishes appliance data on specific topics with JSON payloads — state, pricing, scheduling, savings data. ESPHome's MQTT integration is designed for Home Assistant's auto-discovery format, and bending it to work with custom topic structures felt like writing C++ with extra steps. Worse steps, actually, because you're debugging generated code you didn't write.&lt;/p&gt;

&lt;p&gt;So I pivoted to PlatformIO with the Arduino framework. Full C++ control, direct access to proven libraries like TFT_eSPI for the display and espMqttClient for MQTT, and — crucially — the ability to structure my code the way the problem demanded rather than the way a YAML schema allowed.&lt;/p&gt;

&lt;p&gt;Was it more work? Absolutely. Was it the right call? Without question. Some projects fit neatly into a configuration-driven framework. This one needed a real codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a UI on 320 by 240 Pixels
&lt;/h2&gt;

&lt;p&gt;Designing a touch interface for a 2.8-inch screen is an exercise in brutal prioritization. There's no room for nice-to-haves. Every pixel has a job.&lt;/p&gt;

&lt;p&gt;The layout I landed on has three elements. A top bar showing the current time, date, energy price, and WiFi status. Two appliance cards — one for the washer, one for the dryer — each showing their current state with a color-coded badge, key stats, and an action button. That's it.&lt;/p&gt;

&lt;p&gt;The action button is context-sensitive. If the appliance is idle, it says PLAN and opens a scheduling modal. If it's already scheduled, it says CANCEL. The modal lets you pick a deadline: 4, 8, 12, or 24 hours from now. Tap your choice and the system finds the cheapest slot within that window.&lt;/p&gt;

&lt;p&gt;Color does a lot of heavy lifting on a small screen. Green badge means idle and ready. Blue means scheduled. Yellow means the system is still learning this appliance's power profile. Red means something needs attention. You can read the state of both appliances from across the room without your glasses.&lt;/p&gt;

&lt;p&gt;One design choice I'm particularly happy with: when an appliance is scheduled, the card shows the start time, the expected price per cycle, and how much you're saving compared to running it right now. That last number — "Saving €0.38" — turns out to be incredibly motivating. It makes the abstract concept of energy optimization tangible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bugs That Taught Me Things
&lt;/h2&gt;

&lt;p&gt;Embedded development has a way of humbling you. Here are three problems that took longer to solve than I'd like to admit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dual SPI buses.&lt;/strong&gt; The CYD board has two SPI peripherals: one for the display (ILI9341) and one for the touch controller (XPT2046). Most example code assumes they share a bus. They don't — and they can't, because the display's SPI runs at 40 MHz while the touch controller maxes out at 2.5 MHz. Sharing a bus means reconfiguring speed on every swap, which causes timing glitches. The fix was putting them on separate hardware SPI buses (HSPI and VSPI), each with their own pins. Obvious in hindsight. Took a full afternoon to figure out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SPI reentrancy crashes.&lt;/strong&gt; This one was subtle. MQTT messages arrive asynchronously via callbacks. My first implementation updated the display directly from the MQTT callback — parse the JSON, update the card, done. It worked great until a message arrived while the display was mid-draw. Two SPI transactions on the same bus at once: instant crash, no useful stack trace.&lt;/p&gt;

&lt;p&gt;The solution is almost embarrassingly simple: the MQTT callback sets a boolean flag. The main loop checks the flag, and if it's set, redraws the screen. No concurrent SPI access, no crashes. It's the embedded equivalent of "don't update the DOM from a web worker" — except the consequence isn't a console warning, it's a hard reset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Touch calibration.&lt;/strong&gt; Every CYD unit has slightly different touch calibration values. The raw coordinates from the XPT2046 don't map 1:1 to screen pixels — they need to be scaled and offset. My first unit worked perfectly with the default calibration. My second unit registered taps about 30 pixels to the left. The fix was a calibration routine and storing per-unit values, but the debugging process involved a lot of tapping one spot and watching a dot appear somewhere else. It felt like playing Operation with an invisible board.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;The finished device sits on the wall near our washing machine. It shows the current energy price, the status of both appliances, and lets you schedule a run with two taps: hit PLAN, pick your deadline. The system finds the cheapest slot within your window and confirms the schedule.&lt;/p&gt;

&lt;p&gt;It updates over-the-air, reconnects automatically if WiFi drops, and uses about as much power as a phone charger. The total hardware cost was under €20.&lt;/p&gt;

&lt;p&gt;But the real measure of success isn't technical. It's that my partner uses it without thinking about it. There's no app to open, no concept to explain. Tap the card, pick when you need it done. The rest happens automatically.&lt;/p&gt;

&lt;p&gt;Sometimes the best smart home upgrade isn't smarter software — it's a simple screen on the wall that does exactly one thing well.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://refactoredroad.blogspot.com/2026/04/i-built-dedicated-smart-home-controller.html" rel="noopener noreferrer"&gt;The Refactored Road&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>esp32</category>
      <category>smarthome</category>
      <category>embedded</category>
      <category>mqtt</category>
    </item>
    <item>
      <title>My Washing Machine Picks Its Own Schedule (and Saves Money)</title>
      <dc:creator>The Refactored Road</dc:creator>
      <pubDate>Mon, 30 Mar 2026 15:09:11 +0000</pubDate>
      <link>https://forem.com/ndygen/my-washing-machine-picks-its-own-schedule-and-saves-money-3ape</link>
      <guid>https://forem.com/ndygen/my-washing-machine-picks-its-own-schedule-and-saves-money-3ape</guid>
      <description>&lt;p&gt;My electricity price changes every hour. Some hours it’s 0.05 EUR/kWh. Other hours it’s 0.38. The difference between running the dishwasher at 4 PM versus 2 AM can easily be half a euro — per cycle, every day.&lt;/p&gt;

&lt;p&gt;I have a washing machine, a dishwasher, and a dryer. They run almost daily. None of them came with a “wait for cheap power” button.&lt;/p&gt;

&lt;p&gt;So I built one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I run a &lt;a href="https://homey.app" rel="noopener noreferrer"&gt;Homey&lt;/a&gt; smart home hub. Each appliance is plugged into a smart plug that reports real-time power consumption. The idea was simple: if I know &lt;em&gt;how much&lt;/em&gt; power an appliance uses and &lt;em&gt;when&lt;/em&gt; electricity is cheapest, I can schedule the run automatically.&lt;/p&gt;

&lt;p&gt;The result is &lt;a href="https://homey.app/en-us/app/net.dongen.power-profiler/Power-Profiler/" rel="noopener noreferrer"&gt;Power Profiler&lt;/a&gt;, a Homey app that watches your appliances, learns their patterns, and triggers a flow at the cheapest moment. Here’s how it works — and what I learned building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teaching a smart plug to recognize a wash cycle
&lt;/h2&gt;

&lt;p&gt;A smart plug gives you one number: watts. Right now, my dishwasher is drawing 3 watts. Boring. But when it starts a cycle, that number jumps to 2,200 during the heating phase, drops to 50 during a pause, spikes again for the rinse, and eventually settles back to idle.&lt;/p&gt;

&lt;p&gt;The challenge is knowing when a cycle starts and — more importantly — when it actually ends. My first attempt was simple: if power goes above 50 watts, the cycle started. If it drops below 50, it ended.&lt;/p&gt;

&lt;p&gt;That lasted about one wash.&lt;/p&gt;

&lt;p&gt;The problem is mid-cycle dips. A washing machine drops to near-zero between the wash and spin phases. The dishwasher pauses between wash and rinse. My naive detector saw these pauses and thought each one was a separate cycle.&lt;/p&gt;

&lt;p&gt;The fix was a &lt;strong&gt;cooldown period&lt;/strong&gt;: a 2-minute grace window after power drops. If the appliance kicks back in during those 2 minutes, it’s still the same cycle. If it stays quiet, the cycle is done. This one change turned messy data into clean recordings.&lt;/p&gt;

&lt;p&gt;The finite state machine has three states: &lt;strong&gt;idle&lt;/strong&gt; (waiting), &lt;strong&gt;active&lt;/strong&gt; (recording), and &lt;strong&gt;cooldown&lt;/strong&gt; (waiting to see if it’s really over). Simple enough to reason about, robust enough to handle the real world.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three cycles and you’re profiled
&lt;/h2&gt;

&lt;p&gt;After three complete runs, the app has enough data to build a &lt;em&gt;power profile&lt;/em&gt; — a minute-by-minute picture of what the appliance does during a full cycle.&lt;/p&gt;

&lt;p&gt;And this is where the appliances show their personalities.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;dishwasher&lt;/strong&gt; runs for about 3 hours and 20 minutes. That surprised me — I always thought of it as a quick appliance. It heats water to 2,200 watts in bursts, runs pumps at moderate power, pauses, rinses, and dries. Three profiled cycles averaging 199 minutes each, using about 1 kWh per run.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;washing machine&lt;/strong&gt; is faster but more dramatic. About 2 hours and 20 minutes on a standard program, peaking at 2,285 watts when the heating element kicks in. The power curve looks like a mountain range — big spikes for heating, quiet valleys during soaking, and a final burst for the spin cycle.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;dryer&lt;/strong&gt; is the gentle one. A steady 550 watts for the duration of the run — no dramatic spikes, just a long, patient hum. It’s still being profiled, so it’s not scheduling yet. Three cycles and it’ll join the team.&lt;/p&gt;

&lt;p&gt;The profile captures more than just averages. For each minute, the app records the minimum, maximum, and average power across all recorded cycles. And it keeps learning — every new cycle gets added to a rolling buffer of the last 20 runs. Run your dishwasher on eco mode a few times, then switch to intensive? The profile gradually shifts to reflect your actual usage, not just the first three cycles you happened to record. Three cycles gets you started. Twenty keeps you accurate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the cheapest hour
&lt;/h2&gt;

&lt;p&gt;Here’s where the money comes in. In the Netherlands, &lt;a href="https://refactoredroad.blogspot.com/2026/03/my-solar-panels-only-work-30-why-air.html" rel="noopener noreferrer"&gt;day-ahead electricity prices&lt;/a&gt; are published every afternoon. Prices for each hour of the next day, straight from the EPEX spot market. Some hours are 5 cents per kWh. Others are 35. Occasionally they go negative — yes, you can get &lt;em&gt;paid&lt;/em&gt; to use electricity.&lt;/p&gt;

&lt;p&gt;The algorithm is a sliding window. Take the power profile and slide it across every possible start time within your deadline. For each position, multiply the minute-by-minute power draw by the energy price at that moment. The cheapest total wins.&lt;/p&gt;

&lt;p&gt;Here’s what that looks like in practice, step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You trigger “Schedule cheapest start within 12 hours”&lt;/strong&gt; — say, at 8 PM. That gives the app a window from 8 PM tonight until 8 AM tomorrow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app grabs the power profile&lt;/strong&gt; — for the dishwasher, that’s 199 minutes of minute-by-minute power data, learned from three previous cycles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It grabs tonight’s energy prices&lt;/strong&gt; — a price for each hour (or quarter-hour, depending on your provider) within the 12-hour window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It tries every possible start time.&lt;/strong&gt; “What if I start at 8:00 PM? 8:01? 8:02?” For each one, it overlays the power profile onto the price timeline and calculates the total cost: watts times price, minute by minute, summed up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cheapest start time wins.&lt;/strong&gt; Say starting at 1:15 AM costs EUR 0.18, while starting at 8 PM would have cost EUR 0.34. The app picks 1:15 AM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A timer is set.&lt;/strong&gt; The app counts down to 1:15 AM. When it fires, it triggers your Homey flow — which turns on the smart plug, and the dishwasher starts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The dishwasher’s 3.5-hour cycle needs a big window — it can’t just squeeze into any single cheap hour. The algorithm has to find the best &lt;em&gt;stretch&lt;/em&gt; of hours, weighing the expensive heating minutes against the cheaper idle phases. The washing machine’s 2.5-hour cycle has a bit more flexibility, but its high-power heating spikes mean the price during those specific minutes matters a lot.&lt;/p&gt;

&lt;p&gt;The math is almost embarrassingly simple. No machine learning, no neural networks. Just a loop that tries every start time and picks the cheapest one. It runs in milliseconds. Sometimes brute force is the right answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The things that bit me
&lt;/h2&gt;

&lt;p&gt;The algorithm worked quickly. Getting the details right took longer. During testing, I displayed estimated cost with two decimal places — sensible for money, except when the dishwasher runs at 2 AM during cheap hours and the total cost is EUR 0.003, which rounds to EUR 0.00. Looks like the calculation is broken. Then there was the schedule that showed “Tomorrow, 23:00” for tonight’s run — a timezone comparison bug where UTC and local time disagreed about which day it was. Three lines to fix, two hours to find. The kind of bugs that only show up at midnight, in a timezone you didn’t consider.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when you press “go”
&lt;/h2&gt;

&lt;p&gt;Here’s the actual user experience.&lt;/p&gt;

&lt;p&gt;You install the app, pick your energy provider (EasyEnergy, EnergyZero, or one of three others), and add a device. The app shows all your smart plugs — pick the one under your dishwasher. Done. You now have a “Dishwasher Profiler” on your Homey dashboard.&lt;/p&gt;

&lt;p&gt;For the next few days, just use your appliances normally. The app watches. After three cycles, your profile is ready. The dashboard shows average cycle duration, energy per run, and a cycle count.&lt;/p&gt;

&lt;p&gt;Now you create a Homey flow: “Schedule cheapest start within 12 hours.” The app slides your profile across tonight’s prices and picks the winner. Your dashboard shows “Next start: 02:15” and “Estimated cost: EUR 0.1825.”&lt;/p&gt;

&lt;p&gt;At 2:15 AM, the app fires a trigger. Your flow turns on the dishwasher. The dishwasher starts. You’re asleep.&lt;/p&gt;

&lt;p&gt;Is it life-changing money? No. But it’s money I save by doing absolutely nothing. The app watches, learns, waits, and acts. I just load the dishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I’d do differently
&lt;/h2&gt;

&lt;p&gt;If I started over, I’d track cumulative savings from day one. Right now, users can see their total energy and cost, but not “how much you saved compared to running at peak.” That comparison would make the value instantly visible. This will be a future improvement.&lt;/p&gt;

&lt;p&gt;But the core idea — watch, learn, schedule — holds up. It’s the kind of automation that disappears into the background, which is exactly where the best smart home tech should be.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://refactoredroad.blogspot.com/2026/03/my-washing-machine-now-picks-its-own.html" rel="noopener noreferrer"&gt;The Refactored Road&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>homeautomation</category>
      <category>iot</category>
      <category>smarthome</category>
      <category>energy</category>
    </item>
  </channel>
</rss>
