Jakarta, Indonesia
Posts

The 900x Overcounting Bug: Time-Series Surgery in InfluxDB

In my ongoing quest to perfect my local home energy monitoring stack, I recently made what I thought was a sensible architectural change. Previously, I calculated energy consumption manually. My Python service (tuyamon) listened to real-time power updates (cur_power via Tuya DPS 19) every few seconds, calculated the Riemann sum ($Power \times \Delta t$), and accumulated the total. It worked well, but it felt a bit "hacky." So, I decided to use the vendor-provided energy metric. Tuya devices expose a data point called add_ele (DPS 17), which represents the incremental energy consumed over a given interval. "Why do the math myself when the hardware metering IC does it natively?" I reasoned. I deployed the change to production. Everything seemed fine. Then I checked Grafana the next morning.
900x Overcounting Spike
My guest room AC, which normally consumes maybe 5-10 kWh on a busy day, was reporting 314 kWh in a single day. My main circuit breaker reported an impossible 300 kWh. I had introduced a massive data corruption event. To understand what went wrong, you have to understand the difference between how IoT devices communicate with their proprietary clouds versus how they communicate locally. I ran a live packet capture on the local TCP socket to monitor the device's behavior. The results were illuminating:
  1. Tuya devices do NOT push add_ele asynchronously in local mode. While they happily push real-time power (cur_power) on their own, the energy interval (add_ele) never arrived in the push stream.
  2. add_ele only appeared when I explicitly polled the device using status() or updatedps().
  3. When polled, the device returned a cached interval value (e.g., 100 Wh). It did not reset this value after the poll.
Here is where my architecture betrayed me. To keep the local push stream alive, tuyamon sends a heartbeat updatedps() command every 2 seconds. Because my accumulation logic didn't deduplicate the responses, it treated every poll response as a fresh incremental push. The Math:
  • Expected: Receive 100 Wh interval update every ~30 minutes = 0.2 kWh / hour
  • Actual: Receive the same cached 100 Wh every 2 seconds = 180 kWh / hour
That is a 900x overcount. What works for the cloud doesn't work locally. The add_ele metric was explicitly designed for Tuya's cloud infrastructure, which polls the device at specific intervals and handles the deduplication server-side. In a high-frequency local polling environment, relying on interval metrics without stateful deduplication is a recipe for disaster. I reverted the primary energy calculation back to the reliable Riemann sum. However, I kept add_ele around, writing it to a separate _crosscheck measurement every 30 minutes just for accuracy validation. Trust, but verify. Fixing the code was easy. Fixing the data was hard. I had roughly 36 hours of heavily corrupted energy data mixed in with perfectly valid power, voltage, and current readings. I couldn't just drop the bucket or delete the measurement.
InfluxDB Time-Series Surgery
My first thought was to just delete the energy field for that time window using the InfluxDB Python client.
Python
# What I thought would work
delete_api.delete(
    start="2026-05-11T00:45:00Z",
    stop="2026-05-12T14:34:00Z",
    predicate='_measurement="smart_plug" AND _field="energy"',
    bucket="iot"
)
Error: delete by field is not supported. InfluxDB (and many Time-Series Databases) optimize for fast ingestion and querying of massive datasets. They achieve this by grouping fields under a single timestamp (a Point). You can delete a measurement, or you can delete based on tags, but you cannot selectively delete a single field (like energy) while leaving other fields (like power) intact at the same timestamp. Since I couldn't delete the data, I had to overwrite it. InfluxDB deduplicates based on Measurement + Tags + Timestamp. If you write a new value with the exact same timestamp, it overwrites the old one. I wrote a "Time-Series Surgery" script to perform a two-step reconstruction: Step 1: Neutralize the Corrupt Data I queried the database for every single timestamp that contained a corrupt energy reading (over 30,000 points across all devices). I then wrote a batch of points back to those exact timestamps with an energy value of 0.0. This effectively wiped the absurd 300 kWh spikes from the aggregates. Step 2: Reconstruct and Offset I then queried the perfectly valid power data from the same time window. I ran my Riemann sum integration over the historical power data in memory to reconstruct what the energy should have been. To write this reconstructed data back without accidentally hitting my newly zeroed-out timestamps, I applied a 1-microsecond offset to the new timestamps:
Python
# Offset by 1 microsecond to avoid collisions
write_ts = curr_time.replace(microsecond=min(curr_time.microsecond + 1, 999999))
The surgery was a complete success.
Device
Corrupted Total
Reconstructed Total
MCB AC & Water Heater300.5 kWh11.07 kWh
AC Plug Guest Room314.4 kWh9.71 kWh
Smart Plug Bedroom30.9 kWh0.53 kWh
The Grafana charts smoothed out, the daily aggregates aligned perfectly with historical baselines, and I didn't lose a single data point of the underlying power metrics. Sometimes, the most elegant solution isn't the one provided by the vendor API—it's the math you do yourself. And when you inevitably mess up the math, knowing the low-level deduplication mechanics of your database is the only way to perform surgical data recovery.
On this page