Cloud Integrations Expressions - SolarNetwork/solarnetwork GitHub Wiki
Cloud Datum Stream Mapping Property entities can be configured as a dynamic expression that can perform a calculation on the acquired cloud data values.
Expressions are evaluated after all datum for a given Cloud Datum Stream request have been resolved. Each expression configured on the Cloud Datum Stream Mapping is evaluated in the order defined, against all resolved datum. Thus the expression will be evaulated against each datum's resolved node (or location) and source ID values and all property values as resolved by the Cloud Datum Stream Mapping's non-expression references.
For example, imagine a Cloud Datum Stream acquires a datum with properties like this:
{
"created": "2024-11-13 04:20:00Z",
"nodeId": 123,
"sourceId": "test/meter",
"i": {
"voltage_a": 287.652,
"voltage_b": 287.652,
"voltage_c": 287.318
}
}
Here the meter has provided 3-phase voltage properties, but you would like the overall voltage as
well, on a voltage
property. You can use an expression property to do that, like this:
{
"enabled": true,
"propertyType": "i",
"propertyName": "voltage",
"valueType": "s",
"valueReference": "round(rms({voltage_a, voltage_b, voltage_c}), 1)"
}
The valueType
value s
means Spel Expression and the valueReference
value is the actual
expression. In this example, a rms()
function is passed the three phase voltage values to
calculate the root-mean-square value, which is then passed to round()
to round the result to at
most one decimal place.
After the expression is evaulated, the resulting datum then looks like this:
{
"created": "2024-11-13 04:20:00Z",
"nodeId": 123,
"sourceId": "test/meter",
"i": {
"voltage_a": 287.652,
"voltage_b": 287.652,
"voltage_c": 287.318,
"voltage": 287.5
}
}
As each expression Cloud Datum Stream Mapping Property is evaulated against all resolved datum
from each Cloud Datum Stream request, for integrations that resolve multiple datum streams at
once you might want to restrict the expression to apply only to one specific stream (or set of
streams). You can do that in the expression by making use of the sourceId
property that is
available to the expression.
For example, imagine the integration produces several INV/*
streams like INV/1
, INV/2
, and
so on for a set of solar inverters, along with a GEN/1
stream for an energy meter that tracks
the overall energy production of all meters. You'd like to include a sum total of the inverter
power output on the meter stream as a invTotPower
property to compare with the value recorded
by the energy meter, which could be done with an expression like this:
sourceId == "GEN/1"
? sum(latestMatching('INV/*', timestamp).![watts])
: null
Expressions can be used to derive new datum streams from mapped datum streams, by configuring a
virtualSourceIds
service property on the Cloud Datum Stream associated with the expression. This
service property can be configured as a single virtual source ID, a comma-delimited list of
virtual source IDs, or an actual array of virtual source IDs.
A virtual source ID is simply a made-up source ID, not already present on the Cloud Datum
Stream. You could use this to derive a datum stream out of an expression that aggregates
multiple actual datum streams, for example summing a watts
property on inverter datum
streams into a total watts
property on a virtual datum stream.
For example, here is a Cloud Datum Stream configuration that generates two inverter datum
streams S1/INV
and S2/INV
and one virtual datum stream TOTAL
:
{
"enabled": true,
"name": "SMA Test",
"datumStreamMappingId": 1,
"schedule": "1800",
"kind": "n",
"objectId": 123,
"sourceId": "unused",
"serviceIdentifier": "s10k.c2c.ds.sma",
"serviceProperties": {
"sourceIdMap": {
"/12345/67890": "S1/INV",
"/23456/78901": "S2/INV"
},
"virtualSourceIds": "TOTAL"
}
}
An expression like the following could be used to populate a watts
property on the generated TOTAL
datum stream:
sourceId == 'TOTAL'
? sum(latestMatching('*/INV', timestamp).![watts])
: null
All virtual datum streams will share the kind and object ID of the Cloud Datum Stream they
are defined on. In the previous section's example the virtual datum stream would be for node
ID 123
becuase of these configuration properties:
{
"kind": "n",
"objectId": 123
}
SolarNetwork will generate virtual datum for every unique timestamp generated by the Cloud Datum Stream mapping. In the previous section's example, imagine datum for the two inverter streams are captured like the following:
Timestamp | Source ID | Watts |
---|---|---|
14:00 |
S1/INV |
1000 |
14:00 |
S2/INV |
1200 |
14:30 |
S1/INV |
1350 |
14:30 |
S2/INV |
1295 |
The TOTAL
virtual datum stream would then generate the following additional datum:
Timestamp | Source ID | Watts |
---|---|---|
14:00 |
TOTAL |
2200 |
14:30 |
TOTAL |
2645 |
"Spel" stands for Spring Expression Language. See the Spring Expression Language Syntax guide for more details.
The Cloud Datum Stream expression support is very similar to SolarNode Datum Expressions. Expressions can refer to datum properties directly, and many helper functions are provided.
Some functions return ExpressionRoot
objects. An ExpressionRoot
object supports all the
properties and functions described in this guide.
The following properties are available:
Property | Type | Description |
---|---|---|
datum |
Datum |
A Datum object, in case you need direct access to the functions provided there. |
node |
NodeInfo |
A NodeInfo object, for node datum streams only. This can be used to obtain the node's time zone, for example. |
locId |
Number |
For location datum streams, the location ID of the datum. |
nodeId |
Number |
For node datum streams, the node ID of the datum. |
props |
Map<String,Object> |
Simple map based access to all properties in datum . As datum properties are also available directly as variables, this is rarely needed but can be helpful for accessing dynamically-calculated property names or properties with names that are not legal variable names. |
sourceId |
String |
The source ID of the datum stream. |
timestamp |
Instant |
The date of the datum. |
The NodeInfo
object has the following properties:
Property | Type | Description |
---|---|---|
nodeId |
long |
A node ID. |
userId |
long |
The ID of an account owner. |
country |
String |
The ISO 3166-1 alpha 2-character country code associated with the node, for example NZ . |
zone |
ZoneId |
The time zone associated with the node. Useful with date time functions, for example now(node.zone) will return the current node-local time. |
The following sections detail the functions that are available to Cloud Datum Stream expressions.
The same bitwise integer manipulation functions provided in SolarNode are available.
Function | Arguments | Result | Description |
---|---|---|---|
union(map1, map2...) |
Map , Map...
|
Map |
Combine the properties of two or more maps into a new map. Later map argument values will replace earlier map argument values. |
The same date time functions provided in SolarNode are available.
Often Cloud Datum Streams acquire multiple datum at once, for example multiple datum for a single datum stream over time, or multiple datum streams, or both. The following functions make it possible to perform calculations that draw from values in any of the available datum streams.
For example, imagine a Cloud Datum Stream that collects two datum streams, one from a weather station and another from a PV inverter. You could configure an expression on the PV inverter stream that uses data from the weather stream, perhaps a calculation of the expected PV generation based on the weather conditions. Here's a contrived example that multiplies a "cloudiness" percentage collected by the weather station by a pre-computed expected output rate provided by a tariff schedule in the node's metadata:
latest('weather').cloudiness
* resolveTariffScheduleRate(nodeMetadata(), '/pm/modelled-output', now(node.zone))
For another example, imagine a Cloud Datum Stream that collects many PV inverter datum streams, with
source IDs like inv/1
, inv/2
, and so on. You could use an expression to sum up the total
combined output of all inverter watts
properties like this:
sum(latestMatching('inv/*', timestamp).![watts])
💡 The
timestamp
property is a useful value to pass for thedate
argument in these functions; the effect is to find datum from other streams that are near (in time) to the current stream. For examplelatestMatching('inv/*', timestamp)
will find allinv/X
datum nearest in time to the datum currently being evaulated.
👇 Note that the
date
argument is optional in some of these functions. When omitted the functions use the latest available data as the time reference point from which offsets are calculated. When provided the given date becomes the time reference point.
👇 The
pattern
argument is a wildcard-style pattern.
The following functions test the availability of datum matching certain criteria. Generally they are each closely associated with a datum stream query function.
Function | Arguments | Result | Description |
---|---|---|---|
hasDatumAt(source, date) |
String , Instant
|
boolean |
Returns true if datumAt(source, date) returns a value. |
hasDatumAtMatching(pattern, date) |
String , Instant
|
boolean |
Returns true if datumAtMatching(pattern, date) returns a non-empty result. |
hasLatest(source, date) |
String , [Instant ] |
boolean |
Returns true if latest(source, date) returns a value. |
hasLatestMatching(pattern, date) |
String , [Instant ] |
boolean |
Returns true if latestMatching(pattern, date) returns a non-empty result. |
hasOffset(offset, date) |
int , [Instant ] |
boolean |
Returns true if offset(offset) returns a value. |
hasOffset(source, offset, date) |
String , int , [Instant ] |
boolean |
Returns true if offset(source,offset) returns a value. |
hasOffsetMatching(pattern, offset, date) |
String , int , [Instant ] |
boolean |
Returns true if offsetMatching(pattern, offset) returns a non-empty result. |
Function | Arguments | Result | Description |
---|---|---|---|
datumAt(source, date) |
String , Instant
|
ExpressionRoot |
Provides access to a datum matching a source ID and exact timestamp, or null if not available. |
datumAtMatching(pattern, date) |
String , Instant
|
ExpressionRoot[] |
Return a collection of datum matching a source ID wildcard pattern and exact timestamp. |
deltaAt(source, date, key, toZero) |
String , Instant , String , [boolean ] |
Number |
For the datum stream with source ID source , calculate the difference of the datum property key between date and its value on the next earlier datum within the stream. The date value is compared exactly, as in the datumAt(source, date) function, and the next earlier datum is discovered with the same logic as the offset(source, 1, date) function. Put differently, if a datum d1 is found for the given source and date , and an earlier datum d2 is also found, then the returned value is d1[key] - d2[key]. If a next earlier datum is not available, then when toZero is omitted or false , return the current datum's key value, otherwise return 0 . |
latest(source, date) |
String , [Instant ] |
ExpressionRoot |
Provides access to the latest available datum matching a source ID, or null if not available. This is a shortcut for calling offset(source,0,date) . |
latestMatching(pattern, date) |
String , [Instant ] |
ExpressionRoot[] |
Return a collection of the latest available datum matching a source ID wildcard pattern. This is a shortcut for calling offsetMatching(pattern,0,date) . |
offset(offset, date) |
int , [Instant ] |
ExpressionRoot |
Provides access to a datum from the same stream as the current datum, offset by offset in time, or null if not available. Offset 1 means the next earlier datum with the same source ID as this datum, and so on. |
offset(source, offset, date) |
String , int, [ Instant]
|
ExpressionRoot |
Provides access to an offset from the latest available datum matching the given source ID, or null if not available. Offset 0 represents the "latest" datum, 1 the one before that, and so on. |
offsetMatching(pattern, offset, date) |
String , int , [Instant ] |
ExpressionRoot[] |
Return a collection of datum matching a given source ID wildcard pattern, each offset by offset in time. Offset 0 represents the "latest" datum, 1 the next earlier, and so on. |
-
The
*Matching()
functions only match source IDs of datum in the current request. Functions likehasOffsetMatching()
andoffsetMatching()
fall into this category. That means they will not find datum previously stored in SolarNetwork for source IDs not also present in the current request. -
The
hasOffset()
,offset()
,hasLatest()
, andlatest()
functions have access to datum previously stored in SolarNetwork, including sources not present in the current request. That means you can reference datum from past requests or even datum collected by other Cloud Integrations or SolarNode devices. -
The offset functions, when querying datum previously stored in SolarNetwork, will search backwards in time at most 90 days for a previous datum, and will include at most 100 datum. That means the maximum offset you can query is 100 from the oldest datum in the current request.
-
All functions are restricted to the node or location ID configured on the Cloud Datum Stream configuration.
Here is an expression that derives an accumulating irradianceHours
property on a datum stream that
has an irradiance
property, similar to what the SolarNode Virtual Meter
filter does.
It calculates the average irradiance value between the current and previous datum, multiplied by the
fractional number of hours between the two, and adds that to the previous datum's calculated value.
The result is that irradianceHours
becomes an ever-increasing "irradiance energy meter" property.
hasOffset(1, timestamp) && offset(1, timestamp).props['irradianceHours'] != null
? offset(1, timestamp).irradianceHours + round(
(secondsBetween(offset(1, timestamp).timestamp, timestamp) / 3600.0)
* avg({offset(1, timestamp).irradiance, irradiance})
)
: 0
Here is an expression that computes a delta aggregation of a wattHours
property across multiple
inv/*
datum streams. This can be used to track the overall energy of a set of inverters, when
no other overall meter source is available. Note that this expression assumes all datum are
captured at regular timestamps, which is often the case with Cloud Integrations.
hasOffset(1, timestamp) && offset(1, timestamp)['wattHours'] != null
? offset(1, timestamp).wattHours + sum(datumAtMatching('inv/*', timestamp).![
deltaAt(sourceId, timestamp, 'wattHours')
])
: has('wattHours')
? wattHours
: 0
Both datum stream and node metadata for the datum the expression is evaulating can be accessed with the following functions:
Function | Arguments | Result | Description |
---|---|---|---|
metadata() |
MetadataOperations |
Returns a MetadataOperations object for the metadata associated with the datum's stream. | |
metadata(path) |
String |
Object |
Extract the value at a metadata key path on the metadata associated with the datum's stream. |
nodeMetadata() |
MetadataOperations |
Returns a MetadataOperations object for the metadata associated with the datum's node ID. For location datum this returns null . |
|
nodeMetadata(path) |
String |
Object |
Extract the value at a metadata key path on the metadata associated with the datum's node ID. For location datum this returns null . |
tariffSchedule(meta, path) |
MetadataOperations , String
|
TariffSchedule |
Extracts a TariffSchedule object from metadata at a given path. |
resolveTariffScheduleRate(meta, path, date, rateName) |
MetadataOperations , String , LocalDateTime , String
|
Object |
Resolve a tariff schedule rate from metadata at a given path, date, and tariff rate name. The date and rateName arguments are optional. If no date is given then the current time in the UTC time zone will be used. If no rateName is given then the first-available resolved rate will be returned. |
A MetadataOperations
object provides the same properties and functions as
SolarNode Datum metadata.
A TariffSchedule
object represents a set of time-based criteria with associated tariffs. A tariff
in this scheme is nothing more than a set of one or more number values, each with an associated
name. See the SolarNode TariffSchedule object
functions
guide for details on the properties and functions that are avaialble on a TariffSchedule
object.
Function | Arguments | Result | Description |
---|---|---|---|
httpBasic(user, pass) |
String , String
|
String |
Generate an HTTP Authorization header value for the Basic authentication scheme. |
httpGet(uri, params, headers) |
String , [Map , Map ] |
Result<Map> |
Make a HTTP GET request for JSON content. Both params and headers are optional. Returns a Result object with a Map value for the data property. |
A Result<T>
object is the General Response as returned by the SolarNetwork API.
For example, the Result<Map>
returned from httpGet()
might look like this:
{
"success": true,
"data": {
"answer": "thing"
}
}
In an expression, you could refer to answer
like this:
httpGet('http://example.com/thing')?.data?.answer
Here is an expression example that makes use of node metadata to provide query parameters for a web service that can calculate POA irradiance from a GHI irradiance value. It includes Basic authentication as well:
has('irradiance') ? httpGet(
'https://example.com/ghi-to-poa',
union(nodeMetadata('/pm/pv-characteristics'), {
date: local(timestamp.atZone(nodeMetadata('/pm/pv-characteristics/zone'))),
irradiance: irradiance
}), {
'Authorization': httpBasic('foo', 'bar')
}
)?.data?.poa_global : null
- First
has('irradiance')
checks for the existance of anirradiance
property. - Then
httpGet()
is called.- Query parameter are provided via a
union()
function that combines a node metadata object at/pm/pv-characteristics
with a literal map containing the datum'stimestamp
(converted to the time zone specified in node metadata at/pm/pv-characteristics/zone
) and the datum'sirradiance
property. - Headers are provided as a literal map with
Authorization
set to HTTP Basic authorization value generated by thehttpBasic()
function.
- Query parameter are provided via a
- The HTTP result value
poa_global
is then returned (the?.data?.poa_global
snippet).
The same math functions provided in SolarNode are available.
The same property functions provided in SolarNode are available.
The following functions provide access to user secret values. The secret topicId
must be c2c/i9n
, which means you must configure a key pair
with that value as the key pair key
(not to be confused by the key
arguments below, which refer to a secret key).
Function | Arguments | Result | Description |
---|---|---|---|
secret(key) |
String |
String |
Decrypt a user secret with key key as a string. |
secretData(key) |
String |
byte[] |
Decrypt a user secret with key key as bytes. |