openadr

package module
v0.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 9 Imported by: 0

README

go-openadr — OpenADR 3 for Go

Go Reference CI

A Go implementation of OpenADR 3, the demand-response protocol between utilities or aggregators and the energy resources of their customers. It provides both sides of the REST API:

  • client: a VEN client and a Business Logic client for any OpenADR 3.1 VTN;
  • server: a VTN as an embeddable http.Handler;
  • openadr (the module root): the domain model both share.

Status: pre-release. OpenADR 3.1.0 only. Programs, events, reports, VENs and resources work end to end, with OAuth2 client credentials, scopes, object ownership and targeting. Subscriptions, webhooks and MQTT notifications are not implemented yet, and storage is in-memory only. The API may still change. The clients are tested against the OpenLEADR VTN, and the VTN with the ElaadNL client. See INTEROP.md for the evidence, docs/CONFORMANCE.md for exactly what is covered and PLAN.md for what comes next.

Out of scope: OpenADR 2.x, dispatch or optimization logic, integrations with physical resources (Modbus, OPC UA, ...), a web UI and a CLI. The library implements the protocol; what to do with a price or a load-shed request is the application's business.

Install

go get github.com/otfabric/go-openadr

Go 1.23 or later. No dependencies outside the standard library.

A VEN

ven, err := client.NewVEN(client.Config{
    BaseURL:     "https://vtn.example.com/openadr3",
    Credentials: &client.Credentials{ClientID: id, ClientSecret: secret},
})
if err != nil {
    return err
}

events, err := ven.Events(ctx, client.EventQuery{ProgramID: programID, ActiveOnly: true})
if err != nil {
    return err
}
for _, event := range events {
    for _, interval := range event.Intervals {
        for _, payload := range interval.Payloads {
            if price, ok := payload.Values[0].Number(); ok && payload.Type == "PRICE" {
                fmt.Println(interval.ID, price)
            }
        }
    }
}

_, err = ven.CreateReport(ctx, openadr.Report{
    EventID:    events[0].ID,
    ClientName: "site-42",
    Resources: []openadr.ReportResource{{
        ResourceName: "main-meter",
        Intervals: []openadr.Interval{{ID: 0, Payloads: []openadr.ValuesMap{
            {Type: "USAGE", Values: []openadr.Value{openadr.Number(1.8)}},
        }}},
    }},
})

The client discovers the token endpoint from the VTN (GET /auth/server), obtains a token, keeps it until shortly before it expires, and fetches a new one when the VTN answers 401. client.BL is the same for the Business Logic side: it creates programs and events, provisions VENs and their targets, and reads reports.

Errors from the VTN are *openadr.Error values and match sentinels:

if errors.Is(err, openadr.ErrNotFound) { ... }

A VTN

vtn, err := server.New(server.Config{
    Store:     storage.NewMemory(),
    Validator: validator,              // resolves a bearer token to a client ID and scopes
    TokenURL:  "https://idp.example.com/oauth/token",
})
if err != nil {
    return err
}
http.Handle("/openadr3/", http.StripPrefix("/openadr3", vtn))

The server does not bundle an identity provider: it takes an auth.Validator. For tests and small deployments, auth.Issuer is an in-memory client-credentials token service that is both the /auth/token handler and the validator. TokenURL is what GET /auth/server tells clients and has to be an absolute URL.

examples/quickstart runs a VTN, a Business Logic client and a VEN through the program → event → report workflow in one process:

go run ./examples/quickstart

examples/vtn is a standalone VTN to point other clients at.

Things to know

  • Concurrency. Clients and the server are safe for concurrent use.
  • Timeouts. Every client call takes a context that bounds the whole call, including a token request it may trigger. The default HTTP client has a 30 second timeout; pass your own http.Client for TLS settings, proxies or other limits.
  • Paging. A VTN returns at most 50 objects per search. client.Each iterates over a whole result.
  • Values. JSON does not distinguish integers from other numbers, so payload numbers are float64.
  • Validation. Objects are checked against the schema before they are sent; a violation is an *openadr.ValidationError and no request is made. Payload types, units and other enumerations of the specification are plain strings and are not restricted, so private values work.
  • Logging. The library is silent. The server logs internal errors to the slog.Logger it is given, if any.

