Code samples
Learn how to communicate with the Switch OpenADR 3 VTN API through simple code samples.
Overview
Using the Switch OpenADR 3 VTN API is as simple as communicating with any REST API. Below you will find examples of how to authenticate with the VTN prior to making any request to it and it follows with example of how to make a simple request.
Information about the VTN URL you can find on the VTN Endpoint page, details about the authentication parameters at Token Endpoint, and information about the available API endpoints at API Reference page.
Samples shown below are not meant to be production-ready code or to be used as-is. They are meant to illustrate the use of an API or feature in the simplest way possible. For this reason, these samples make certain assumptions on hosting, authentication, request/response handling etc.
It is of utmost importance that the fetched access token is cached by the client for the duration of its validity and reused for subsequent requests. The Switch Authorization Server will soon start imposing rate limits on the token endpoint which when reached will reject the requests to fetch an access token for a given time period.
Please note that the samples provided below do not do any caching of the access token, they are as stated, just a sample that should not be used in production environments.
Samples
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace ConsoleAppOadr3SamplesDocs
{
internal class Application
{
private const string TokenProviderUrl = "<token_provider_url>";
private const string ClientId = "<your_client_id>";
private const string ClientSecret = "<your_client_secret>";
private const string GrantType = "client_credentials";
private const string Audience = "<audience>";
private const string VtnUrl = "<vtn_url>";
static async Task Main()
{
// NOTE: This is just a very simple example for authenticating.
// Ideally you would use an established library instead, allowing token re-use
// and refresh token when needed.
var accessToken = await GetAccessToken();
// NOTE: Another way to get access token is to call the Auth endpoint of the VTN but
// this way is to be used for testing purposes only.
// Search for available programs.
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await client.GetAsync($"{VtnUrl}/programs");
var content = await response.Content.ReadAsStringAsync();
var programs = JsonSerializer.Deserialize<Program[]>(content);
var program = programs.FirstOrDefault();
// Search for available events with filters.
response = await client.GetAsync($"{VtnUrl}/events?programID={program.Id}&skip=1&limit=5");
content = await response.Content.ReadAsStringAsync();
var events = JsonSerializer.Deserialize<Event[]>(content);
// Create subscription.
var subscription = new Subscription
{
ProgramId = program.Id,
ClientName = "MyVEN",
ObjectOperations = new List<SubscriptionObjectOperation>
{
new()
{
Objects = new List<string> { "EVENT", "REPORT" },
Operations = new List<string> { "POST", "PUT", "DELETE" },
CallbackUrl = "https://mycompany.com/callback"
}
}
};
var payload = new StringContent(JsonSerializer.Serialize(subscription), Encoding.UTF8, "application/json");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Add("Accept", "application/json");
await client.PostAsync($"{VtnUrl}/subscriptions", payload);
}
private static async Task<string> GetAccessToken()
{
using var client = new HttpClient();
var response = await client.PostAsync(TokenProviderUrl, new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", GrantType },
{ "client_id", ClientId },
{ "client_secret", ClientSecret },
{ "audience", Audience }
}));
var content = await response.Content.ReadAsStringAsync();
var token = JsonSerializer.Deserialize<Token>(content);
return token.AccessToken;
}
private class Token
{
[JsonPropertyName("access_token")]
public string AccessToken { get; init; }
}
private class Program
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("programName")]
public string ProgramName { get; set; }
// Remaining fields omitted for this example, see the API reference documentation for the full list.
}
private class Event
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("programID")]
public string ProgramId { get; set; }
[JsonPropertyName("eventName")]
public string EventName { get; set; }
// Remaining fields omitted for this example, see the API reference documentation for the full list.
}
private class Subscription
{
[JsonPropertyName("clientName")]
public string ClientName { get; set; }
[JsonPropertyName("programID")]
public string ProgramId { get; set; }
[JsonPropertyName("objectOperations")]
public List<SubscriptionObjectOperation> ObjectOperations { get; set; }
// Remaining fields omitted for this example, see the API reference documentation for the full list.
}
private class SubscriptionObjectOperation
{
[JsonPropertyName("objects")]
public List<string> Objects { get; set; }
[JsonPropertyName("operations")]
public List<string> Operations { get; set; }
[JsonPropertyName("callbackUrl")]
public string CallbackUrl { get; set; }
// Remaining fields omitted for this example, see the API reference documentation for the full list.
}
}
}const VTN_URL = 'https://qa-vtn3.switchmarket.se/api/openadr/v1';
const POLL_MS = 10_000;
// See https://developer.switchmarket.se/openadr-3/authentication/vtn-credentials
// and https://developer.switchmarket.se/openadr-3/authentication/token-endpoint
const TOKEN_URL = '';
const AUDIENCE = '';
const CLIENT_ID = '';
const CLIENT_SECRET = '';
async function run() {
// A proper integration should utilize an oauth client library to be able to utilize refresh tokens
// and more, e.g https://github.com/panva/node-openid-client
const tokenResponse = await fetch(TOKEN_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')
},
body: `grant_type=client_credentials&audience=${AUDIENCE}`
});
if (!tokenResponse.ok) {
console.error(await tokenResponse.json());
process.exit(1);
}
const token = (await tokenResponse.json()).access_token;
async function readEvents() {
const params = new URLSearchParams({ 'x-start': new Date().toISOString() }).toString();
const eventResponse = await fetch(`${VTN_URL}/events?${params}`, {
headers: {
authorization: `Bearer ${token}`
}
});
if (!eventResponse.ok) {
console.error(await eventResponse.json());
process.exit(1);
}
const events = await eventResponse.json();
if (events.length > 0) {
console.log();
console.log(`Found ${events.length} event(s)...`)
} else {
console.log(`Found no events... Trying again in ${POLL_MS} ms`);
}
for (const event of events) {
const resourceId = event.targets.find(t => t.type === 'RESOURCE_NAME').values[0];
const payloadLimitVersion = event.intervals[0].payloads.find(p => p.type === 'POWER_LIMIT_VERSION').values[0];
console.log();
console.log(`ID = ${event.id}`);
console.log(`RESOURCE ID = ${resourceId}`)
console.log(`START = ${event.intervalPeriod.start}`);
console.log(`DURATION = ${event.intervalPeriod.duration}`);
// For a production asset the payload type will instead be 'PRODUCTION_POWER_LIMIT'
console.log(`LIMIT = ${event.intervals[0].payloads.find(p => p.type === 'CONSUMPTION_POWER_LIMIT').values[0]} KW`);
console.log(`LIMIT VERSION = ${payloadLimitVersion}`);
// See https://developer.switchmarket.se/openadr-3/payloads#openadr-acknowledgement-report
if (event.reportDescriptors.some(r => r.payloadType === 'POWER_LIMIT_ACKNOWLEDGEMENT')) {
const params = new URLSearchParams({ 'eventID': event.id }).toString();
const reportResponse = await fetch(`${VTN_URL}/reports?${params}`, {
headers: {
authorization: `Bearer ${token}`
}
});
if (!reportResponse.ok) {
console.error(await reportResponse.json());
process.exit(1);
}
const reports = await reportResponse.json();
// Check for existing acknowledgement - each event should only be acknowledged once for each power limit version
const acks = reports.filter(r => r.payloadDescriptors.some(pd => pd.payloadType === 'POWER_LIMIT_ACKNOWLEDGEMENT'));
const ackedVersions = acks.map(r => r.resources[0].intervals[0].payloads.find(p => p.type === 'POWER_LIMIT_ACKNOWLEDGEMENT').values[0]);
if (!ackedVersions.includes(payloadLimitVersion)) {
console.log();
if (acks.length > 0) {
console.log(`Event has been acknowledged for power limit version(s) ${ackedVersions.join(', ')}, but the version is now ${payloadLimitVersion}`);
}
console.log('Sending acknowledgement...');
const ackResponse = await fetch(`${VTN_URL}/reports`, {
method: 'POST',
headers: {
authorization: `Bearer ${token}`,
'content-type': 'application/json'
},
body: JSON.stringify({
eventID: event.id,
programID: event.programID,
clientName: "<client name>",
payloadDescriptors: [{
payloadType: "POWER_LIMIT_ACKNOWLEDGEMENT"
}],
resources: [{
resourceName: resourceId,
intervalPeriod: event.intervalPeriod,
intervals: [{
id: 0,
payloads: [{
type: "POWER_LIMIT_ACKNOWLEDGEMENT",
values: [payloadLimitVersion]
}]
}]
}]
})
});
if (!ackResponse.ok) {
console.error(await ackResponse.json());
process.exit(1);
} else {
console.log('Acknowledgement sent sucessfully');
}
} else {
console.log();
console.log('Event already acknowledged');
}
}
}
}
await readEvents();
setInterval(readEvents, POLL_MS);
}
run();Last updated
Was this helpful?