Fentrica Expression Language (FEXL)
Fentrica Expression Language (FEXL) is a powerful context-based expression parser and evaluator designed to manage dynamic rules and logic within the Fentrica platform. It enables users to define flexible expressions that interact with real-time datasets and automate control processes based on dynamic data.
Expression Types
An expression in FEXL can be:
- Literal: A fixed value (e.g., a number, string, or boolean).
- Variable: A named value that can be accessed or modified.
- Access Operator: Accessing object properties.
- Function Call: Calling predefined or custom functions.
- Evaluation Operator: Performing logical or arithmetic evaluations.
- Arithmetic Operation: Performing calculations such as addition, subtraction, multiplication, or division. FEXL supports standard arithmetic operations and allows you to calculate values dynamically.
Using Datasets in Schedule Definitions
FEXL supports datasets, allowing them to be used as variables in expressions. Datasets represent structured data, such as energy prices, power production, or weather forecasts. Once a dataset is defined, it becomes accessible in expressions by its variable name.
For example, if a dataset energyPrices is defined, it can be referenced directly in expressions using energyPrices.
Variables
In addition to schedule datasets, default variables are also assigned to the context.
hour - representing the date-time for the hour in which the dynamic schedule rule is running.
dayStart - representing the start of the day in which the dynamic schedule rule is running.
dayEnd - representing the end of the day in which the dynamic schedule rule is running.
Functions
FEXL supports a variety of built-in functions, including:
Get Dataset Item value
Functions to get specific dataset item value.
First
first(array|dataset[])
Returns the first element of an array. If input is dataset, it will return the first sample's value.
first([1, 2, 3]) // returns 1
first(dataset) // returns dataset[0].value
Last
last(array|dataset[])
Returns the last element of an array. If input is dataset, it will return the last sample's value.
last([1, 2, 3]) // returns 3
last(dataset) // returns dataset[dataset.length-1].value
Has
has(dataset[], isoDateTime, granularity?: 'day'|'hour' = 'hour')
Checks if there is a sample in dataset with specified timestamp and returns true or false. The optional granularity parameter defaults to 'hour', and the timestampProperty argument specifies the property holding the timestamp.
has(dataset, '2011-10-05T14:48:00.000Z')
has(dataset, '2011-10-05T14:48:00.000Z', 'day')
Find
find(dataset[], isoDateTime, granularity?: 'day'|'hour' = 'hour')
Finds the first match from a dataset using a timestamp and returns the matching entry. The optional granularity parameter defaults to 'hour', and the timestampProperty argument specifies the property holding the timestamp.
find(dataset, '2011-10-05T14:48:00.000Z') // returns dataset element sample matching 2011-10-05T14:48:00.000Z date and hour .value
find(dataset, '2011-10-05T14:48:00.000Z', 'day') // returns dataset sample matching 2011-10-05T14:48:00.000Z date .value
Dataset filtering
Dataset filter functions return a dataset with matching samples.
Filter
filter(dataset[], isoDateTime, granularity?: 'day'|'hour' = 'hour')
Returns an filtered dataset matching the specified timestamp. The optional granularity parameter defaults to 'hour', and the timestampProperty argument specifies the property holding the timestamp.
filter(dataset, '2011-10-05T14:48:00.000Z') // returns filtered dataset matching 2011-10-05T14:48:00.000Z same day and hour.
filter(dataset, '2011-10-05T14:48:00.000Z', 'day') // returns filtered matching 2011-10-05T14:48:00.000Z same day.
Between
between(dataset[], startIsoDateTime, endIsoDateTime)
Returns an array of all matching entries from the dataset.
between(dataset, '2011-10-05T12:00:00.000Z', '2011-10-05T14:00:00.000Z') // returns all dataset samples matching between (inclusive) 2011-10-05T12:00:00.000Z and 2011-10-05T14:00:00.000Z .values
Exclude
exclude(dataset[], isoDateTime[], granularity?: 'day'|'hour' = 'hour')
Returns an array of all entries from the dataset except entries matching the specified timestamps. The optional granularity parameter defaults to 'hour', and the timestampProperty argument specifies the array holding the timestamps.
exclude(dataset, ['2011-10-05T14:48:00.000Z', '2011-10-05T14:49:00.000Z']) // returns dataset samples excluding 2011-10-05T14:48:00.000Z and 2011-10-05T14:49:00.000Z
limit
limit(dataset[], items: number)
Returns an array of the specified number of items from the dataset.
If number is negative, it will return the last items number of items in reverse order.
limit(dataset, 2) // returns dataset first 2 samples
limit(dataset, -2) // returns dataset last 2 samples in reverse order
Values Manipulation
Size & Count
size(array|dataset[])count(array|dataset[])
Returns the size of an array or dataset.
size([1, 2, 3]) // returns 3
count([1, 2, 3]) // returns 3
Min
min(array|dataset[])
Returns the minimum value in an array or dataset.
min([1, 2, 3]) // returns 1
Max
max(array|dataset[])
Returns the maximum value in an array or dataset.
max([1, 2, 3]) // returns 3
Sum
sum(array|dataset[])
Returns the sum of all values in an array or dataset.
sum([1, 2, 3]) // returns 6
Avg
avg(array|dataset[])
Returns the average value of an array or dataset.
avg([1, 2, 3]) // returns 2
Abs
abs(number|datasetItem)
Returns the absolute value of a number or dataset item.
abs(-5) // returns 5
Round
round(number|datasetItem)
Returns the rounded value of a number or dataset item.
round(3.14159) // returns 3
Context store
Set
set(context, 'key', value)
Stores a numeric value in a context object for use across the entire pipeline. The context parameter refers to the storage object (currently, the only available storage is context). For Schedule Dynamic Rules, the pipeline runs for each day, and the context is reset at the beginning of each new day.
Set return value is always true.
Usage:
set(context, 'key', 1) // stores the value 1 in the context under the key 'key'. Returns true.
Get
get(context, 'key', defaultValue?)
Retrieves a numeric value stored in the context. If the key does not exist, it returns 0.
Usage:
get(context, 'key') // retrieves the value stored under 'key'. If the key 'key' does not exist, it returns 0.
get(context, 'key', 10) // retrieves the value stored under 'key'. If the key 'key' does not exist, it returns 10.
Date & Time
Before
before(isoDateTime, whatIsoDateTime, granularity?: 'day'|'hour'|'minute'|'second'|'15min' = 'hour')
Returns true if the first ISO date time is before the second ISO date time. When using '15min' granularity, both timestamps are rounded down to the nearest 15-minute interval before comparison, enabling MTU-aligned comparisons for electricity metering systems.
before('2011-10-05T14:48:00.000Z', '2011-10-05T14:49:00.000Z') // returns true
before('2011-10-05T14:48:00.000Z', '2011-10-05T14:47:00.000Z', 'day') // returns false
before('2025-01-15T10:07:00.000Z', '2025-01-15T10:18:00.000Z', '15min') // returns true (10:00 < 10:15)
After
after(isoDateTime, whatIsoDateTime, granularity?: 'day'|'hour'|'minute'|'second'|'15min' = 'hour')
Returns true if the first ISO date time is after the second ISO date time. When using '15min' granularity, both timestamps are rounded down to the nearest 15-minute interval before comparison, enabling MTU-aligned comparisons for electricity metering systems.
after('2011-10-05T14:48:00.000Z', '2011-10-05T14:47:00.000Z') // returns true
after('2011-10-05T14:48:00.000Z', '2011-10-05T14:47:00.000Z', 'day') // returns false
after('2025-01-15T10:18:00.000Z', '2025-01-15T10:07:00.000Z', '15min') // returns true (10:15 > 10:00)
Same
same(isoDateTime, whatIsoDateTime, granularity?: 'day'|'hour'|'minute'|'second'|'15min' = 'hour')
Returns true if the first ISO date time is the same as the second ISO date time. When using '15min' granularity, both timestamps are rounded down to the nearest 15-minute interval before comparison, enabling MTU-aligned comparisons for electricity metering systems.
same('2011-10-05T14:48:00.000Z', '2011-10-05T14:48:00.000Z') // returns true
same('2011-10-05T14:48:00.000Z', '2011-10-05T15:48:00.000Z', 'day') // returns true
same('2025-01-15T10:07:00.000Z', '2025-01-15T10:12:00.000Z', '15min') // returns true (both round to 10:00)
Special Functions
isEffectiveDischargeHour
isEffectiveDischargeHour(startSOC, dataset[], dischargeAmountEachHour, hour)
isEffectiveDischargeHour is a utility function designed to determine whether discharging the battery during a specific hour is effective. It evaluates the current State of Charge (SOC) and uses a sorted dataset to identify the most valuable hours for discharge based on the largest values in the dataset (e.g., energy prices, demand, etc.).
This sorting ensures that the battery is discharged during hours with the highest strategic value, maximizing profitability or aligning with operational goals.
The dataSet is sorted in descending order to prioritize hours with the largest values (e.g., highest prices or production levels). This ensures that the most beneficial hours are targeted for discharge.
Evaluates if the battery has enough SOC to sustain discharge for the current hour and subsequent hours.
Compare Current Hour with Priority Hours Determines whether the current hour falls within the top n hours, where n is the maximum number of hours the battery can sustain discharge (SOC / dischargeAmountEachHour).
isEffectiveDischargeHour(40, dataset, 10, hour);
filterEffectiveChargeHours
filterEffectiveChargeHours(currentSOC, maxSOC, dataset[], chargeAmountEachHour)
Returns most valuable hour values (eg. lowest prices) from dataset for charging the battery.
filterEffectiveChargeHours(40, 100, dataset, 10);
isEffectiveChargeHour
isEffectiveChargeHour(currentSOC, maxSOC, dataset[], chargeAmountEachHour, hour)
isEffectiveChargeHour is a utility function designed to determine whether charging the battery during a specific hour is effective. It evaluates the current State of Charge (SOC), maximum allowed SOC, and uses a sorted dataset to identify the most valuable hours for charging based on the smallest values in the dataset (e.g., lowest energy prices).
The dataSet is sorted in ascending order to prioritize hours with the smallest values (e.g., lowest prices). This ensures that the most cost-effective hours are targeted for charging.
Evaluates if the battery has enough capacity (maxSOC - currentSOC) to sustain charging for the current hour and subsequent hours.
Compare Current Hour with Priority Hours Determines whether the current hour falls within the bottom n hours, where n is the maximum number of hours the battery can sustain charging ((maxSOC - currentSOC) / chargeAmountEachHour).
isEffectiveChargeHour(40, 90, dataset, 10, hour);
filterEffectiveDischargeHours
filterEffectiveDischargeHours(currentSOC, maxSOC, dataset[], dischargeAmountEachHour)
Returns most valuable hour values (eg. highest prices or production levels) from dataset for discharging the battery.
filterEffectiveDischargeHours(40, 100, dataset, 10);
Battery Optimization Functions for 15-Minute Intervals
The following functions provide battery optimization at 15-minute granularity, enabling more precise control for systems using 15-minute MTU (Meter Time Unit) intervals. These functions work similarly to their hourly counterparts but operate on quarter-hour intervals.
filterEffectiveDischarge15Min
filterEffectiveDischarge15Min(startSOC, dataset[], dischargeAmountEach15Min, returnValuesOnly?)
Filters dataset to find optimal 15-minute intervals for battery discharge based on highest strategic values (e.g., highest prices or demand). The dataset is sorted in descending order to identify the most valuable discharge intervals.
Parameters:
startSOC(number): Starting State of Chargedataset(array): Array of timestamped data with strategic valuesdischargeAmountEach15Min(number): Amount discharged per 15-minute intervalreturnValuesOnly(boolean, optional): Return only values vs full objects (default: true)
Returns: Array of optimal discharge intervals with highest strategic value
// Find top discharge intervals based on 50 kWh starting SOC, 5 kWh per 15 min
filterEffectiveDischarge15Min(50, priceDataset, 5);
// Returns: [prices from optimal 15-min discharge intervals]
isEffectiveDischarge15Min
isEffectiveDischarge15Min(startSOC, dataset[], dischargeAmountEach15Min, timestamp)
Checks if a specific timestamp falls within an optimal discharge 15-minute interval. This function evaluates whether discharging during the given timestamp aligns with the highest-value intervals identified by the optimization algorithm.
Parameters:
startSOC(number): Starting State of Chargedataset(array): Array of timestamped data with strategic valuesdischargeAmountEach15Min(number): Amount discharged per 15-minute intervaltimestamp(string): ISO timestamp to check
Returns: Boolean - true if timestamp is in optimal discharge interval
// Check if 10:07 is an optimal discharge time
isEffectiveDischarge15Min(50, priceDataset, 5, '2025-01-15T10:07:00');
// Returns: true/false
filterEffectiveCharge15Min
filterEffectiveCharge15Min(startSOC, maxSOC, dataset[], chargeAmountEach15Min, returnValuesOnly?)
Filters dataset to find optimal 15-minute intervals for battery charging based on lowest strategic values (e.g., cheapest electricity prices). The dataset is sorted in ascending order to identify the most cost-effective charging intervals.
Parameters:
startSOC(number): Starting State of ChargemaxSOC(number): Maximum State of Chargedataset(array): Array of timestamped data with strategic valueschargeAmountEach15Min(number): Amount charged per 15-minute intervalreturnValuesOnly(boolean, optional): Return only values vs full objects (default: true)
Returns: Array of optimal charge intervals with lowest strategic value
// Find cheapest charge intervals: 30 kWh current, 80 kWh max, 5 kWh per 15 min
filterEffectiveCharge15Min(30, 80, priceDataset, 5);
// Returns: [prices from cheapest 15-min intervals to reach 80 kWh]
isEffectiveCharge15Min
isEffectiveCharge15Min(startSOC, maxSOC, dataset[], chargeAmountEach15Min, timestamp)
Checks if a specific timestamp falls within an optimal charging 15-minute interval. This function evaluates whether charging during the given timestamp aligns with the lowest-value intervals identified by the optimization algorithm.
Parameters:
startSOC(number): Starting State of ChargemaxSOC(number): Maximum State of Chargedataset(array): Array of timestamped data with strategic valueschargeAmountEach15Min(number): Amount charged per 15-minute intervaltimestamp(string): ISO timestamp to check
Returns: Boolean - true if timestamp is in optimal charge interval
// Check if 14:23 is an optimal charging time
isEffectiveCharge15Min(30, 80, priceDataset, 5, '2025-01-15T14:23:00');
// Returns: true/false
Technical Implementation Details
15-Minute Rounding Algorithm:
All 15-minute functions use consistent timestamp rounding logic to align with quarter-hour boundaries:
Rounding Examples:
10:07:00→10:00:0010:15:00→10:15:0010:23:45→10:15:0010:47:12→10:45:00
Key Differences from Hourly Functions:
| Aspect | Hourly Functions | 15-Minute Functions |
|---|---|---|
| Time granularity | 1 hour | 15 minutes (¼ hour) |
| Calculation multiplier | ×1 | ×4 (4 intervals per hour) |
| Parameter naming | ...EachHour | ...Each15Min |
| Timestamp comparison | isSame(date, 'hour') | Custom 15-min rounding |
Operators
Unary Operators
| Operation | Symbol |
|---|---|
| Negate | ! |
Binary Operators
| Operation | Symbol |
|---|---|
| Add, Concat | + |
| Subtract | - |
| Multiply | * |
| Divide | / |
| Divide and floor | // |
| Modulus | % |
| Power of | ^ |
| Logical AND | && |
| Logical OR | || |
Comparison Operators
| Comparison | Symbol |
|---|---|
| Equal | == |
| Not equal | != |
| Greater than | > |
| Greater than or equal | >= |
| Less than | < |
| Less than or equal | <= |
| Element in array or string | in |
Example: Using FEXL in Schedule Dynamic Rule
Assume you have defined two datasets:
energyPrices: contains day-ahead energy prices for each hour.powerProduction: contains estimated power production values for each hour based on weather forecasts.
You want to create a dynamic schedule that checks if the energy price during the generation hour is positive and enough power is available (above 100W). The following FEXL expression achieves this:
has(energyPrices, hour) && has(powerProduction, hour) &&
find(energyPrices, hour) > 0 &&
find(powerProduction, hour) > 100
This expression validates each hour in the schedule, allowing an entry to be created when both conditions are satisfied: the energy price is not negative, and power production exceeds 100 units.