Documentation

  • API.md: the public API by package, and the behaviour that signatures do not show.
  • ERRORS.md: every error a caller can get, and how to tell them apart.
  • INTEROP.md: what has been tested against which other implementation, and how to repeat it.
  • docs/CONFORMANCE.md: operation coverage, and how the VTN reads the parts of the specification that leave room.
  • ARCHITECTURE.md: packages, the version strategy, and the storage and authentication boundaries.
  • SECURITY.md: reporting a vulnerability, and what to know before deploying.
  • RELEASE.md: release notes. PLAN.md: roadmap and status. CONTRIBUTING.md: how to change things.

The reference implementations it is tested against, and a conformance suite that asserts the specification, come from a separate repository, otfabric/openadr-interop, which shares no code with this one.

Development

make check    # format, lint, vet, vulnerability scan, tests with -race, coverage, examples
make fuzz     # the parser fuzz targets
make interop  # against the OpenLEADR and ElaadNL reference images (needs Docker, see INTEROP.md)

License

MIT, see LICENSE.

Documentation

Overview

Package openadr defines the domain model of OpenADR 3: programs, events, reports, VENs, resources and the value types they are built from.

The types in this package are independent of any one protocol revision's JSON representation. Encoding and decoding for a specific revision (3.1.0 today) is an internal concern of the client and server packages, so the structures here carry no JSON tags and are not meant to be marshalled directly.

Entry points:

  • package client: VEN and Business Logic clients for a VTN.
  • package server: an embeddable VTN http.Handler.
  • package auth: bearer-token validation and OAuth2 scopes.

OpenADR 2.x is out of scope, as are dispatch algorithms and integrations with physical energy resources.

Index

Constants

View Source
const AggregatedReport = "AGGREGATED_REPORT"

AggregatedReport is the resource name of an aggregated report.

Variables

View Source
var (
	ErrBadRequest   = errors.New("openadr: bad request")
	ErrUnauthorized = errors.New("openadr: unauthorized")
	ErrForbidden    = errors.New("openadr: forbidden")
	ErrNotFound     = errors.New("openadr: not found")
	ErrConflict     = errors.New("openadr: conflict")
)

Categories of failure reported by a VTN. An *Error matches the one for its HTTP status through errors.Is. The storage layer of the server returns ErrNotFound and ErrConflict directly.

View Source
var Infinite = Duration{Years: 9999}

Infinite is P9999Y, which the specification uses to mean "indefinitely".

Functions

func ValidID

func ValidID(id string) bool

ValidID reports whether id is a well-formed object ID: 1 to 128 characters of letters, digits, '_' and '-'.

Types

type Duration

type Duration struct {
	Negative bool
	Years    int64
	Months   int64
	Weeks    int64
	Days     int64
	Hours    int64
	Minutes  int64
	Seconds  float64
}

Duration is an ISO 8601 duration as OpenADR uses it, for example PT1H or P1DT12H. Years and months are kept as written because their length depends on the date they are applied to; use AddTo for calendar arithmetic and TimeDuration when the duration has no such components.

The format allows either days or weeks, not both; String writes weeks only when Days is zero.

func DurationOf

func DurationOf(td time.Duration) Duration

DurationOf converts a time.Duration, using hours, minutes and seconds only.

func ParseDuration

func ParseDuration(s string) (Duration, error)

ParseDuration parses an ISO 8601 duration in the form the OpenADR 3 schema accepts.

func (Duration) AddTo

func (d Duration) AddTo(t time.Time) time.Time

AddTo returns t moved by the duration, applying years, months and days on the calendar in t's location.

func (Duration) IsInfinite

func (d Duration) IsInfinite() bool

IsInfinite reports whether the duration is P9999Y.

func (Duration) IsZero

func (d Duration) IsZero() bool

IsZero reports whether the duration has no length.

func (Duration) String

func (d Duration) String() string

String returns the ISO 8601 form. The zero Duration is "PT0S".

func (Duration) TimeDuration

func (d Duration) TimeDuration() (time.Duration, bool)

