No kWh Meter? Estimate Daily Electricity Costs from Current
A current sensor only gives you amps, not energy usage, but if you can accept assumptions about voltage and power factor, you can turn those readings into daily electricity cost estimates useful for making decisions. Just know what you are estimating, and avoid comparing it directly with the bill.
A system I maintain happens to have a current measurement module connected to several CT sensor channels, with readings available directly in amps over Modbus. But the system owner's question was, "How many baht a day does this machine use in electricity?" Getting from amps to baht takes several steps. So I built an electricity cost estimator using the data I already had, and ran into three or four traps along the way that can quietly distort the numbers if you are not careful. In this article, I walk through everything from the formula to the summary table that keeps the web page responsive.
A CT sensor measures only current. It does not measure voltage or power factor. I have to assume those two values, so the resulting numbers are always estimates, and they also exclude demand charges (based on the peak demand of the entire factory) and monthly service charges, which are added to the actual bill. Use these numbers to compare which days use more electricity, or whether changes to machine operation actually save energy. Do not use them to dispute the utility bill baht for baht.
From Amps to Kilowatts: Two Values You Have to Assume
For a balanced three-phase load, real power is calculated using the standard formula.
import math
SQRT3 = math.sqrt(3) # 1.7320508...
V_LINE = 380.0 # แรงดันระหว่างสาย สมมุติค่าคงที่
PF = 0.85 # ตัวประกอบกำลัง สมมุติตามชนิดโหลด
def kw_from_amp(amp: float) -> float:
"""กระแสหนึ่งค่า -> กำลังไฟฟ้าเป็นกิโลวัตต์ (ค่าประมาณ)"""
return SQRT3 * V_LINE * amp * PF / 1000.0
# ตัวอย่าง: คอมเพรสเซอร์ที่อ่านได้ 36.6 A
print(round(kw_from_amp(36.6), 2)) # ~20.5 kW
The two values I assume are voltage and power factor. Actual voltage can vary quite a bit with the time of day and distance from the transformer, while a motor's power factor changes with its load. It is very different at idle and at full load. I chose fixed values because I wanted numbers I could compare across days, not meter-level accuracy. If you want greater accuracy, the only way is to switch to a meter that also measures voltage, which is another job in itself.
The Sampling Interval Is Part of the Formula: Do Not Change It Casually
This is the trap I most want to warn about. Power is measured in kilowatts, while energy is measured in kilowatt-hours. Converting the former to the latter means multiplying by time, and in a system that takes readings at intervals like this, "time" is the gap between two readings.
I configured the reader to collect current readings every one second, then wrote the simplest possible energy calculation: one sample equals one second, so multiply by the power and divide by three thousand six hundred to get the energy usage. This worked well until someone slowed down the sampling interval to reduce the load on the bus, and the electricity cost figures suddenly plummeted without anyone knowing why.
Leave a prominent comment right where the sampling interval is configured, explaining that this value is tied to the energy calculation. A better approach is to avoid assuming how far apart the samples are: store the actual timestamp and calculate the time difference between consecutive rows. The latter costs a little more at query time, but handles changes to the sampling interval and periods when the reader stalls.
def kwh_between(rows):
"""rows = [(ts_unix, amp), ...] เรียงตามเวลา
คิดพลังงานจากส่วนต่างเวลาจริง ไม่สมมุติว่าห่างกันกี่วินาที"""
MAX_GAP = 15 # ห่างเกินนี้ถือว่าข้อมูลขาด ไม่ใช่เครื่องเดินต่อเนื่อง
total = 0.0
for (t0, a0), (t1, _a1) in zip(rows, rows[1:]):
dt = t1 - t0
if dt <= 0 or dt > MAX_GAP:
continue # ข้ามช่วงที่ข้อมูลขาด
total += kw_from_amp(a0) * dt / 3600.0
return total
Skip Outages Instead of Connecting the Dots
A related issue comes up during a power outage or when a signal cable disconnects: the reader stops writing data altogether. When power returns and a new row appears, if the energy calculation subtracts the previous row's timestamp from the new one, it might get a two-hour interval. It then multiplies that by the power reading from before the outage, recording the machine as running at full load throughout two hours with no power at all.
The MAX_GAP value in the code above guards against this. I set it to fifteen seconds for data sampled every second, allowing brief interruptions but treating anything longer as missing data. I also added another layer to the display: if the latest data is more than thirty seconds old, show a warning banner saying "Power outage or signal cable disconnected," and display dashes instead of leaving the old values on screen. Stale values left on screen are one of the easiest ways to mislead the person watching.
Separate Peak and Off-Peak with Epoch Arithmetic
Time-of-use electricity tariffs charge different rates for peak and off-peak periods. For the customer category I manage, peak hours are Monday through Friday, from 9 in the morning to 10 at night. Everything else is off-peak, including all of Saturday and Sunday.
At first, I planned to fetch an entire month's raw data, loop through it in PHP, and use date functions to classify each row. That was very slow because the data was recorded every second. I eventually moved this into SQL using pure epoch arithmetic, without touching date functions or relying on the server's timezone settings at all. A useful side effect is that moving to another machine does not change the numbers.
-- เลข 25200 = 7 ชั่วโมง เลื่อน epoch ให้เป็นเวลาท้องถิ่นก่อนหารวัน
-- วันที่แบบเลข : FLOOR((ts_unix + 25200) / 86400)
-- วันในสัปดาห์ : (วันที่แบบเลข + 4) % 7 -> 0=อาทิตย์ ... 6=เสาร์
-- ชั่วโมง : FLOOR(MOD(ts_unix + 25200, 86400) / 3600)
SELECT
SUM(CASE WHEN (FLOOR((ts_unix + 25200) / 86400) + 4) % 7 BETWEEN 1 AND 5
AND FLOOR(MOD(ts_unix + 25200, 86400) / 3600) BETWEEN 9 AND 21
AND FLOOR((ts_unix + 25200) / 86400) NOT IN (:holiday_days)
THEN kwh ELSE 0 END) AS kwh_peak,
SUM(CASE WHEN (FLOOR((ts_unix + 25200) / 86400) + 4) % 7 BETWEEN 1 AND 5
AND FLOOR(MOD(ts_unix + 25200, 86400) / 3600) BETWEEN 9 AND 21
AND FLOOR((ts_unix + 25200) / 86400) NOT IN (:holiday_days)
THEN 0 ELSE kwh END) AS kwh_offpeak
FROM energy_seconds
WHERE ts_unix >= :from_ts AND ts_unix < :to_ts;
The + 4 in the day-of-week formula comes from the fact that day zero of the epoch was a Thursday. Before using it, check it against two or three dates whose weekdays you know for sure. Being off by one day can distort an entire month's electricity costs without looking obviously wrong.
Public Holidays Are Off-Peak All Day
The :holiday_days variable above is the list of holidays the utility designates as off-peak all day. It does not exactly match the general public holiday calendar. Some public holidays still have normal peak rates, and not every substitute holiday qualifies. So I keep an explicit list in the configuration file and set myself a reminder to update it at the start of every year.
{
"updated_at": "2026-10-29T06:15:02+07:00",
"rate": {
"peak": 4.1839,
"offpeak": 2.6037,
"ft": 0.1623,
"vat": 0.07
},
"peak_window": { "days": [1, 2, 3, 4, 5], "hour_from": 9, "hour_to": 21 },
"holidays": ["2026-01-01", "2026-04-13", "2026-04-14", "2026-04-15",
"2026-05-01", "2026-12-05", "2026-12-10", "2026-12-31"],
"note": "รายการวันหยุดต้องอัปเดตทุกต้นปี ค่า ft มาจากตัวดึงอัตโนมัติรายสัปดาห์"
}
The only value in this file that the automatic fetcher can overwrite is ft. I require the peak rate, off-peak rate, and holiday list to be edited manually. Those last three rarely change, but affect an entire month's figures. If I let a script scrape them from a web page and the page changes one day, the script could silently overwrite them with empty values, making electricity costs across the system drop to zero without anyone noticing.
Ft Changes by Billing Period: Use cron to Fetch It Automatically
Ft is announced for set periods. It does not change often, but forgetting to update it makes the numbers wrong for the entire period. So I wrote a small fetcher that runs every week, reads the public electricity tariff announcement page, and writes to the configuration file only when the value actually changes.
import json, pathlib, re, urllib.request
CFG = pathlib.Path("/var/www/html/energy/rate_config.json")
PAGE = "https://www.example-utility.or.th/tariff/ft"
def fetch_ft():
html = urllib.request.urlopen(PAGE, timeout=30).read().decode("utf-8", "ignore")
m = re.search(r"Current Ft.*?<span>([-\d.]+)</span>", html, re.S)
if not m:
raise RuntimeError("หน้าเว็บเปลี่ยนรูปแบบ หา Ft ไม่เจอ")
return float(m.group(1))
cfg = json.loads(CFG.read_text(encoding="utf-8"))
new = fetch_ft()
old = cfg["rate"]["ft"]
if abs(new - old) < 1e-9:
print(f"[OK] Ft ยังเท่าเดิม {old}")
else:
cfg["rate"]["ft"] = new
CFG.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"[UPDATE] Ft {old} -> {new}")
# ดึงค่า Ft ทุกเช้าวันจันทร์ แล้วเก็บ log ไว้ดูย้อนหลัง
0 6 * * 1 /usr/bin/python3 /opt/energy/rate_update.py >> /var/log/energy/rate.log 2>&1
I use this configuration file as the single source of truth. Both the web interface on the on-site machine and the central system read the same file. When a rate changes, I update it in one place and I am done, without having to track down constants scattered across three places in the code and forget one of them.
A Daily Summary Table Made the Web Page Seven Times Faster
Initially, the monthly summary page calculated everything from the raw per-second data every time someone opened it. That meant waiting five seconds per click, which was unacceptable. So I created a daily summary table, had cron aggregate yesterday's totals every morning, and changed the web page to read from the summary table.
CREATE TABLE energy_day (
day_epoch INT NOT NULL, -- FLOOR((ts+25200)/86400) เวลาท้องถิ่น
channel SMALLINT NOT NULL, -- ช่อง CT / อุปกรณ์
kwh_peak DECIMAL(12,4) NOT NULL DEFAULT 0,
kwh_offpeak DECIMAL(12,4) NOT NULL DEFAULT 0,
samples INT NOT NULL DEFAULT 0, -- ไว้ดูว่าวันนั้นข้อมูลครบไหม
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (day_epoch, channel)
);
The samples column exists because a day with a half-day power outage and a day with complete data will have very different energy usage figures. Without storing the sample count, when someone asks, "Why was electricity usage so low on the twentieth?" I cannot tell whether the machine really stopped or the reader was down for half the day.
A 5-second wait per click
Every request calculates from raw per-second data. The more data a month has, the slower it gets. Once two or three people open the page at the same time, it starts interfering with other work on the same machine.
Down to 0.7 seconds
cron aggregates yesterday's totals at one in the morning. The web page reads just a few hundred summary rows per month. For today's data, which has not yet been finalized, it calculates on demand only from midnight to now, a short enough interval.
- At one in the morning, cron aggregates "yesterday" into energy_day, allowing existing rows to be overwritten (using REPLACE or ON DUPLICATE KEY UPDATE)
- The monthly summary page reads only from energy_day, without touching raw data
- Today's summary page calculates on demand from raw data only between midnight and now
- If rates or formulas are changed retroactively, recalculate energy_day for the entire affected period. Do not forget this step
What These Numbers Can Actually Tell You
This system is very good at answering comparative questions, such as what percentage a new way of operating a machine saves over the old one, or which operating cycle gives the best output per unit of electricity. Because the voltage and power factor assumptions are fixed on both sides of the comparison, much of the error cancels out. The question it cannot answer is, "How much do I have to pay this month?" For that, I have to wait for the bill.
Personally, I think the real benefit of doing this was not the baht figure on the screen. It was that once people could see daily numbers, they started noticing unusual days. That led to finding a step where the machine ran several minutes longer than necessary every cycle. Fixing that produced substantial real savings, even though the electricity cost figures that pointed to it were only estimates. If you already have current sensors and have not done anything with them yet, try starting with the formula and a daily summary table. Where the tariffs for different customer categories differ from the ones I have used here, I will come back and update the article later.
Hit something like this? Stuck somewhere?
Come trade notes on the LinuxThai board



Comments 0
No comments yet — be the first