Webhook icon
Schedule icon
WorkingDirectory icon
Commands icon
Script icon
Process icon
Loop icon
If icon
Create icon
SlackIncomingWebhook icon
Log icon

Reconcile Smart Meter Reads Against the Billing Estimate After Each AMI Import

Reconcile AMI meter reads against billed kWh in Kestra, flag tolerance breaches, missing hours and backward register jumps, then escalate via GitHub and Slack.

Categories
BusinessData

How it works

  1. ami_import_completed (io.kestra.plugin.core.trigger.Webhook, keyed with the AMI_IMPORT_WEBHOOK_KEY secret) starts reconciliation the moment the AMI importer finishes loading the interval reads and the billing estimate. daily_backstop (io.kestra.plugin.core.trigger.Schedule, cron 0 6 * * *) is the morning net and ships disabled: true until an operator turns it on after a webhook that never fired.
  2. reconciliation_workspace (io.kestra.plugin.core.flow.WorkingDirectory) keeps the fixtures and the reconciler in one directory, with only runnable script tasks inside. write_meter_fixtures (io.kestra.plugin.scripts.shell.Commands) writes meter-reads.csv with twelve hourly reads from a single meter, the register deliberately rolling backward at 07:00, and billing-estimate.json with the billed kWh for the same window.
  3. reconcile_reads (io.kestra.plugin.scripts.python.Script, stdlib only) sums read_kwh into metered_kwh, reads billed_kwh, computes delta_pct, walks the expected hourly slots for missing_hour entries and the register column for backward_jump entries, then prints one ::{"outputs": ...}:: line carrying verdict, billed_kwh, metered_kwh, delta_pct and gaps.
  4. reconcile_reads (io.kestra.plugin.scripts.python.Script) writes exceptions.txt with one pipe-delimited line per gap and publishes it with outputFiles, so the artifact survives the workspace. log_gap_report (io.kestra.plugin.core.flow.Loop) then runs right after the workspace closes and append_gap_line (io.kestra.plugin.scripts.shell.Commands) echoes one pipe-delimited line per gap into the execution log. discrepancy_gate (io.kestra.plugin.core.flow.If) escalates when the absolute delta crosses tolerance_pct or any gap exists, filing open_discrepancy_issue (io.kestra.plugin.github.issues.Create) with the gap table and posting alert_metering_ops (io.kestra.plugin.slack.notifications.SlackIncomingWebhook), while a clean run records log_reconciled. The flow level errors handler reports a broken run with its execution id, because a failure and a clean import look identical in the logs.

What you get

  • verdict (STRING) - OK when the delta sits inside tolerance and no gap exists, MISMATCH when either check fails, NO_DATA when the reads or the bill could not be read.
  • billed_kwh (FLOAT), metered_kwh (FLOAT) and delta_pct (FLOAT) - the three numbers the gate weighed, ready for a billing dashboard.
  • One exceptions.txt artifact with a line per gap, the same lines echoed into the execution log, a GitHub issue with the gap table and a Slack message whenever the gate opens.

Who it's for

  • Metering and billing ops teams who eyeball an AMI export against a utility estimate before the statement goes out.
  • Energy engineers who want a missing hour or a rolled back register to raise an issue the same morning it happens.
  • Anyone who has found a month of drifted reads only when the customer disputed the bill.

Why orchestrate this with Kestra

A reconciliation script in cron tells nobody when it breaks, and an AMI import that lands half written looks exactly like one that landed clean. Kestra gives the importer a webhook to call, a disabled morning backstop, a shared workspace for the fixtures, a loop that leaves a paper trail per gap, and a gate that turns any breach into a GitHub issue plus a Slack ping. The verdict and the three numbers are typed outputs, so a billing dashboard reads them without scraping logs, and the errors handler keeps a broken run visible.

Prerequisites

  • A Kestra worker with python available to the process runner.
  • An AMI export shaped like meter-reads.csv (columns meter_id, timestamp, read_kwh, register_kwh) and a billing estimate shaped like billing-estimate.json carrying a billed_kwh number, or the shipped fixtures while you trial the flow.
  • A GitHub token able to open issues in the repository named by github_repository, plus a Slack incoming webhook for the breach and failure alerts.

Secrets

  • AMI_IMPORT_WEBHOOK_KEY: key segment guarding the webhook URL, so only the AMI importer can start a run.
  • GITHUB_TOKEN: token used by open_discrepancy_issue to file the gap table.
  • SLACK_WEBHOOK_URL: Slack incoming webhook URL for the breach alert and for failure alerts.

Quick start

  1. Add the three secrets to your namespace and import the flow.
  2. Run once with the default inputs and confirm verdict reads MISMATCH, billed_kwh reads 9.95, metered_kwh reads 10.12, delta_pct reads 1.71 and gaps holds the backward jump at 07:00.
  3. Point github_repository at a repository where issues are welcome, then re-run with tolerance_pct at 5.0 and watch the gate stay open on the gap alone.
  4. Trigger ami_import_completed at /api/v1/{tenant}/executions/webhook/{namespace}/smart-meter-reads-reconciliation/{AMI_IMPORT_WEBHOOK_KEY}, or set disabled: false on daily_backstop once the importer is wired.

How to extend

  • Replace the fixture heredocs with an io.kestra.plugin.core.http.Request or an io.kestra.plugin.jdbc.postgresql.Query task that reads the AMI export from its real source, keeping the same reconciler contract.
  • Loop gaps with io.kestra.plugin.core.flow.ForEach to open one issue per missing hour when the metering team triages by feeder.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.