TimeDuration converts to a time.Duration, taking a day as 24 hours. It reports false when the duration has years or months, whose length is not fixed, or does not fit in a time.Duration.

type Error

type Error struct {
	// Method and Path identify the request.
	Method string
	Path   string
	// StatusCode is the HTTP status of the response.
	StatusCode int
	Problem    Problem
}

Error is a non-success response from a VTN. Problem is zero when the response body was not a problem object.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches the category sentinel for the response's status code.

type Event

type Event struct {
	Metadata

	// ProgramID is the program the event belongs to.
	ProgramID string
	Name      string
	// Duration optionally bounds the event; it may be used to loop intervals.
	Duration *Duration
	// Priority orders events; a lower number is a higher priority.
	Priority           *int
	Targets            []string
	ReportDescriptors  []ReportDescriptor
	PayloadDescriptors []EventPayloadDescriptor
	// IntervalPeriod sets the default start and duration of Intervals.
	IntervalPeriod *IntervalPeriod
	Intervals      []Interval
}

Event communicates a demand-response request to VENs.

func (Event) Validate

func (e Event) Validate() error

Validate checks the client-provided properties against the schema. It does not look at Metadata.

type EventPayloadDescriptor

type EventPayloadDescriptor struct {
	PayloadType string
	Units       string
	Currency    string
}

EventPayloadDescriptor gives context, such as units and currency, to event payloads of one type.

type Interval

type Interval struct {
	ID             int32
	IntervalPeriod *IntervalPeriod
	Payloads       []ValuesMap
}

Interval is a temporal window with payloads. ID is chosen by the client that creates the interval and is not a sequence number.

type IntervalPeriod

type IntervalPeriod struct {
	Start    *time.Time
	Duration *Duration
	// RandomizeStart is the range of a client-applied offset to Start.
	RandomizeStart *Duration
}

IntervalPeriod defines the temporal aspects of one or more intervals.

The specification gives two values a special meaning: a Start in year 1 may mean "now", and a Duration of P9999Y may mean "indefinitely"; see Duration.IsInfinite.

type Metadata

type Metadata struct {
	ID       string
	Created  time.Time
	Modified time.Time
}

Metadata is assigned by the VTN when an object is created or modified. A VTN ignores these fields in create and update requests, so callers leave them zero when building an object to send.

type ObjectType

type ObjectType string

ObjectType names a kind of object addressable through the API.

const (
	ObjectProgram      ObjectType = "PROGRAM"
	ObjectEvent        ObjectType = "EVENT"
	ObjectReport       ObjectType = "REPORT"
	ObjectSubscription ObjectType = "SUBSCRIPTION"
	ObjectVEN          ObjectType = "VEN"
	ObjectResource     ObjectType = "RESOURCE"
)

Object types defined by OpenADR 3.1.0.

type PayloadDescriptor

type PayloadDescriptor interface {
	// contains filtered or unexported methods
}

PayloadDescriptor is an EventPayloadDescriptor or a ReportPayloadDescriptor.

type Point

type Point struct {
	X, Y float64
}

Point is a pair of numbers, typically a point on a two dimensional grid.

type Problem

type Problem struct {
	Type     string
	Title    string
	Status   int
	Detail   string
	Instance string
}

Problem is the error body a VTN returns with a 4xx or 5xx response.

type Program

type Program struct {
	Metadata

	// Name identifies the program and is unique within a VTN.
	Name string
	// IntervalPeriod is the temporal span of the program.
	IntervalPeriod *IntervalPeriod
	// Descriptions link to human or machine readable descriptions.
	Descriptions []ProgramDescription
	// PayloadDescriptors give context to the payloads of the program's
	// events and reports. Each element is an EventPayloadDescriptor or a
	// ReportPayloadDescriptor.
	PayloadDescriptors []PayloadDescriptor
	Attributes         []ValuesMap
	// Targets restrict which VENs may read the program. A VTN only shows a
	// VEN the targets that VEN asked for.
	Targets []string
}

Program is a demand-response program or tariff.

func (Program) Validate

func (p Program) Validate() error

Validate checks the client-provided properties against the schema. It does not look at Metadata.

type ProgramDescription

type ProgramDescription struct {
	URL string
}

ProgramDescription links to a description of a program.

