EnergyLogger-logo energylogger
Command-line analyzer for the Voltcraft Energy-Logger 4000. It decodes the .BIN files the device writes to its SD card and produces a minute-by-minute parameter history plus daily, overall and blackout statistics.
Initial Go port of Valer Bocan's vbocan/voltcraft-energy-analyzer via Claude Opus 5, followed by manual changes.
The only dependency is peterbourgon/ff for flag and environment parsing.
go build ./cmd/energylogger
energylogger [flags]
Both the input and output directories default to the current directory.
| Flag | Environment variable | Meaning |
|---|---|---|
-input <dir> |
ENERGYLOGGER_INPUT |
directory to read Voltcraft .BIN files from |
-output <dir> |
ENERGYLOGGER_OUTPUT |
directory to write the output files to (created if missing) |
-quiet |
ENERGYLOGGER_QUIET |
suppress the banner and per-file progress output |
-no-color |
ENERGYLOGGER_NO_COLOR |
disable ANSI colour (also honours NO_COLOR and a non-terminal stdout) |
-h, --help, /? |
— | show help |
Every setting can come from either source, in this order of precedence: a flag on the command line, then the environment variable, then the default.
energylogger -input /Volumes/SDCARD -output ~/energy # read from the SD card, write to ~/energy energylogger -input /Volumes/SDCARD # read from the SD card, write to the current directory energylogger # read and write in the current directory export ENERGYLOGGER_INPUT=/Volumes/SDCARD # or keep the folders in the environment, e.g. in .envrc export ENERGYLOGGER_OUTPUT=~/energy energylogger
Every file in the input folder is tried; anything that is not a Voltcraft data file is reported as Invalid and skipped, including the small INFO: metadata .BIN files the device also writes. Subdirectories and dotfiles such as .DS_Store are passed over silently. Samples from all files are merged, sorted and deduplicated by timestamp, so dumping the same SD card twice is harmless.
Deduplication keeps the first sample of each timestamp and reports how many it dropped. If any of the dropped samples carried different readings from the one kept, that is not a re-dump and a real measurement was lost, so the tool warns about it — see Timestamps.
Exit code is 0 on success, 1 if the input folder cannot be read, the output folder is unusable, or a file could not be written, and 2 for a bad flag or environment variable. An input folder that does not exist is an error, not an empty run, since it usually means a mistyped path or an unmounted card.
| File | Contents |
|---|---|
voltcraft_history.txt |
one line per recorded minute: voltage, current, cos φ, active and apparent power |
voltcraft_history.csv |
the same history, values unrounded, for spreadsheets |
voltcraft_stats.txt |
overall statistics, per-day statistics, and the blackout history |
==== OVERALL STATISTICS ==================
Interval: [2014年07月20日 22:04]-[2014年09月12日 23:59] (54d:01h:55m)
Average consumption: 1.53kWh/day | Projected: 46.03kWh/month or 559.98kWh/year.
- ACTIVE POWER
Total energy consumption: 82.97kWh.
Peak power was 1.34kW and occurred on [2014年08月26日 07:52].
Minute by minute average power: 0.06kW.
...
Energy totals only count minutes that were actually recorded, so a day with a blackout reports the energy used while logging, not an extrapolation. The per-day "recorded activity" percentage spans the first to the last sample of the day and therefore includes any gaps in between.
Two duration quirks are inherited from the Rust original and kept for comparability:
-
The overall
Intervallength is the plain distance between the first and last sample, so it omits the final sample's own minute; per-day durations add that minute, which is why a fully recorded day reads01d:00h:00m (100.0%)while a month-long interval reads29d:22h:05m. -
Average consumptionlikewise divides gap-inclusive elapsed time into energy counted only over recorded minutes, so it under-reports on data with long blackouts.
The device clock is set by hand and stores no timezone, so a recording carries nothing but the wall-clock digits the device displayed. Those digits are printed back verbatim, in YYYY-MM-DD HH:MM at one-minute resolution — the finest the format offers, since a block header has no seconds field.
Internally the timestamps are held in time.UTC. That is a storage choice, not a claim that the readings are UTC: it keeps every duration, day boundary and blackout length free of the host's TZ and of DST jumps. Nothing converts them, and nothing should.
The practical consequence is about the device's clock, not the machine running this tool. Leave the device clock alone and a DST changeover is invisible here. Adjust it mid-recording and:
- Spring forward — the hour you skipped looks like a one-hour gap, so it is reported as a blackout that never happened.
- Fall back — the repeated hour produces duplicate timestamps. Deduplication keeps the first sample of each minute and discards the rest, so up to 60 real samples and their energy drop out of the statistics. The tool warns when this happens, since the discarded samples carried different readings.
The same warning covers the other way duplicates arise: cards from two different devices sitting in one input folder.
In the CSV the column is named Timestamp (device local time) rather than plain Timestamp, so importers are less likely to reinterpret it in the reader's own timezone.
The .BIN files hold a flat byte stream: no file header, no length field, no checksum. Real data files are exactly 10244 bytes.
BLOCK: E0 C5 EA 3-byte magic number
MM DD YY HH MI block start timestamp, plain binary (not BCD), year + 2000
<record>... 5 bytes each, one sample per minute
EOD: FF FF FF FF end of data; the rest of the file is 0xFF padding
RECORD: voltage uint16 big-endian / 10 -> V
current uint16 big-endian / 1000 -> A
powerfactor uint8 / 100 -> cos φ
derived: active power = V ×ばつ A ×ばつ cos φ / 1000 (kW)
apparent power = V ×ばつ A / 1000 (kVA)
Records carry no timestamp of their own: the n-th record of a block is stamped block start + n minutes. A file can hold several blocks at arbitrary offsets, one per recording session, so the magic number is tested at every record position and the minute counter restarts at each block. Gaps between blocks and between files are what the blackout detection reconstructs.
The device also writes a short file beginning with the ASCII text INFO:. It carries no samples and is skipped.
voltcraft_history.txtis byte-identical to the Rust tool's outputvoltcraft_history.csvdiffers only in its header line (item 4 below)voltcraft_stats.txtthe only differences are the apparent-power lines listed first
- Fixed: the daily Total apparent power line printed the active-power figures (
src/export.rs:181-190in the original), which is why every day's apparent values equaled its active ones in the original README. This port prints the real apparent-power totals, averages and peaks. - Fixed: the overall Peak power ... kVA line printed the active power of the peak-apparent-power sample (
src/export.rs:114). This port prints its apparent power. - Timestamps are timezone-naive rather than bound to the host machine timezone, as described under Timestamps. The original attaches each reading to the machine's zone, which on a DST fall-back day labels one hour twice: for the bundled
sample_data2it prints the 60 minutes of2014年10月26日 02:00–02:59under two different instants each, and its durations, daily grouping and blackout lengths vary withTZ. - The CSV timestamp column is named
Timestamp (device local time)instead ofTimestamp, so spreadsheets are less likely to reinterpret a bare naive timestamp in the reader's own zone. The values themselves are unchanged. - Duplicate timestamps are reported. Deduplication behaves as before, but the count of dropped samples is printed, and dropped samples whose readings disagreed with the one kept raise a warning instead of vanishing silently.
- No panics on damaged input. A truncated file, a file with no end-of-data marker, an impossible date, or an implausible mains voltage is reported as invalid and skipped; the original aborted the whole run.
- The tool's own output files are skipped when the input and output folders are the same, so a second run in the same directory does not try to parse them.
- Per-day statistics are computed in a single pass instead of re-scanning every sample once per day.
go test ./...The test suite covers the parser (including the original's own byte fixture, block boundaries, truncation, padding and out-of-range values), the statistics (aggregation, extremum tie-breaking, day grouping, blackout gaps), the output formats, and the command line. TestRunGolden runs the whole pipeline over the real device captures in testdata/ and compares all three outputs against testdata/golden/. After an intentional format change, regenerate them and review the diff:
go test -run TestRunGolden ./cmd/energylogger -updateExtremum tie-breaking follows the original deliberately — the last of several equal maxima and the first of several equal minima — so that reported peak timestamps stay comparable with the Rust tool's.
See LICENSE.