// energy and utilities

Energy data is wrong long before it looks wrong. We check it against a calculation, not the screen.

Meter readings, forecasts and settlement files break on clock changes, late files and unit mix-ups. We test those failures on every release, with QA tools we built ourselves.

Start a two-week pilot
ISO 27001:2022embedded QA since 2023we built NIS2 Manager$25-45/hour
I rebuilt a proxy that records the app's API calls and replays them, so every Playwright run sees the same data. When a test fails, the code changed, not the data.
Richard Gasztany, QA engineer on renewable-energy data platforms since 2024
// what keeps you up at night, and what you get
"A wrong forecast or meter reading reaches a grid operator or a trader."
Data checked against an independent calculation, not against what the screen shows.Flows
"A data pipeline fails overnight and nobody notices until a customer does."
Imports and pipelines tested for the failures that happen at night: late files, duplicates, gaps.BugBoard
"NIS2 now covers us, and we can't show how we test."
Security testing and NIS2 evidence from the team that built NIS2 Manager.AI Security Toolkit
// four places energy data breaks
01 · clock change

The day with 25 hours loses an hour

On the last Sunday of October a local day has 25 hours (100 quarter-hours) and in March only 23 (92). If readings are keyed by local time, the repeated 02:00 hour overwrites the first one, so the daily total quietly covers 24 hours. Billing and settlement inherit the gap.

also: the missing hour in March, midnight rollover across time zones
01:00CEST
02:00CEST
02:00CET
03:00CET
day24 slots
SHORTshould be 25 hours
how we test itReplay the 23-hour and 25-hour days through the whole chain with known readings. Assert the interval count and that the total equals an independent sum made in UTC.
const day = readings('Europe/Brussels', '2026-10-25')
await ingest(day)
expect(db.count('intervals', { day })).toBe(100)   // got 96
expect(report.dailyKwh(day)).toBeCloseTo(sumUtc(day), 3)
02 · late files

The same file arrives twice, or after the report

Suppliers resend corrected files, retry on timeouts and deliver late. An ingest that appends instead of replacing counts the day twice. An ingest that ignores a late file leaves the corrected values out. Neither raises an error.

found when a customer compares the report with their own meter
sftpingeststorereportmeters_0310.csv, 96 rowsINSERT 96 rowsday total 41 200 kWhdaily report sent 06:00same file resent 09:30INSERT 96 rows againday total 82 400 kWh
FAILthe day is counted twice, and the report is already out
how we test itDeliver every file twice, out of order and late, then a corrected version with changed values. Assert one row per meter and interval, the latest values, and a flag when a closed day is restated.
await ingest(file('meters_0310.csv'))
await ingest(file('meters_0310.csv'))              // same file again
expect(db.count('readings', { day })).toBe(96 * meters)   // got 2x
await ingest(corrected('meters_0310.csv', { m7: 3.5 }))
expect(db.read('m7', day)).toBe(corrected.total)   // replaced once
03 · units

A forecast in kW is compared with energy in kWh

Power (kW) is a rate and energy (kWh) is power over time. A 15-minute meter interval of 0.5 kWh is an average of 2 kW. Compare a forecast in kW with readings in kWh, or sum four quarter-hour kW values as if they were kWh, and the error is a factor of four that looks like model drift.

also: MW vs MWh, interval start vs interval end, UTC vs local
forecast, hour2.0 kWactual, one 15 min interval0.5 kWhcompared as plain numbers2.0 vs 0.5error shown4x too high
0.5 kWhx 4 intervals= 2.0 kWh per hour
FAILkW compared with kWh
PASSboth sides in kWh per interval
how we test itRecompute forecast error from raw readings with the units written out, and compare it with what the dashboard shows. Feed intervals of 15 minutes and one hour and check both agree.
const actual = readings.kwhPerInterval('15min')
const fc = forecast.kw().map(kw => kw * 0.25)       // kW -> kWh
expect(dashboard.errorPct()).toBeCloseTo(mape(fc, actual), 2)
expect(dashboard.unit('forecast')).toBe(dashboard.unit('actual'))
04 · nis2

The pipeline fails and no alert fires

A nightly job can exit 0 after loading nothing, so monitoring that watches exit codes stays green. NIS2 article 23 expects an early warning within 24 hours and a notification within 72 hours of becoming aware of a significant incident. You can only meet that if the logs say when it started, which service, and how many customers.

NIS2 energy operators: Annex I, high criticality
02:10job fails
06:00report
09:30customer
+24 hearly warn
+72 hnotify
DUEincident notification
how we test itInject the failure: an empty file, a killed job, a feed that stops. Time how long until an alert fires, and check the log carries the facts an early warning needs.
await feed.deliver(emptyFile())             // job exits 0, 0 rows
await clock.advance('15m')
expect(alerts.fired('ingest.zero_rows')).toBe(true)   // got none
expect(alerts.last().fields).toEqual(
  expect.arrayContaining(['detectedAt', 'service', 'scope']))

Illustrative examples built from defect patterns we test for. Not client data.

// on every pull request

Your code is written in hours. Our pipeline keeps up.

Pull requestfrom your coding agent
Flowsjourneys and fault injection
Security Toolkit15 scanners + 20 own checks
QA engineerconfirms every finding
BugBoardrelease report, Jira sync
// the regulation, in testing terms
regimewhere it actually breakswhat we run
NIS2 risk measuresSecurity measures exist on paper, their effectiveness is never assessedsecurity and functional test evidence per control, kept in BugBoard
NIS2 incident reportingFailures nobody detects, so no 24 h or 72 h clock can startalert-on-silence tests, log fields an early warning needs
NIS2 supply chainThird-party feeds and vendors trusted without a testfeed contract tests, schema and gap checks, dependency review
GDPRSmart meter readings can show when a household is at hometest data scrubbing, access and retention tests, export checks
// proof

Energy data, tested for the long run

embedded QA since 2023renewable-energy analyticsNIS2 Manager
  • A Belgian renewable-energy analytics company: embedded QA engineers since 2023.
  • We built NIS2 Manager, a compliance tool for the EU NIS2 directive.

Client name withheld. We test the software and data around the grid; control systems and operational technology are not in scope.

BUG-0519 · metering / daily totals · examplehigh

Daily total is one hour short on the day the clocks go back

  1. Load 15-minute readings for 25 October 2026 (Europe/Brussels)
  2. Open the daily consumption report for that day
  3. The report holds 96 intervals instead of 100

Fix: Key readings by UTC instant and convert to local time only for display.

REQ-METER-12TC-0731 addedJira ENR-562

Two weeks on your energy data stack. Free if you're not happy.

Start a pilot