type Report

type Report struct {
	Metadata

	// ClientID is written by the VTN from the creating client's token.
	ClientID string
	// EventID is the event the report is associated with.
	EventID string
	// ClientName is chosen by the reporting client.
	ClientName         string
	Name               string
	PayloadDescriptors []ReportPayloadDescriptor
	Resources          []ReportResource
}

Report carries data from a VEN in response to an event.

func (Report) Validate

func (r Report) Validate() error

Validate checks the client-provided properties against the schema. It does not look at Metadata or ClientID, which the VTN writes.

type ReportDescriptor

type ReportDescriptor struct {
	PayloadType string
	ReadingType string
	Units       string
	Targets     []string
	// Aggregate asks for one result over all targeted resources.
	Aggregate bool
	// StartInterval is the interval on which to report; -1 is the end of
	// the last interval.
	StartInterval int32
	// NumIntervals is the number of intervals to include; -1 is all.
	NumIntervals int32
	// Historical reports on the intervals before StartInterval, as opposed
	// to those after it.
	Historical bool
	// Frequency is the number of intervals between reports; -1 is the same
	// as NumIntervals.
	Frequency int32
	// Repeat is the number of reports to generate; -1 is indefinitely.
	Repeat          int32
	ReportIntervals ReportIntervals
}

ReportDescriptor requests a report from a VEN. NewReportDescriptor returns one holding the defaults of the specification; the zero value does not.

func NewReportDescriptor

func NewReportDescriptor(payloadType string) ReportDescriptor

NewReportDescriptor returns a descriptor for payloadType with every other field at the default the specification defines for it.

type ReportIntervals

type ReportIntervals string

ReportIntervals says which intervals a requested report covers.

const (
	ReportIntervalsIntervals    ReportIntervals = "INTERVALS"
	ReportIntervalsSubIntervals ReportIntervals = "SUB_INTERVALS"
	ReportIntervalsOpen         ReportIntervals = "OPEN_INTERVALS"
)

Report interval options defined by OpenADR 3.1.0.

type ReportPayloadDescriptor

type ReportPayloadDescriptor struct {
	PayloadType string
	ReadingType string
	Units       string
	Accuracy    *float64
	// Confidence is a percentage, 0 to 100.
	Confidence *int
}

ReportPayloadDescriptor gives context, such as units and data quality, to report payloads of one type.

type ReportResource

type ReportResource struct {
	ResourceName   string
	IntervalPeriod *IntervalPeriod
	Intervals      []Interval
}

ReportResource is the report data of one resource. A ResourceName of AggregatedReport denotes data aggregated over several resources.

type Resource

type Resource struct {
	Metadata

	ClientID string
	// VENID is the VEN the resource belongs to.
	VENID string
	// Name is unique within its VEN.
	Name       string
	Attributes []ValuesMap
	// Targets are granted by Business Logic; see VEN.Targets.
	Targets []string
}

Resource is a resource of a VEN, such as a device or a group of devices.

func (Resource) Validate

func (r Resource) Validate() error

Validate checks the client-provided properties against the schema. ClientID is not required here: whether it is depends on who sends the object.

type VEN

type VEN struct {
	Metadata

	// ClientID ties the VEN to the client credentials it authenticates with.
	// Business Logic supplies it; for a VEN-created object the VTN writes it
	// from the token.
	ClientID string
	// Name is unique within a VTN.
	Name       string
	Attributes []ValuesMap
	// Targets are granted by Business Logic and decide which targeted
	// programs and events the VEN may read. Only Business Logic may set them.
	Targets []string
}

VEN is a virtual end node known to the VTN.

func (VEN) Validate

func (v VEN) Validate() error

Validate checks the client-provided properties against the schema. ClientID is not required here: whether it is depends on who sends the object.

type ValidationError

type ValidationError struct {
	// Field is the path of the property in the JSON naming of the
	// specification, such as "intervals[0].payloads[1].type".
	Field  string
	Reason string
}

ValidationError reports a property that violates the OpenADR schema. It is returned by the Validate methods, by the clients before a request is sent, and by the wire codecs for malformed messages.

func (*ValidationError) Error

func (e *ValidationError) Error() string

