Resources
Now that we have a site, we can add flexible resources to it. This step is crucial for providing flexibility and enables our platform to accurately gauge activation rates, resulting in more activations for your resources. π
Typesβ
There's a long list of resources (devices, assets) that can be flexible, the most common ones are listed below.
The Powernaut platform is indifferent to the type of resource, meaning all types can be registered. However, the type of asset is a required data point to adequately activate flexibility.
- Batteries
- Electric Vehicles (EV)
- Charging stations for EVs
- Heat pumps
- Solar panels
- Electrical boilers
- Windmill
- Groups (beta) - Aggregated resources representing hybrid sites
- Meters β Observational only: no bids, no activation. Useful to model loads or production that don't offer flexibility.
A "head meter" is a special type of meter. It measures the total consumption or production of a site, but we don't expect you to create head meters as resources! It's enough to send head meter data over MQTT.
Managingβ
Creatingβ
To create a new flexible resource, you need at least its site, type and power constraints.
- Python
- JavaScript
- Java
- Go
- C#
- cURL
import requests
import json
url = "https://api.powernaut.io/v1/connect/resources"
payload = json.dumps({
"site_id": "<uuid>",
"type": "electric_vehicle_charging_point",
"power": {
"active": {
"minimum": "0",
"maximum": "7.4"
}
}
})
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer <token>'
}
response = requests.request("POST", url, headers=headers, data=payload)
print(response.text)
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Accept", "application/json");
myHeaders.append("Authorization", "Bearer <token>");
const raw = JSON.stringify({
"site_id": "<uuid>",
"type": "electric_vehicle_charging_point",
"power": {
"active": {
"minimum": "0",
"maximum": "7.4"
}
}
});
const requestOptions = {
method: "POST",
headers: myHeaders,
body: raw,
redirect: "follow"
};
fetch("https://api.powernaut.io/v1/connect/resources", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result))
.catch((error) => console.error(error));
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\"site_id\":\"<uuid>\",\"type\":\"electric_vehicle_charging_point\",\"power\":{\"active\":{\"minimum\":\"0\",\"maximum\":\"7.4\"}}}");
Request request = new Request.Builder()
.url("https://api.powernaut.io/v1/connect/resources")
.method("POST", body)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.addHeader("Authorization", "Bearer <token>")
.build();
Response response = client.newCall(request).execute();
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.powernaut.io/v1/connect/resources"
method := "POST"
payload := strings.NewReader(`{"site_id":"<uuid>","type":"electric_vehicle_charging_point","power":{"active":{"minimum":"0","maximum":"7.4"}}}`)
client := &http.Client {
}
req, err := http.NewRequest(method, url, payload)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", "Bearer <token>")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.powernaut.io/v1/connect/resources");
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Authorization", "Bearer <token>");
var content = new StringContent("{\"site_id\":\"<uuid>\",\"type\":\"electric_vehicle_charging_point\",\"power\":{\"active\":{\"minimum\":\"0\",\"maximum\":\"7.4\"}}}", null, "application/json");
request.Content = content;
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
curl --location 'https://api.powernaut.io/v1/connect/resources' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer <token>' \
--data '{"site_id":"<uuid>","type":"electric_vehicle_charging_point","power":{"active":{"minimum":"0","maximum":"7.4"}}}'
You'll notice that throughout the API, numbers are communicated with a string datatype, instead of the usual number.
This is on purpose, and is done to avoid floating point errors.
Number types will be rejected by the API.
Updateβ
You can also update a resource, e.g. update its name.
- Python
- JavaScript
- Java
- Go
- C#
- cURL
import requests
import json
url = "https://api.powernaut.io/v1/connect/resources/<uuid>"
payload = json.dumps({
"name": "Home Battery"
})
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer <token>'
}
response = requests.request("PATCH", url, headers=headers, data=payload)
print(response.text)
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Accept", "application/json");
myHeaders.append("Authorization", "Bearer <token>");
const raw = JSON.stringify({
"name": "Home Battery"
});
const requestOptions = {
method: "PATCH",
headers: myHeaders,
body: raw,
redirect: "follow"
};
fetch("https://api.powernaut.io/v1/connect/resources/<uuid>", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result))
.catch((error) => console.error(error));
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\"name\":\"Home Battery\"}");
Request request = new Request.Builder()
.url("https://api.powernaut.io/v1/connect/resources/<uuid>")
.method("PATCH", body)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.addHeader("Authorization", "Bearer <token>")
.build();
Response response = client.newCall(request).execute();
package main
import (
"fmt"
"strings"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.powernaut.io/v1/connect/resources/<uuid>"
method := "PATCH"
payload := strings.NewReader(`{"name":"Home Battery"}`)
client := &http.Client {
}
req, err := http.NewRequest(method, url, payload)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", "Bearer <token>")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Patch, "https://api.powernaut.io/v1/connect/resources/<uuid>");
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Authorization", "Bearer <token>");
var content = new StringContent("{\"name\":\"Home Battery\"}", null, "application/json");
request.Content = content;
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
curl --location --request PATCH 'https://api.powernaut.io/v1/connect/resources/<uuid>' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer <token>' \
--data '{"name":"Home Battery"}'
Deleteβ
Deleting a resource is done similarly to sites:
- Python
- JavaScript
- Java
- Go
- C#
- cURL
import requests
url = "https://api.powernaut.io/v1/connect/resources/<uuid>"
payload = {}
headers = {
'Accept': 'application/json',
'Authorization': 'Bearer <token>'
}
response = requests.request("DELETE", url, headers=headers, data=payload)
print(response.text)
const myHeaders = new Headers();
myHeaders.append("Accept", "application/json");
myHeaders.append("Authorization", "Bearer <token>");
const requestOptions = {
method: "DELETE",
headers: myHeaders,
redirect: "follow"
};
fetch("https://api.powernaut.io/v1/connect/resources/<uuid>", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result))
.catch((error) => console.error(error));
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("text/plain");
RequestBody body = RequestBody.create(mediaType, "");
Request request = new Request.Builder()
.url("https://api.powernaut.io/v1/connect/resources/<uuid>")
.method("DELETE", body)
.addHeader("Accept", "application/json")
.addHeader("Authorization", "Bearer <token>")
.build();
Response response = client.newCall(request).execute();
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.powernaut.io/v1/connect/resources/<uuid>"
method := "DELETE"
client := &http.Client {
}
req, err := http.NewRequest(method, url, nil)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", "Bearer <token>")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Delete, "https://api.powernaut.io/v1/connect/resources/<uuid>");
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Authorization", "Bearer <token>");
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
curl --location --request DELETE 'https://api.powernaut.io/v1/connect/resources/<uuid>' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer <token>'
Deleting a resource is a permanent action and will delete all future flexibility. Ensure this is the intended action. The API will reject the creation of bids and baselines for resources that have been deleted.
In the case that a bid was selected for a procurement in the future, this bid will also be cancelled and the resource will not be activated.
Listingβ
You can fetch a resource by its ID, or list all of them.
Listing all Resourcesβ
- Python
- JavaScript
- Java
- Go
- C#
- cURL
import requests
url = "https://api.powernaut.io/v1/connect/resources"
payload = {}
headers = {
'Accept': 'application/json',
'Authorization': 'Bearer <token>'
}
response = requests.request("GET", url, headers=headers, data=payload)
print(response.text)
const myHeaders = new Headers();
myHeaders.append("Accept", "application/json");
myHeaders.append("Authorization", "Bearer <token>");
const requestOptions = {
method: "GET",
headers: myHeaders,
redirect: "follow"
};
fetch("https://api.powernaut.io/v1/connect/resources", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result))
.catch((error) => console.error(error));
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("text/plain");
RequestBody body = RequestBody.create(mediaType, "");
Request request = new Request.Builder()
.url("https://api.powernaut.io/v1/connect/resources")
.method("GET", body)
.addHeader("Accept", "application/json")
.addHeader("Authorization", "Bearer <token>")
.build();
Response response = client.newCall(request).execute();
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.powernaut.io/v1/connect/resources"
method := "GET"
client := &http.Client {
}
req, err := http.NewRequest(method, url, nil)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", "Bearer <token>")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.powernaut.io/v1/connect/resources");
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Authorization", "Bearer <token>");
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
curl --location 'https://api.powernaut.io/v1/connect/resources' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer <token>'
Listing a Site's Resourcesβ
- Python
- JavaScript
- Java
- Go
- C#
- cURL
import requests
url = "https://api.powernaut.io/v1/connect/resources"
payload = {}
headers = {
'Accept': 'application/json',
'Authorization': 'Bearer <token>'
}
response = requests.request("GET", url, headers=headers, data=payload)
print(response.text)
const myHeaders = new Headers();
myHeaders.append("Accept", "application/json");
myHeaders.append("Authorization", "Bearer <token>");
const requestOptions = {
method: "GET",
headers: myHeaders,
redirect: "follow"
};
fetch("https://api.powernaut.io/v1/connect/resources", requestOptions)
.then((response) => response.text())
.then((result) => console.log(result))
.catch((error) => console.error(error));
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
MediaType mediaType = MediaType.parse("text/plain");
RequestBody body = RequestBody.create(mediaType, "");
Request request = new Request.Builder()
.url("https://api.powernaut.io/v1/connect/resources")
.method("GET", body)
.addHeader("Accept", "application/json")
.addHeader("Authorization", "Bearer <token>")
.build();
Response response = client.newCall(request).execute();
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
url := "https://api.powernaut.io/v1/connect/resources"
method := "GET"
client := &http.Client {
}
req, err := http.NewRequest(method, url, nil)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("Accept", "application/json")
req.Header.Add("Authorization", "Bearer <token>")
res, err := client.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.powernaut.io/v1/connect/resources");
request.Headers.Add("Accept", "application/json");
request.Headers.Add("Authorization", "Bearer <token>");
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
curl --location 'https://api.powernaut.io/v1/connect/resources' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer <token>'
Resource Groupsβ
Groups let you combine multiple physical resources into a single flexible unit for bidding. A group is a logical construct and has no meter data of its own. Members of a group activate together as a unit and their flexibility is aggregated into a single bid.
A group can be assigned to pools and activated, with its power and capacity values representing the aggregate across its members.
Creating Groupsβ
Groups are created as resources with type: "group". When creating a group,
you must manually specify the power constraintsβthe platform does not
automatically calculate these from group members.
- Power constraints: Provide the combined import/export capability in kW
using
power.active.minimum(for export/consumption) andpower.active.maximum(for import/production). These values represent the aggregate across all underlying assets. - Capacity (optional): If the group includes batteries, provide the total
storage capacity in kWh using
capacity.maximum.
All group members must belong to the same site.
Adding Members to Groupsβ
Add resources to a group by setting the group_id field when creating or
updating a resource:
- When creating a resource: include
group_idin the request body - When updating a resource: set
group_idto the group's identifier, or set it tonullto remove a resource from its existing group
Behaviourβ
- Meter data is expected on the individual member resources, not the group itself β the group has no physical measurement point. Do not submit streaming data against the group's identifier.
- Availability and overall status on the group are derived from its members. Individual members retain their own statuses.
- Power and capacity constraints must be specified manually and updated when members are added or removed β the platform does not recalculate them automatically.
Resource Parentsβ
Resources in a site can be arranged into a physical topology tree β a hierarchy that reflects which meter a resource sits downstream of electrically. Think of it as the physical base layout of the site. Submetering relies on this hierarchy: it tells the platform which assets each intermediate meter covers, so their data can be handled correctly.
Groups are a separate, logical overlay on top of that layout. They bundle resources for flexibility bidding and have no interaction with the parent-child tree at all.
A typical use case is a site with a main meter at the grid connection point and several sub-meters beneath it, each measuring a distinct subset of generation or consumption. Only meter-type resources can be parents β the hierarchy models how meters are physically wired, not arbitrary resource relationships.
Set this relationship using the parent_id field on a resource.
Functional effectsβ
For most sites, setting parent_id is purely visual: it controls how the site layout is rendered in the portal, with no effect on bidding or activation.
The one functional exception is virtual head metering. When a site has infer_meter_data_from_resources enabled, the platform derives the head meter reading by summing meter data from resources at that site. Resources with a parent_id set are excluded from this sum β their parent meter is included instead, because the meter already physically measures everything downstream of it. Including both the meter and its children would double-count. If your site does not use virtual head metering, this has no effect.
How it differs from groupsβ
| Concept | Field | Purpose | Structure |
|---|---|---|---|
| Physical topology | parent_id | Electrical layout, virtual metering | Tree (up to 10 levels deep) |
| Logical grouping | group_id | Bundle resources for flexibility bidding | One level (member β group) |
A non-group resource can have both a parent_id and a group_id at the same time β the two fields are independent. Group resources are outside the parent-child tree entirely: they cannot have a parent_id, and they cannot be set as the parent_id of another resource.
Setting a parentβ
Set parent_id when creating or updating a resource. Set it to null to remove the relationship.
Rulesβ
- Only
meter-type resources can be a parent. - The parent must be on the same site as the resource.
- A resource cannot be its own parent.
- Cycles are not allowed. A cycle would mean resource A is an ancestor of B and B is an ancestor of A β equivalent to a meter being electrically both upstream and downstream of another meter at the same time, which is physically impossible.
- The maximum depth is 10. Depth is the number of layers in the tree: a chain A β B β C has a depth of 3.
- Group resources are outside the parent-child tree entirely. They cannot have a
parent_id, and cannot be theparent_idof another resource.
Terminologyβ
Use parent / child for parent_id relationships. Use group / member for group_id relationships. The two concepts are distinct β a group member is not a "child" of its group.
Statusβ
We continuously monitor all resources through automated health checks. The status of all these health checks is aggregated into a single, high-level resource status, providing a quick overview of a resource's operational readiness.
| Status | Meaning |
|---|---|
onboarding | The resource has been added to the platform, but no meter data has been received yet. |
operational | All health checks are successful. |
degraded | Some health checks are failing. This causes suboptimal steering. |
suspended | The resource has failing health checks. |
βΉοΈ Note on a Future Update: For now, the statuses are purely informational to help you troubleshoot and resolve issues with your resources. In a future update, resources with status
suspendedwill no longer be eligible to have their flexibility bids activated.
Health checksβ
The Powernaut platform performs several health checks on each resource to ensure it's functioning properly and can deliver flexibility as promised.
Availabilityβ
The availability health check monitors if the Powernaut platform is receiving meter data from the resource.
| Status | Meaning |
|---|---|
initial | Health check hasn't run yet. |
online | The platform is receiving meter data. |
offline | The platform hasn't received any data for the past 15 minutes. |
Activation complianceβ
π§ We are working to add this health check
The activation compliance health check monitors if resources are accurately delivering as much flex as was requested.
| Status | Meaning |
|---|---|
initial | Health check hasn't run yet. |
compliant | The resource is delivering between 60-140% of the energy requested. |
not_compliant | Delivered energy deviates more than 40% from the energy requested for 4 consecutive quarter hours. |
Baselines too inaccurateβ
π§ We are working to add this health check
This health check monitors the accuracy of the baselines for your site and resources. Without accurate baselines we cannot verify the delivered flexibility accurately.
Keep the following in mind:
- For connection points, baselines need to be accurate even when their individual resources are not bidding, as they might be part of a larger pool that is being balanced.
- You can update baselines for any resource on a connection point up until the moment the first resource on that connection point gets activated. We encourage you to take full advantage of this window to finalise your baselines for maximum accuracy and fair settlement.
| Status | Meaning |
|---|---|
initial | Health check hasn't run yet. |
accurate | Baseline is accurate. |
inaccurate | Normalised MAE* of corresponding site's baseline is larger than 0.75 for over 1 day. |
* MAE stands for Mean Absolute Error. This is the average of the difference between the baseline and actual meter data. We normalise MAE by dividing by the average absolute consumption.
No Bidsβ
π§ We are working to add this health check
This health check monitors the bids made by a resource. If a resource has not made any bids for a long time, something is probably wrong. To see how we expect resources to bid have a look at our examples.
| Status | Meaning |
|---|---|
initial | Health check hasn't run yet. |
active | Resource is actively making bids. |
no_bids | Resource has not made any bids for a longer period. |
When we flag resources for not bidding depends on the resource type:
- Solar Panels: after 1 day
- Batteries: after 1 day
- Electric Vehicles: after 5 days
Meter data anomaliesβ
π§ We are working to add this health check
This health check monitors if resources submit weird meter data.
| Status | Meaning |
|---|---|
initial | Health check hasn't run yet. |
ok | Resource has logged meter data correctly. |
rare_anomaly | Less than 1% of meter data is outside of bounds in the last 1 day. |
frequent_anomaly | More than 1% of meter data is outside of bounds in the last 1 day. |