See which appliance is using your electricity, what it costs you, and switch it off automatically. All of it from one clip-on sensor.
SmartWatt is a small do-it-yourself energy monitor. You clip a sensor around one wire (nothing is ever wired into the mains), and SmartWatt works out which appliance just switched on: the kettle, the fan, the lamp. It does this from the appliance's electrical "fingerprint". It then shows you:
- how much power each appliance is using right now;
- what it costs on your electricity bill. SmartWatt understands Sarawak Energy's Tariff D, where using a little more in a month can move the whole month into a more expensive price band. SmartWatt warns you before you cross that line;
- how much CO₂ your usage is responsible for;
- and, if you add smart plugs, it can switch appliances off for you when they are left on. It always warns you first and gives you 60 seconds to cancel. It never touches anything you mark as protected, and it never switches on anything that heats up.
Everything runs on your own laptop. There is no cloud, no account and no subscription.
Status: prototype. SmartWatt is a working research prototype, and its software is covered by automated tests. How accurate it is on your appliances depends on how carefully you build and calibrate your sensor board (Part 5 shows how). No accuracy figures are claimed here.
flowchart TB
subgraph Hardware["⚡ Hardware Layer: the monitored power strip"]
A["🔌 Clip-on current sensors<br/>SCT-013-005 + SCT-013-020"]
B["🔄 9 V AC-AC adapter<br/>voltage reference"]
C["📟 ESP32-S3 sensor board<br/>MCP3208 ADC · NILM firmware"]
D["🔘 Tasmota smart plugs<br/>optional"]
end
subgraph Laptop["💻 Laptop Layer: Windows, no cloud"]
E["📬 Mosquitto MQTT broker<br/>port 1883"]
F["⚙️ FastAPI server<br/>ingest · ledger · rules · safety gate"]
G["🗃 SQLite<br/>smartwatt.db"]
H["💰 Tariff & CO₂ engine<br/>Sarawak Energy Tariff D"]
I["📋 appliances.toml<br/>fingerprints.csv"]
J["🕒 Windows time server<br/>NTP"]
K["🧪 Simulator<br/>optional"]
end
subgraph Client["🖥 Client Layer"]
L["🌐 React dashboard<br/>Live · Month · Appliances · Control · Setup"]
end
A -->|"current"| C
B -->|"voltage"| C
J -->|"clock sync"| C
C -->|"MQTT over Wi-Fi hotspot<br/>smartwatt/telemetry · smartwatt/event"| E
K -->|"MQTT"| E
D -->|"stat/…/POWER"| E
E -->|"telemetry · events · plug state"| F
F -->|"cmnd/…/POWER<br/>safety gate only"| E
E -->|"switch on / off"| D
F --> G
F --> H
I <-->|"appliance list · training captures"| F
I -.->|"pio uploadfs over USB"| C
L <-->|"HTTP :8000<br/>REST + WebSocket"| F
- How it works (in plain words)
- Safety first: read this before anything else
- What you need
- Part 1: Try SmartWatt on your laptop, no hardware needed
- Part 2: Build the sensor board
- Part 3: Prepare your laptop (one time only)
- Part 4: Put SmartWatt onto the ESP32 board
- Part 5: First power-up and calibration
- Part 6: Set up smart plugs (optional)
- Part 7: Tell SmartWatt about YOUR appliances
- Part 8: Train SmartWatt to recognise your appliances
- Part 9: Everyday use
- Troubleshooting
- Using SmartWatt outside Sarawak
- For developers
Wall socket ── safety switch (RCD) ── line splitter ── power strip ── your appliances
│
two clip-on current sensors, around ONE wire
│
9 V AC adapter (senses the voltage) ──► SENSOR BOARD (breadboard + ESP32-S3)
│
Wi-Fi
▼
YOUR LAPTOP
├── Mosquitto: a "post office" for the messages
├── SmartWatt server: saves the data, works out the bill
├── the dashboard: a web page you open in your browser
└── smart plugs (optional) ◄── SmartWatt switches them
The line splitter is optional. Without one, the sensors clip around the live wire inside the power strip's own cable (Step 2.6).
- Measuring. The sensor board measures the voltage and the current 4,000 times a second.
- Noticing a change. When the total power suddenly jumps up or down, something was switched on or off.
- Recognising it. SmartWatt compares the jump's electrical fingerprint (14 measurements, such as how much power, how "spiky" the current is, and how big the start-up surge is) with fingerprints you taught it in Part 8. If nothing matches well enough, it honestly says "unknown" instead of guessing.
- Reporting. The board sends everything over Wi-Fi to your laptop, which stores it and shows it on the dashboard.
SmartWatt monitors everything plugged into one power strip. It does not monitor your whole house: that would mean clipping the sensor inside your fuse box, which only a licensed electrician may open.
- Nothing on your breadboard ever connects to mains. Its only inputs are the low-voltage output of a 9 V AC adapter and the tiny signals from the clip-on sensors.
- Never open a plug, socket or fuse box, and never cut into the insulation of an individual wire. The clip-on sensors go around the live wire only: either the loop of a ready-made line splitter (item 17), or, if you don't buy one, the brown wire inside the power strip's cable after stripping back its outer jacket only, with the power strip unplugged (Step 2.6). The line splitter is the safer choice: once the jacket is removed, only the wires' own thin insulation stands between you and 240 V.
- Only use the SCT-013-005 and SCT-013-020 current sensors (or the -010 / -030 substitutes). Never use the SCT-013-000. It has no built-in protection resistor, and it can produce dangerous voltages when unplugged while clipped on.
- Use an RCD (a portable safety switch, 30 mA) between the wall and everything else. Press its TEST button at the start of every session: it must switch off. Then reset it.
- Power the ESP32 board only from your laptop's USB port.
- Heating appliances (kettle, iron, heater): never leave them unattended while testing. Keep water and fire away from the electronics, and keep a fire extinguisher (ABC powder type) nearby.
- If anything gets hot, smells or smokes: switch off at the wall first, then investigate.
You build and use this project at your own risk. If you are unsure about any step, ask someone qualified.
Everything below is for one SmartWatt. Search for the part names on Shopee or Lazada (Malaysia), or any electronics shop. Buy one or two spares of the three chips (items 1-3). They are cheap, and a spare saves a week of waiting if one gets damaged.
| # | Item | Qty | What it's for | |
|---|---|---|---|---|
| 1 | ESP32-S3-DevKitC-1 N16R8 | 1 | The small computer that runs SmartWatt | Must be the N16R8 version (16 MB flash, 8 MB PSRAM). Other versions will not work with the settings in this project |
| 2 | MCP3208 12-bit ADC, DIP-16 (MCP3208-CI/P) |
1 | Turns the sensor signals into numbers | Get the DIP (through-hole) package, not SOIC/SMD |
| 3 | MCP6002 op-amp, DIP-8 (MCP6002-I/P) |
1 | Makes a steady 1.65 V "middle" reference | Get the DIP package, not "I/SN" (SMD) |
| 4 | SCT-013-005 clip-on current sensor (5 A : 1 V) | 1 | Measures small loads precisely | Never the SCT-013-000. Many listings are titled "SCT-013-000" and let you pick other ratings in a menu. Make sure you are buying 005 ("5A 1V") |
| 5 | SCT-013-020 clip-on current sensor (20 A : 1 V) | 1 | Measures big loads such as a kettle | Same warning. The only allowed substitute is the SCT-013-030 |
| 6 | 9 V AC-AC adapter (a plug-in transformer) | 1 | Lets the board sense the mains voltage safely | Its label must say AC output ("9V~", "9VAC", "AC-AC"). Most "9 V adapters" are AC-DC and will not work. Part 2 shows how to check |
| 7 | 10 kΩ resistor (¼ W) | 3 | Buy a resistor assortment kit: it has all values | |
| 8 | 820 Ω resistor (¼ W) | 4 | ||
| 9 | 100 nF ceramic capacitor (marked "104") | 5 | ||
| 10 | 3.5 mm audio jack to screw terminal breakout board | 2 | Connects each current sensor's plug to the breadboard | Look for a small board with screw terminals, not a cable adapter |
| 11 | DC barrel jack (female) to screw terminal adapter | 1 | Connects the AC adapter's plug to the breadboard | Must fit your adapter's plug (usually 5.5 × 2.1 mm) |
| 12 | Solderless breadboard, 830 points | 1 | Where the circuit is built. No soldering needed | |
| 13 | Jumper wire kit (male-male, male-female, female-female) | 1 | The wires | |
| 14 | USB-C data cable | 1 | Connects the ESP32 to your laptop | Some cheap cables only charge. It must carry data |
| 15 | Kitchen aluminium foil | a little | Optional shield under the breadboard, which lowers noise |
| # | Item | Qty | What it's for |
|---|---|---|---|
| 16 | Portable RCD plug adapter, 30 mA (SIRIM approved) | 1 | Cuts the power in milliseconds if current leaks to earth |
| 17 | AC line splitter with a UK/Malaysian (BS 1363) plug, optional | 1 | Separates the live wire into a loop you can clip the sensors around, without cutting anything. Use the ×1 loop, not the ×10 loop. No splitter? Strip back the outer jacket of the power strip's cable and clip around the live wire alone instead (Step 2.6). The splitter is the safer choice |
| 18 | Power strip (extension socket) | 1 | Everything plugged in here is what SmartWatt monitors |
| 19 | Multimeter (with AC volts, DC volts and continuity/beep) | 1 | Checks your parts and wiring |
| 20 | Plug-in energy meter (shows volts, amps and watts) | 1 | Your reference for calibration in Part 5. Safest option: no probes near mains |
| 21 | Fire extinguisher, ABC powder type | 1 | For work with heating appliances |
| # | Item | Qty | What it's for |
|---|---|---|---|
| 22 | Tasmota smart plug with energy monitoring (Type G / UK plug, pre-flashed with Tasmota) | 1 or more | Lets SmartWatt switch appliances on and off. Must run Tasmota firmware. Plugs sold for other phone apps (Tuya, Smart Life) will not work unless you re-flash them |
| 23 | Clamp meter | 1 | A more precise calibration reference than the energy meter |
- A Windows 10 or 11 laptop with Wi-Fi and a USB port. This guide is written for Windows.
- Your appliances. SmartWatt learns your appliances. It notices changes of about 8 W or more, so very small loads (a phone charger with nothing charging, say) are below what it can see.
The original prototype was budgeted at about RM 700 (Malaysia, 2026), including two smart plugs and spare chips. Prices change, so treat that as a rough guide.
You install these in Part 1 and Part 4. Each has an installer, and nothing needs programming knowledge.
| Software | What it does |
|---|---|
| uv | Installs Python and everything SmartWatt's server needs |
| Node.js | Builds the dashboard web page |
| Mosquitto | The "post office" that carries messages between the board and the laptop |
| PlatformIO | Uploads SmartWatt onto the ESP32 board (Part 4) |
SmartWatt comes with a simulator that pretends to be the sensor board. Getting this working first shows your laptop is set up correctly, before you touch any hardware. It takes about 30 minutes.
PowerShell is a window where you type commands. You will use it a lot. Whenever this guide shows a grey box like this:
uv --versionclick in the PowerShell window, type the command exactly (or copy it and right-click to paste), and press Enter.
To open PowerShell: press the Windows key, type powershell, and click Windows PowerShell.
In PowerShell, copy and paste this line, then press Enter:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"When it finishes, close PowerShell and open it again (new programs are only found in a new window). Check that it worked:
uv --versionYou should see something like uv 0.8.23. If you see "uv is not recognized", close PowerShell and open it again.
- Go to https://nodejs.org and download the version marked LTS.
- Run the installer and click Next through every page (the defaults are fine).
- Close PowerShell and open it again, then check:
node --version- Go to https://mosquitto.org/download/ and download the Windows 64-bit installer (
mosquitto-…-install-windows-x64.exe). - Run it. On the Choose Components page, untick "Service". SmartWatt starts Mosquitto itself, with its own settings, and the service would get in the way.
- Keep the install folder as
C:\Program Files\mosquitto. SmartWatt looks for it there.
Easy way: at the top of this GitHub page, click the green Code button, then Download ZIP. Open your Downloads folder, right-click the ZIP file, choose Extract All…, and extract it to somewhere simple such as Documents.
If you know Git: git clone the repository instead.
You now have a SmartWatt folder containing README.md, appliances.toml, and folders such as firmware and server.
Almost every command in this guide must run inside the SmartWatt folder.
- Open the SmartWatt folder in File Explorer (the one containing
README.md). - Click once on the address bar at the top (where the folder path is shown). It turns into editable text.
- Type
powershelland press Enter.
A PowerShell window opens in that folder: the line before the cursor ends with your SmartWatt folder's path. Do this whenever the guide says "open PowerShell in the SmartWatt folder".
In PowerShell in the SmartWatt folder, run these one at a time. Each can take a few minutes the first time.
uv sync --all-packagesThis downloads Python and every library SmartWatt's server uses. Type it exactly: --all-packages matters.
cd dashboard
npm install
npm run build
cd ..This builds the dashboard web page. cd dashboard goes into the dashboard folder, and cd .. comes back out. A yellow warning about "chunks larger than 500 kB" is normal.
SmartWatt needs two windows running at the same time. Each keeps running until you close it, so open a new PowerShell in the SmartWatt folder for each (repeat Step 1.6).
Window 1: the post office (Mosquitto):
powershell -ExecutionPolicy Bypass -File tools\run-broker.ps1It prints a few lines and then waits. Leave it open.
Window 2: the SmartWatt server:
powershell -ExecutionPolicy Bypass -File tools\run-server.ps1Wait until you see Application startup complete. If Windows asks whether to allow Python on networks, tick Private networks and click Allow. Leave it open.
Why the long command? Windows blocks script files by default.
-ExecutionPolicy Bypasslets this one script run, without changing any setting on your computer.
Window 3: the simulator:
uv run sim publish --scenario demo --nowNow open your web browser (Chrome, Edge, Firefox…) and go to:
You should see the Live screen come alive: power, cost, CO₂ and a chart, with a SIMULATED badge in the corner. The demo pretends to switch appliances on and off for about two minutes. Click through the Month, Appliances and Control screens too.
🎉 If you see this, your laptop is ready.
To stop everything, click each PowerShell window and press Ctrl + C, or just close the windows.
Tip: "Cost per hour" can read RM 0.00 early in the month. That is correct, not a bug: while your month's usage is still under Sarawak Energy's minimum charge, using a bit more doesn't change your bill.
Take your time here. Most problems later come from one wrong wire. Do all of Part 2 with nothing plugged into the wall.
- The AC adapter must output AC. This is the most common mistake: an AC-DC adapter looks identical and fails silently.
- Plug the adapter into the wall, with nothing connected to its output. Attach the barrel-jack screw-terminal adapter (item 11) to its plug.
- Set your multimeter to V~ (AC volts) and touch the probes to the two screw terminals. You should read 9 to 13 V. It reads higher than 9 because nothing is connected.
- Switch the multimeter to V⎓ (DC volts). You should read almost 0 V (below 0.5 V).
- If DC reads high and AC reads near zero, you have an AC-DC adapter. Stop and get the right one.
- Unplug the adapter again.
- Read the labels. The current sensors must say SCT-013-005 and SCT-013-020 (never 000). The chips must say MCP3208 and MCP6002.
- Match two 10 kΩ resistors. Set your multimeter to resistance (Ω) and measure all your 10 kΩ resistors. Pick the two closest to each other (ideally within 10 Ω). They form the 1.65 V middle reference, and a mismatch shifts every measurement. Use a third one for the voltage channel.
Push each chip into the breadboard so it straddles the centre gap, with its notch (or dot) pointing left. Pin 1 is then the bottom-left leg. Pins count left to right along the bottom row, then right to left along the top row:
MCP3208 (16 legs)
MCP6002 (8 legs)
A breadboard has long rails along its top and bottom edges. In this build:
| Rail | Use it for | Tip |
|---|---|---|
| Top red + rail | 3V3: 3.3 V power | |
| Top blue − rail | GND: ground | |
| Bottom red + rail | BIAS: the 1.65 V middle reference | Put a piece of tape on it and write "BIAS" |
If your rails have a break in the middle (a gap in the red/blue line), bridge each one with a short wire.
The ESP32 board stays off the breadboard. Connect it with female-to-male jumper wires. Its pins are labelled on the board (3V3, GND, 10, 11, 12, 13 …).
Make each connection below and tick it off. "→" means "one wire (or one component) from … to …".
Power
| ✔ | Connect | → | To |
|---|---|---|---|
| ☐ | ESP32 pin 3V3 | → | top 3V3 rail |
| ☐ | ESP32 pin GND | → | top GND rail |
The 1.65 V middle reference ("BIAS"): MCP6002
| ✔ | Connect | → | To |
|---|---|---|---|
| ☐ | MCP6002 pin 8 (VDD) | → | 3V3 rail |
| ☐ | MCP6002 pin 4 (VSS) | → | GND rail |
| ☐ | a 100 nF capacitor | between | MCP6002 pin 8 and pin 4 (keep it right next to the chip) |
| ☐ | a matched 10 kΩ | → | from the 3V3 rail to an empty column. Call that column DIV |
| ☐ | the other matched 10 kΩ | → | from DIV to the GND rail |
| ☐ | MCP6002 pin 3 (IN+ A) | → | DIV |
| ☐ | MCP6002 pin 2 (IN− A) | → | MCP6002 pin 1 (OUT A) |
| ☐ | MCP6002 pin 1 (OUT A) | → | the BIAS rail |
| ☐ | MCP6002 pin 5 (IN+ B) | → | DIV (this ties off the unused half of the chip) |
| ☐ | MCP6002 pin 6 (IN− B) | → | MCP6002 pin 7 (OUT B) |
The ADC: MCP3208
| ✔ | Connect | → | To |
|---|---|---|---|
| ☐ | MCP3208 pin 16 (VDD) and pin 15 (VREF) | → | 3V3 rail |
| ☐ | MCP3208 pin 14 (AGND) and pin 9 (DGND) | → | GND rail |
| ☐ | a 100 nF capacitor | between | MCP3208 pin 16 and pin 14 (right next to the chip) |
| ☐ | MCP3208 pin 13 (CLK) | → | ESP32 pin 12 |
| ☐ | MCP3208 pin 12 (DOUT) | → | ESP32 pin 13 |
| ☐ | MCP3208 pin 11 (DIN) | → | ESP32 pin 11 |
| ☐ | MCP3208 pin 10 (CS) | → | ESP32 pin 10 |
Voltage input: the AC adapter → MCP3208 channel 0. Your barrel-jack adapter has two screw terminals. Call them A and B (either way round is fine, and Part 5 tells you if they need swapping).
| ✔ | Connect | → | To |
|---|---|---|---|
| ☐ | terminal A | → | a 10 kΩ resistor → an empty column. Call it VD |
| ☐ | VD | → | an 820 Ω resistor → the BIAS rail |
| ☐ | VD | → | an 820 Ω resistor → an empty column. Call it N0 |
| ☐ | N0 | → | a 100 nF capacitor → the BIAS rail |
| ☐ | N0 | → | MCP3208 pin 1 (CH0) |
| ☐ | terminal B | → | the BIAS rail |
Current input, small loads: SCT-013-005 → channel 1. Plug the sensor into a 3.5 mm breakout board. Use its tip (T) and sleeve (S) terminals, and leave the ring (R) unused.
| ✔ | Connect | → | To |
|---|---|---|---|
| ☐ | sleeve | → | the BIAS rail |
| ☐ | tip | → | an 820 Ω resistor → an empty column. Call it N1 |
| ☐ | N1 | → | a 100 nF capacitor → the BIAS rail |
| ☐ | N1 | → | MCP3208 pin 2 (CH1) |
Current input, big loads: SCT-013-020 → channel 2. Same again, with the second breakout board:
| ✔ | Connect | → | To |
|---|---|---|---|
| ☐ | sleeve | → | the BIAS rail |
| ☐ | tip | → | an 820 Ω resistor → an empty column. Call it N2 |
| ☐ | N2 | → | a 100 nF capacitor → the BIAS rail |
| ☐ | N2 | → | MCP3208 pin 3 (CH2) |
Two things people get wrong:
- The sensors' sleeves, adapter terminal B, and the three filter capacitors on N0, N1 and N2 all go to BIAS, not GND. That is what lets the board measure both halves of the AC wave.
- Never put a capacitor directly between BIAS and GND. It can make the reference unstable.
You should have used exactly 3 × 10 kΩ, 4 × 820 Ω and 5 × 100 nF. MCP3208 pins 4 to 8 stay empty.
Optional, lowers noise: keep the wires to the chips short, keep the sensor wires away from the USB cable, and tape a sheet of kitchen foil under the breadboard (the breadboard's backing must be intact, so the foil can't touch any metal clip), connected with one wire to the GND rail.
With nothing plugged in (no USB, no adapter, no sensors), set your multimeter to continuity (it beeps when two points are connected):
- 3V3 rail to GND rail: no beep (a beep means a short. Find it before continuing).
- BIAS rail to GND rail: no beep. BIAS rail to 3V3 rail: no beep.
- Each of N0, N1 and N2 beeps to its MCP3208 pin (1, 2, 3) and to nothing else.
Then plug only the USB cable (laptop ↔ ESP32, into the port labelled UART or COM). Set the multimeter to DC volts and measure against the GND rail:
- 3V3 rail: about 3.3 V.
- BIAS rail: half of the 3V3 reading. For example 1.650 V if 3V3 reads 3.300 V. It should be within about ±0.005 V of exactly half. If it's further off, swap in a better-matched pair of 10 kΩ resistors.
Unplug the USB again. Well done: the hard part is over.
Both current sensors clip around the live wire alone. Around a whole cable they read nothing, because the current going out on the live wire and the current coming back on the neutral wire cancel each other out. There are two ways to reach the live wire.
With a line splitter (item 17): nothing to do here. The splitter brings the live wire out as a loop, and you clip the sensors around it in Step 5.1.
Without a line splitter: strip back the outer jacket of the power strip's own cable and clip the sensors around the live wire inside.
- Use a power strip you can dedicate to SmartWatt. Choose a straight stretch of its cable, well away from the plug and from the strip itself.
- Cut only the outer jacket, over about 10 cm, and peel it back. Work slowly with a cable-sheath stripper or a sharp knife, and cut shallow: the blade must never reach the wires inside.
- Inspect the three wires inside. Their own coloured insulation must be completely undamaged: no cuts, no nicks, no copper showing. If you find any damage, don't use this power strip.
- Separate the brown wire (live) from the blue (neutral) and green-and-yellow (earth) wires. Never cut or strip any of the three wires themselves. You can bundle the blue and green-and-yellow wires together with tape or a plastic sleeve to keep them out of the way, as in the photo below.
- Wrap insulating tape around each cut end of the jacket so it can't slide back or peel further.
- Label the power strip "SmartWatt only" and don't return it to everyday use.
In Step 5.1 you clip both sensors around the brown wire alone, like this:
The jacket was the cable's protective layer. Without it, only the wires' thin insulation separates you from 240 V, so every time you use it:
- Keep the stripped section flat on a dry table. Don't bend it sharply, pull on it, step on it, or let it rub on sharp edges.
- Look over the three wires before plugging in. If you see any damage, stop and don't use it.
- Clip, move or remove the sensors only while the power strip is unplugged.
- Always plug the power strip in through the RCD.
The sensor board talks to your laptop over the laptop's own Wi-Fi hotspot, and gets the time from the laptop. The board refuses to send anything until it knows the correct time, so this part matters.
- Press the Windows key, type
powershell, then right-click Windows PowerShell and choose Run as administrator. Click Yes. - Copy and paste this whole block and press Enter:
# Stop Windows' own Mosquitto service, if it was installed, so it doesn't block SmartWatt's
Stop-Service mosquitto -ErrorAction SilentlyContinue; Set-Service mosquitto -StartupType Disabled -ErrorAction SilentlyContinue
# Turn the laptop into a time server for the sensor board
Set-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpServer" -Name Enabled -Value 1
Set-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config" -Name AnnounceFlags -Value 5
Set-Service w32time -StartupType Automatic; Restart-Service w32time
# Let the sensor board and smart plugs through the firewall
New-NetFirewallRule -DisplayName "SmartWatt MQTT" -Direction Inbound -Protocol TCP -LocalPort 1883 -Action Allow
New-NetFirewallRule -DisplayName "SmartWatt time" -Direction Inbound -Protocol UDP -LocalPort 123 -Action Allow- Close the administrator window. You never need to do this again.
- Open Settings → Network & internet → Mobile hotspot.
- Click Edit (or Properties) and set:
- Network name: anything, e.g.
SmartWatt-Hotspot. Write it down. - Network password: at least 8 characters. Write it down.
- Network band: 2.4 GHz. The ESP32 cannot see 5 GHz networks.
- Click Save.
- Network name: anything, e.g.
- Turn Power saving off (if your Windows shows it). Otherwise Windows switches the hotspot off when nothing is connected.
- Switch Mobile hotspot on.
Windows only allows a hotspot while the laptop itself has an internet connection (Wi-Fi or cable), so stay connected to your home network.
Your laptop's address on its own hotspot is always 192.168.137.1, and SmartWatt is already set up for that.
In PowerShell (anywhere):
uv tool install platformioClose PowerShell, open it again, and check:
pio --version-
Open PowerShell in the SmartWatt folder and run:
notepad firmware\src\main.cpp
-
In Notepad press Ctrl + F, type
CHANGE ME, and press Enter. You will find:"YOUR_HOTSPOT_NAME", // CHANGE ME: the hotspot's network name "YOUR_HOTSPOT_PASSWORD", // CHANGE ME: the hotspot's network password
-
Replace
YOUR_HOTSPOT_NAMEandYOUR_HOTSPOT_PASSWORDwith the name and password from Step 3.2. Keep the quote marks and the comma. For example:"SmartWatt-Hotspot", // CHANGE ME: the hotspot's network name "my-secret-pass", // CHANGE ME: the hotspot's network password
-
Save (Ctrl + S) and close Notepad. Don't change anything else in the file.
If you ever share your copy of SmartWatt online, put the placeholders back first. Your password is in this file.
- Plug the USB cable into the ESP32's port labelled UART (or COM), not the one labelled USB, and into your laptop.
- Open Device Manager (right-click the Start button → Device Manager) and expand Ports (COM & LPT). You should see something like "Silicon Labs CP210x … (COM3)" or "USB-SERIAL CH340 (COM5)".
- Nothing there? Try another cable (it may be charge-only). Otherwise install the driver for the USB chip printed near the UART port: search for "CP210x driver" (Silicon Labs) or "CH340 driver" (WCH).
In PowerShell in the SmartWatt folder:
cd firmware
pio run -e esp32-s3 -t uploadThe first time, PlatformIO downloads its tools for the ESP32. That can take 10-20 minutes. It ends with [SUCCESS].
If it says "Failed to connect": hold down the BOOT button on the board, press and release RST, release BOOT, and run the command again.
Still in the firmware folder:
pio device monitor -e esp32-s3 --echoThis window shows the board's messages. You should see:
SmartWatt bring-up
Calibration: none stored in NVS -- using compile-time defaults, ...
Fingerprints: no usable table on LittleFS -- events will classify as unknown_N only
Clock: UNSET at boot ...
Once it has joined the hotspot and reached the laptop's broker (which must be running: Step 1.8, Window 1), it says:
Broker connected: ...
That's all for now. Measurements only start in Part 5, once the AC adapter is connected, because the board measures in step with the mains voltage. To leave the monitor, press Ctrl + C. To go back to the SmartWatt folder, type cd ...
Only one program can use the board's USB connection at a time. Close the monitor (Ctrl + C) before uploading.
Calibration teaches SmartWatt the exact behaviour of your adapter and sensors, so that its volts, amps and watts are right. Calibrate before you train (Part 8). If you re-calibrate later, you have to train again.
Connect, in this order: wall socket → RCD → plug-in energy meter → line splitter (if you have one) → power strip. Leave the RCD unplugged from the wall for now.
- Clip both current sensors around the live wire, facing the same way:
- With a line splitter: around its ×1 loop. Leave the ×10 loop empty.
- Without one: around the brown wire of the power strip's cable that you prepared in Step 2.6. Never around the whole cable.
- Plug the RCD into the wall and press its TEST button: it must switch off. Reset it.
- Plug the sensors' 3.5 mm plugs into their breakout boards (005 → channel 1 board, 020 → channel 2 board).
- Plug the AC adapter into an ordinary wall socket and connect its barrel plug to the breakout (terminals A/B).
- Plug the ESP32 into the laptop's USB.
Start Window 1 (broker) and Window 2 (server) as in Step 1.8. Don't run the simulator now: the real board is the data source. Then, in a PowerShell in the SmartWatt folder:
cd firmware
pio device monitor -e esp32-s3 --echoNow a status line appears every second:
Vrms= 283.41 Irms= 0.0123 P= 1.02 Q1= 0.31 D= 0.20 PF=0.100 f=50.004 range=low overruns=0 worst=40us vclip=0
vclipmust stay 0. If it keeps rising, the voltage signal is too big. Re-check the voltage wiring (10 kΩ / 820 Ω).- The numbers aren't correct yet (Vrms may be far from 240). That's what calibration fixes.
- Open http://127.0.0.1:8000: the Live screen should now show data with a LIVE badge. If the monitor instead repeats
WITHHOLDING PUBLISH: clock NOT set, the board can't get the time from the laptop (see Troubleshooting).
Plug a heater, kettle or ordinary incandescent lamp into the power strip and switch it on. P must be positive.
- Negative P on both small and big loads: unplug the AC adapter from the wall, swap the wires on terminals A and B, and plug it back.
- Negative only on big loads (
range=high) or only on small ones (range=low): turn the matching current sensor around on the wire.
Type this into the monitor window and press Enter:
CAL 0.2835 0.0040283 0.016113 0
It replies CAL: stored in NVS and applied. These are the calculated starting values for this circuit. Now you refine them against your plug-in energy meter.
For each step below you average 30 seconds of readings:
-
Set up the load described in the table.
-
Start a logging monitor (inside the
firmwarefolder):pio device monitor -e esp32-s3 --echo -f log2file
-
Wait 30 seconds without touching anything, and note what your energy meter shows (volts, amps).
-
Press Ctrl + C, then run these two lines to average what was logged:
$log = (Get-ChildItem logs\device-monitor-*.log | Sort-Object LastWriteTime)[-1].FullName uv run python tools\serial_stats.py $log --skip 5
It prints the average of each reading, e.g.
Vrms: mean 283.4100, plus aphi1 = ...line.
Work out the new values like this ("meter" = your energy meter, "board" = the mean from serial_stats):
| What | Load on the strip | New value |
|---|---|---|
| v_cal (voltage) | anything, even nothing | 0.2835 × meter volts ÷ board Vrms |
| i_cal_low (small loads) | a steady load of about 100-1000 W, e.g. an incandescent lamp or a small heater. The status line must say range=low |
0.0040283 × meter amps ÷ board Irms |
| i_cal_high (big loads) | a kettle with water in it (stay with it, switch it off after ~30 s). The status line must say range=high |
0.016113 × meter amps ÷ board Irms |
Send all three in one line (the last number stays 0 for now), for example:
CAL 0.2217 0.0041050 0.016400 0
With the new values stored, put a purely resistive load on the strip (an incandescent lamp or a heater, not a fan and not anything with a charger), log 30 seconds again, and run serial_stats again. Copy the number after phi1 = (in radians, e.g. +0.048712) as the last value:
CAL 0.2217 0.0041050 0.016400 0.048712
(If you repeat this step later, the new last value is the old one plus the new phi1.)
- Type
CAL?and press Enter. It prints the values in use and says from NVS (stored on the board). - Unplug the USB and plug it back. The first lines must say
Calibration: loaded from NVSwith your values. - With the lamp on, the board's Vrms, Irms and P should now agree closely with your energy meter.
Calibration is done. Don't change it again unless you change the hardware. If you do, re-train (Part 8).
Skip this part if you don't have Tasmota smart plugs. SmartWatt still recognises, measures and costs everything. It just can't switch things for you.
For each plug:
- Plug it into a wall socket (nothing plugged into it).
- On your phone or laptop, join the Wi-Fi network the plug creates (named like
tasmota-XXXXXX), then open http://192.168.4.1 in a browser. - Enter your hotspot's name and password (Step 3.2) and save. The plug restarts and joins the hotspot.
- Find the plug's new address: in Settings → Network & internet → Mobile hotspot, it is listed under connected devices (e.g.
192.168.137.123). Open that address in your laptop's browser. - Go to Configuration → Configure MQTT and set:
- Host:
192.168.137.1 - Port:
1883 - Topic: a short name for this plug, e.g.
plug_fan. Use only letters, digits and_. Write it down: it goes intoappliances.tomlin Part 7. - Save.
- Host:
- Go to Tools → Console and type these, pressing Enter after each:
(
PowerOnState 0 TelePeriod 10 Timezone 8PowerOnState 0keeps the plug off after a power cut, which is important for heaters.Timezone 8is Malaysia's time zone.)
Test it. With the broker running (Window 1), in a PowerShell window:
& "C:\Program Files\mosquitto\mosquitto_pub.exe" -h 127.0.0.1 -t cmnd/plug_fan/POWER -m ON
& "C:\Program Files\mosquitto\mosquitto_pub.exe" -h 127.0.0.1 -t cmnd/plug_fan/POWER -m OFF(Replace plug_fan with your Topic.) The plug should click on, then off.
All your appliances are listed in one file: appliances.toml, in the SmartWatt folder. It comes with five example appliances (kettle, desk fan, incandescent lamp, LED bulb, laptop charger). Replace them with your own.
Open it:
notepad appliances.tomlEach appliance is one block like this:
[[appliance]]
id = "rice_cooker"
name = "Rice cooker"
plug = "plug_rice"
protected = false
heating = true
train = true
left_on_rule = false
standby_rule = false| Line | Meaning |
|---|---|
id |
A short code name: lowercase letters, digits and _, starting with a letter, up to 23 characters, no spaces. Choose it once and don't change it later: your training data is saved under this name |
name |
What the dashboard shows. Anything you like |
plug |
The smart plug's Topic from Part 6, or "" if it isn't on a smart plug. One plug per appliance |
protected |
true = SmartWatt will never switch it off, not even if you press the button. Use it for the fridge, router, medical equipment… |
heating |
true = SmartWatt may switch it off but never on. Use it for anything that heats: kettle, iron, heater, rice cooker |
train |
true = the Setup screen asks you to teach SmartWatt this appliance |
left_on_rule |
true = if it stays on for 4 hours, SmartWatt warns you and switches it off after 60 seconds unless you press Cancel. Needs a plug |
standby_rule |
true = if it sits at a small standby power (1-12 W) for 6 hours, the same warning and switch-off. Needs a plug |
Rules for writing the file:
- Each appliance starts with
[[appliance]], with two square brackets on each side. - Text goes in
"quote marks".trueandfalseare lowercase, without quote marks. - To add an appliance, copy a whole block and change it. To remove one, delete its whole block.
- Lines starting with
#are notes, which SmartWatt ignores.
Save the file, then restart the server (click Window 2, press Ctrl + C, and run the run-server.ps1 command again). If there's a mistake, the server will not start: its window names the appliance and the line to fix. For example:
appliances.toml cannot be used (1 problem):
- appliance #2 ("rice_cooker") is missing its "heating" line: heating = true for a kettle, ...
Choosing what to train: pick appliances that switch on and off as a clear step of at least ~10 W (lamps, fans, kettles, heaters, TVs, chargers of laptops, rice cookers…). Appliances with very different power are the easiest to tell apart. Two appliances with nearly identical power and behaviour (two identical lamps, say) will look the same to SmartWatt.
Training means switching each appliance on and off while SmartWatt records its fingerprint. You do it once, on the Setup screen. Allow about 10 minutes per appliance.
Before you start:
- Calibration (Part 5) is finished.
- Windows 1 (broker) and 2 (server) are running, the hotspot is on, and the board is connected and publishing (the Live screen shows LIVE, not SIMULATED).
- Your appliances are listed in
appliances.tomlwithtrain = true(Part 7). - Nothing else on the monitored power strip is switching on and off by itself (a fridge, say), so it doesn't confuse the recording.
Open http://127.0.0.1:8000 and click Setup.
Switch everything on the strip off. Click Record baseline (it shows the quiet circuit's power), then Next step.
For each appliance in the list, with nothing else running:
- Switch the appliance ON by hand.
- Wait about 5 seconds, then click Capture ON on that appliance's row.
- Check the message: it shows the power step it recorded (e.g.
Bound event: 41.2 W · clean edge). If that matches what you did, click Confirm capture. If not (wrong size, or you switched something else), just switch again and capture again without confirming. - Switch it OFF, wait 5 seconds, click Capture OFF, check, Confirm capture.
Repeat until the table shows 5 / 5 ON and 5 / 5 OFF for every appliance, then click Next step.
If a capture says "no edge detected", "never settled" or "overlapped another edge", switch the appliance again (leave a few seconds between switches) and capture again.
Real life has several appliances on at once, so SmartWatt must learn that too.
- Switch another trained appliance on and leave it running, e.g. the desk fan. A steady, medium one is best. Avoid the kettle here, because it drowns out small appliances.
- Type its id (e.g.
desk_fan) into the Concurrent appliance(s) box. - Capture each of the other appliances ON and OFF, exactly as in Step 8.2, until each shows 5 / 5 again. (To capture the running appliance itself, switch it off and run a different one in the background, and change the box to match.)
Click Next step.
SmartWatt has saved everything you taught it in firmware\data\fingerprints.csv. Copy it onto the board:
-
Close the serial monitor if it's open (Ctrl + C).
-
In PowerShell in the SmartWatt folder:
cd firmware pio run -e esp32-s3 -t uploadfs cd ..
-
The board restarts. Wait about 20 seconds until it's publishing again.
-
On the Setup screen, the line "Table id: device reports …, expected …" must show the same code twice. Then click Next step.
(Your calibration is kept: uploadfs only replaces the fingerprints.)
- Choose an appliance in Expected class.
- Switch that appliance on, wait 5 seconds, click Verify.
- Switch it off again.
SmartWatt must answer correctly three times in a row. A wrong answer sends you back to capturing, and the captures you already have are kept, so you only add more. Vary which appliance you test.
When it says verification passed, SmartWatt knows your appliances. 🎉
Add it to appliances.toml with train = true, restart the server, open Setup and go through the steps again. Appliances you already trained are already complete, so you only capture the new one. Then do Step 8.4 and 8.5 again.
Delete the file firmware\data\fingerprints.csv, restart the server, and start Setup from the beginning.
Advanced: automatic training with smart plugs
If every appliance you want to train sits on its own Tasmota plug, a script can do the switching for you: firmware\tools\train_plugs.py. Each --plug is an appliance id from appliances.toml, =, and the IP address of its plug:
uv run python firmware/tools/train_plugs.py capture --plug desk_fan=192.168.137.101 --plug led_bulb=192.168.137.102
cd firmware; pio run -e esp32-s3 -t uploadfs; cd ..
uv run python firmware/tools/train_plugs.py verify --plug desk_fan=192.168.137.101 --plug led_bulb=192.168.137.102It refuses to touch protected or heating appliances. The server and broker must be running.
Each time you want SmartWatt to record:
- Turn on the laptop's Mobile hotspot.
- Open PowerShell in the SmartWatt folder → Window 1:
powershell -ExecutionPolicy Bypass -File tools\run-broker.ps1 - Another PowerShell in the SmartWatt folder → Window 2:
powershell -ExecutionPolicy Bypass -File tools\run-server.ps1 - Plug the sensor board into the laptop's USB.
- Open http://127.0.0.1:8000.
SmartWatt only records while the laptop, the hotspot and both windows are running. Everything it records is saved in smartwatt.db in the SmartWatt folder.
The screens:
| Screen | What it shows |
|---|---|
| Live | Power right now, cost per hour, CO₂ per hour, and a chart of the last 15 minutes split by appliance. Power SmartWatt can't name is shown as unidentified |
| Month | Your bill so far this month and the projected bill. The cliff gauge shows how close you are to the next Tariff D band, where the whole month's units become more expensive. A "what if" tool lets you see the effect of using an appliance less |
| Appliances | Energy and cost per appliance, today, this week and this month |
| Control | Switch appliances that have a smart plug. Protected appliances refuse, and heating appliances can only be switched off. Automatic switch-offs appear here with a Cancel button |
| Setup | Training (Part 8) |
Want to see the automatic switch-off without waiting 4 hours? Before starting Window 2, type $env:SMARTWATT_RULES_DEMO = "1" in that window. The rules then react after 30 seconds instead of 4 hours. Don't leave it like that for normal use.
| Problem | What to do |
|---|---|
"… is not recognized as the name of a cmdlet" (for uv, node, pio) |
Close PowerShell and open it again after installing. If it persists, re-run that program's installer |
| "running scripts is disabled on this system" | Use the full powershell -ExecutionPolicy Bypass -File tools\... command exactly as shown |
| Window 1 (broker) says "Only one usage of each socket address" | Windows' Mosquitto service is running. Do Step 3.1 |
Window 2 (server) stops with ApplianceFileError |
There's a mistake in appliances.toml. The message says which appliance and which line. Fix it and start again |
| Browser shows "Dashboard not built" | Do the npm install / npm run build lines in Step 1.7 |
| Dashboard says "Waiting for telemetry" | No data is arriving. Is the board (or the simulator) running? Is Window 1 open? |
Board monitor repeats WITHHOLDING PUBLISH: clock NOT set |
The board can't get the time from the laptop. Check the hotspot is on, re-do Step 3.1, and check the address in main.cpp is 192.168.137.1 |
Board monitor says MQTT publish skipped … no broker |
Window 1 isn't running, or the firewall rule from Step 3.1 is missing |
| Board never joins the hotspot | The hotspot name or password in main.cpp is wrong (Step 4.2, then upload again), or the hotspot is set to 5 GHz |
| No COM port in Device Manager | Use the port labelled UART on the board, try a different cable (it must carry data), install the CP210x/CH340 driver |
| Upload says "Failed to connect" | Close the serial monitor. Hold BOOT, tap RST, release BOOT, upload again |
| The board restarts whenever you open the monitor | Open it with pio device monitor -e esp32-s3 --echo --dtr 0 --rts 0 |
| No status lines appear at all | The AC adapter isn't connected or powered. The board measures in step with the mains voltage, so it needs the adapter |
vclip keeps increasing |
The voltage signal is too big: check the 10 kΩ / 820 Ω voltage wiring (Part 2) |
| P is negative | Step 5.3 (swap adapter wires, or turn a sensor around) |
| Irms stays near 0 with a load switched on | The sensors must go around the live wire alone, not the whole cable (Step 2.6). Check that each sensor is clicked fully shut and its plug is pushed fully into its breakout board |
Everything shows as unknown_1, unknown_2… |
Not trained yet, the fingerprints weren't uploaded (Step 8.4), you re-calibrated after training, or the appliance changes power by less than ~8 W |
| The hotspot keeps switching itself off | Turn off Power saving in the Mobile hotspot settings |
| Cost per hour shows RM 0.00 | Normal early in the month (see the tip at the end of Part 1) |
SmartWatt is built for Sarawak, Malaysia:
- Bill: Sarawak Energy's domestic Tariff D in
tariff/smartwatt_tariff/config/tariff.toml. Every figure has a source and date. Check them against your latest bill, because tariffs change. - CO₂: Malaysian grid emission factors in
tariff/smartwatt_tariff/config/carbon.toml. - Time zone: fixed at Malaysia time (UTC+8, Asia/Kuching). Changing it means changing the code.
For another region you would change those files to your own tariff and emission factor. The appliance recognition itself works anywhere with 220-240 V, 50 Hz mains.
Project layout, tests and design notes
| Folder | What's in it |
|---|---|
contract/ |
JSON Schemas for the four MQTT payloads (telemetry, event, command, fingerprints). Everything else is derived from or validated against them |
firmware/ |
ESP32-S3 firmware (PlatformIO, Arduino). One library per signal-chain stage in lib/: sample_source → sampler → metrics → events → features → classify (z-scored k-NN with rejection) → track → emit → net. Bench scripts in tools/ |
server/ |
FastAPI + paho-mqtt + SQLite: ingest, storage, ledger, the two-stage rules engine, and the safety gate, the only path a plug command can take |
tariff/ |
Pure bill / tariff-cliff / carbon maths, driven by the TOML files in config/ |
sim/ |
Scenario-driven simulator. The server cannot tell it from the real device |
analysis/ |
Offline measurement harness (cross-validation, confusion matrix, rejection threshold) |
dashboard/ |
React 19 + Vite + Tailwind 4 dashboard, built into server/static/ |
appliances.toml |
The household's appliance list, read by the server at startup |
uv sync --all-packages # not bare `uv sync`: that skips the members' dev dependencies
uv run pytest # all Python suites
cd dashboard; npm test; cd .. # dashboard
cd firmware; pio test -e native; cd .. # firmware logic, on the PC, no board needed
cd firmware; pio run -e esp32-s3; cd .. # device buildThe native firmware tests run the whole signal chain against analytic waveform fixtures in firmware/test/fixtures/. Those files are byte-exact (see .gitattributes).
The device build is effectively gnu++11 (the Arduino-ESP32 build appends -std=gnu++11), while native tests use C++17. Anything under firmware/lib/ that the device links must stay C++11-compatible, so always run pio run -e esp32-s3 too. The loop task has an 8 KB stack, and frames over 4 KB are a compile error.
The server tests always run against server/tests/example_appliances.toml, never against the root appliances.toml, so editing your own appliance list cannot break them.
The server is configured with environment variables (server/smartwatt_server/config.py): SMARTWATT_BROKER, SMARTWATT_BROKER_PORT, SMARTWATT_DB, SMARTWATT_APPLIANCES, SMARTWATT_FINGERPRINTS, SMARTWATT_TRAINED_CLASSES (overrides the train flags), SMARTWATT_RULES_DEMO, SMARTWATT_HZ_RETENTION_S, SMARTWATT_ROLLUP_INTERVAL_S.
uv run sim publish --scenario demo --now # also: record, replay, waveform, validateWithout --now, runs are stamped from a fixed date so recordings and tests are reproducible.



.png)
.png)