type Value

type Value struct {
	// contains filtered or unexported fields
}

Value is one data point of a ValuesMap: a number, a string, a boolean or a Point. The zero Value is invalid. Values are comparable with ==.

JSON does not distinguish integers from other numbers, so both are held as a float64; integers beyond 2^53 lose precision.

func Bool

func Bool(b bool) Value

Bool returns a boolean Value.

func Number

func Number(n float64) Value

Number returns a numeric Value.

func PointValue

func PointValue(x, y float64) Value

PointValue returns a Value holding the point (x, y).

func String

func String(s string) Value

String returns a string Value.

func (Value) Bool

func (v Value) Bool() (bool, bool)

Bool returns the boolean held and whether the Value is a boolean.

func (Value) Kind

func (v Value) Kind() ValueKind

Kind reports what the Value holds; it is 0 for the zero Value.

func (Value) Number

func (v Value) Number() (float64, bool)

Number returns the number held and whether the Value is a number.

func (Value) Point

func (v Value) Point() (Point, bool)

Point returns the point held and whether the Value is a point.

func (Value) String

func (v Value) String() string

String formats the Value for diagnostics.

func (Value) Text

func (v Value) Text() (string, bool)

Text returns the string held and whether the Value is a string.

type ValueKind

type ValueKind uint8

ValueKind is the JSON type of a Value.

const (
	KindNumber ValueKind = iota + 1
	KindString
	KindBool
	KindPoint
)

Kinds of value a ValuesMap may hold.

func (ValueKind) String

func (k ValueKind) String() string

type ValuesMap

type ValuesMap struct {
	Type   string
	Values []Value
}

ValuesMap is a list of values together with a type that says what they are, such as "PRICE". Type is one of the strings enumerated in the specification or a privately defined one; this package does not restrict it.

type Version

type Version string

Version identifies a revision of the OpenADR 3 specification.

const (
	// Version310 is OpenADR 3.1.0 (document 20250807-X).
	Version310 Version = "3.1.0"
)

Supported protocol revisions.

Directories

Path Synopsis
Package auth holds what the VTN needs to authenticate and authorize a request: the identity behind a bearer token and the OAuth2 scopes of OpenADR 3.
Package auth holds what the VTN needs to authenticate and authorize a request: the identity behind a bearer token and the OAuth2 scopes of OpenADR 3.
Package client holds the two clients of a VTN: VEN, for a virtual end node, and BL, for the Business Logic of a utility or aggregator.
Package client holds the two clients of a VTN: VEN, for a virtual end node, and BL, for the Business Logic of a utility or aggregator.
examples
quickstart command
Quickstart runs the basic OpenADR 3 workflow in one process: Business Logic creates a program and an event on a VTN, a VEN discovers them and sends a report, and Business Logic reads the report.
Quickstart runs the basic OpenADR 3 workflow in one process: Business Logic creates a program and an event on a VTN, a VEN discovers them and sends a report, and Business Logic reads the report.
vtn command
Vtn runs a standalone VTN with an in-memory store and its own token endpoint, for trying out clients and for interoperability tests.
Vtn runs a standalone VTN with an in-memory store and its own token endpoint, for trying out clients and for interoperability tests.
internal
vtntest
Package vtntest starts an in-process VTN for the tests of this module.
Package vtntest starts an in-process VTN for the tests of this module.
wire
Package wire defines what a protocol revision has to provide to the client and server: the JSON form of each object.
Package wire defines what a protocol revision has to provide to the client and server: the JSON form of each object.
wire/v310
Package v310 is the JSON representation of OpenADR 3.1.0, following the schemas of the specification's OpenAPI document (20250807-X).
Package v310 is the JSON representation of OpenADR 3.1.0, following the schemas of the specification's OpenAPI document (20250807-X).
Package interop holds tests that run this module's clients against VTNs implemented by others.
Package interop holds tests that run this module's clients against VTNs implemented by others.
Package server is an OpenADR 3.1.0 VTN as an http.Handler.
Package server is an OpenADR 3.1.0 VTN as an http.Handler.
storage
Package storage is the persistence boundary of the VTN.
Package storage is the persistence boundary of the VTN.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL