Asoba Ona Documentation

Validation Guide

ODS-E provides a multi-level validation system that checks energy records against JSON schemas, semantic rules, and conformance profiles. Validation runs locally — no service calls required — making it suitable for CI/CD pipelines, pre-upload checks, and data quality monitoring.

Validation Levels

Schema Validation (level="schema")

The default level. Checks each record against the energy-timeseries.json schema:

Semantic Validation (level="semantic")

Builds on schema validation and adds physical plausibility checks. Requires capacity_kw (and optionally latitude/longitude for nighttime detection):

Semantic issues are returned as warnings, not errors — they don’t fail validation but flag records for review.

Temporal Validation

Temporal bounds are defined in transform specs (e.g., expected 5-minute intervals, max 4-hour gap) and applied during the transform step. These checks are configured per-vendor in the transform YAML:

validation:
  temporal:
    expected_interval: "5min"
    max_gap: "4h"

Conformance Profiles

Profiles are a validator-level concept layered on top of the schema. They specify which fields must be present (and which values are allowed) for a given operating context. Four profiles are defined:

1. bilateral — PPA / Bilateral Trade Settlement

For power purchase agreements and bilateral trades. Both trading parties, the settlement window, contract reference, and settlement type must be present.

Required Field Value Constraint
seller_party_id —
buyer_party_id —
settlement_period_start —
settlement_period_end —
contract_reference —
settlement_type must be "bilateral"

2. wheeling — Wheeled Energy Across Networks

A superset of bilateral. All bilateral fields plus network operator, wheeling model, injection/offtake points, reconciliation status, and loss factor.

Additional Required Field Value Constraint
network_operator_id —
wheeling_type —
injection_point_id —
offtake_point_id —
wheeling_status —
loss_factor —

3. sawem_brp — Wholesale Market (SAWEM) Settlement

For Balance Responsible Parties in the South African Wholesale Electricity Market.

Required Field Value Constraint
seller_party_id —
balance_responsible_party_id —
settlement_type one of sawem_day_ahead, sawem_intra_day, balancing, ancillary
forecast_kWh —
settlement_period_start —
settlement_period_end —

4. municipal_recon — Municipal Billing / Reconciliation

For municipal billing and reconciliation workflows.

Required Field Value Constraint
buyer_party_id —
billing_period —
billed_kWh —
billing_status —

Profile validation runs after schema validation passes. If schema validation produces errors, profile validation is skipped to avoid confusing duplicate messages.

CLI Usage

# Schema-level validation
odse validate --input output/records.json --level schema

# Semantic validation (requires capacity_kw in asset metadata)
odse validate --input output/records.json --level semantic

Expected output:

3/3 valid, 0 errors

With errors:

2/3 valid, 2 errors: 1x TYPE_MISMATCH, 1x ENUM_MISMATCH

Python API

Single record validation

from odse import validate

record = {
    "timestamp": "2025-01-01T00:00:00Z",
    "kWh": 45.2,
    "error_type": "normal",
    "direction": "generation",
}

# Schema validation
result = validate(record, level="schema")
print(result.is_valid)   # True
print(result.errors)     # []
print(result.level)      # "schema"

# Semantic validation with capacity check
result = validate(record, level="semantic", capacity_kw=50.0)
print(result.is_valid)   # True
print(result.warnings)   # [] (45.2 kWh < 50 × 1.1 = 55)

Batch validation

from odse import validate_batch

records = [
    {"timestamp": "2025-01-01T00:00:00Z", "kWh": 100.5, "error_type": "normal"},
    {"timestamp": "invalid-date", "kWh": "not-a-number", "error_type": "unknown"},
    {"timestamp": "2025-01-01T01:00:00Z", "kWh": 50.0, "error_type": "normal"},
]

result = validate_batch(records, level="schema")
print(result.summary)
# 2/3 valid, 2 errors: 1x TYPE_MISMATCH, 1x REQUIRED_FIELD_MISSING

print(f"Total: {result.total}")    # 3
print(f"Valid: {result.valid}")    # 2
print(f"Invalid: {result.invalid}")  # 1

Profile validation

from odse import validate

# Bilateral trade record
bilateral_record = {
    "timestamp": "2026-06-27T14:00:00+02:00",
    "kWh": 87.3,
    "error_type": "normal",
    "seller_party_id": "nersa:gen:SOLARPK-001",
    "buyer_party_id": "nersa:offtaker:MUN042",
    "settlement_period_start": "2026-06-27T14:00:00+02:00",
    "settlement_period_end": "2026-06-27T14:30:00+02:00",
    "contract_reference": "PPA-SOLARPK-MUN042-2025-003",
    "settlement_type": "bilateral",
}

result = validate(bilateral_record, level="schema", profile="bilateral")
print(result.is_valid)  # True

# Wheeling record
wheeling_record = {
    "timestamp": "2026-02-18T14:00:00+02:00",
    "kWh": 87.3,
    "error_type": "normal",
    "direction": "generation",
    "seller_party_id": "nersa:trader:ENPOWER-001",
    "buyer_party_id": "za-city-capetown:offtaker:METRO-4401",
    "settlement_period_start": "2026-02-18T14:00:00+02:00",
    "settlement_period_end": "2026-02-18T14:30:00+02:00",
    "contract_reference": "PPA-ENPOWER-CCT-2025-017",
    "settlement_type": "bilateral",
    "network_operator_id": "nersa:dso:eskom-tx",
    "wheeling_type": "virtual",
    "injection_point_id": "NCAPE-SOLAR-GEN-12",
    "offtake_point_id": "CCT-DIST-MV-BELLVILLE-03",
    "wheeling_status": "confirmed",
    "loss_factor": 0.032,
}

result = validate(wheeling_record, level="schema", profile="wheeling")
print(result.is_valid)  # True

Batch validation with profiles

from odse import validate_batch

result = validate_batch(records, level="schema", profile="wheeling")
print(result.summary)
# 5/5 valid, 0 errors

Error Codes

Code Level Description
REQUIRED_FIELD_MISSING schema A required field (timestamp, kWh, error_type) is absent
TYPE_MISMATCH schema Field value is wrong type (e.g., string where number expected)
ENUM_MISMATCH schema Value not in allowed enum set
OUT_OF_BOUNDS schema Numeric value outside allowed range
PATTERN_MISMATCH schema String doesn’t match required regex pattern
EXCEEDS_PHYSICAL_MAXIMUM semantic (warning) kWh exceeds theoretical max for asset capacity
STATE_PRODUCTION_MISMATCH semantic (warning) Significant production reported with offline status
UNKNOWN_PROFILE profile Profile name not recognized
PROFILE_FIELD_MISSING profile Field required by the profile is absent
PROFILE_VALUE_MISMATCH profile Field value doesn’t satisfy profile constraint

Result Data Classes

@dataclass
class ValidationError:
    path: str       # JSON path, e.g., "$.kWh"
    message: str    # Human-readable description
    code: str       # Error code from the table above

@dataclass
class ValidationResult:
    is_valid: bool
    errors: List[ValidationError]
    warnings: List[ValidationError]
    level: str      # "schema" or "semantic"

@dataclass
class BatchValidationResult:
    total: int
    valid: int
    invalid: int
    errors: List[Tuple[int, ValidationError]]  # (record_index, error)
    summary: str   # e.g., "2/3 valid, 2 errors: 1x TYPE_MISMATCH"