Thing Subscriptions
Subscriptions are services that receive and respond to Thing Events. A subscription has a source, usually a Things. A Thing can have a subscription to an event that responds with an action. For example, if an entity fires a Motor is overheating event, it can subscribe to that event by triggering a Turn motor off subscription. Things can inherit subscriptions from the Thing Templates and Thing Shapes that they use.
A subscription is similar to a standard service, but it is explicitly linked to an event(s). This allows you to decouple the event from the code that responds to it. Like with a service, you can implement custom business logic to react to the event(s). You can leverage the capabilities of the model by sending emails through a mail server Thing, writing to a database, or calling any services available in the platform. A subscription does not have an explicit return output as a service does. However, a subscription can call any other service in the model to which the thread security context has access. The thread security context of a subscription is set to the same thread security context of the event that was fired. You can use the same JavaScript editing environment that is used to implement services.
Subscriptions have a defined input, which is the data packet issued by the event and referred to as event data. If the entity subscribes to a defined event, the event data is passed to the subscription function. The event data is described by the event Data Shapes. Within the subscription implementation, the data that is passed from the event acts as the input to the script function. For example, if an entity is subscribed to a Thing property data change event, the subscription script function is called. As a result, the Thing property value, along with other relevant data from the event, is passed to the function as part of the event data.
Many entities, by a subscription, can subscribe to the same event. Each entity receives a call to the subscription with the passed event data. An entity can take any action from the subscription script to achieve the solution requirements.
Some advantages of using this technique versus using a service that is called from another service include:
An event can be subscribed by one or many subscriptions.
Events are invoked based on system activity, and no user interaction is necessary.
If more than one Thing is subscribed to an event, you can use a subscription instead of chaining multiple services.
A subscription can subscribe to more than one event and from different entities.
* 
Event trigger and Subscriptions execute asynchronously. For example, a property update API request receives an immediate response when the property update action completes. It does not wait for the subsequent subscriptions that respond to the Data Change Event to complete.
Multiple Subscriptions
Subscriptions have a user-defined name as the unique identifier. Entities can have multiple subscriptions to an event on a Thing. For example, if an entity fires a Motor is overheating event, it can subscribe to that event with both a Turn motor off subscription and a Create Work Order subscription to have a maintenance check of the engine. Any number of other subscriptions can also be created for that event.
If a Thing Template or a Thing Shape implements a subscription to an event, the Things that use that Thing Template or Thing Shape can also create subscriptions to the same event without requiring a workaround to take additional actions when those events are fired.
Distributed Subscriptions
Distributed Subscriptions enable the distribution of subscription executions across all ThingWorx nodes when an event triggers many instances of subscriptions. For example, many Things subscribing to the same timer or scheduler event. This enables horizontal scalability of timer/scheduler-based subscription execution across all ThingWorx nodes in High Availability environments for better resource utilization and performance. The Distributed checkbox under the Subscription info tab enables this behavior. If the Distributed checkbox is clear, subscriptions to timers and schedulers would be executed in the same node in which the timer or scheduler event is generated. For more information regarding related configuration, see the following:
For on-premises, see Configuring SSL/TLS for Akka.
For the Docker environment, see Configuring the Akka TLS Communication for ThingWorx.
Multi-Event Subscriptions
The multi-events subscription capability enables customers to subscribe to more than one event, even from different Things, for a single subscription. This is mainly required for complex subscriptions. For example, customers can create subscriptions that could be triggered from multiple different property changes and would run a simple if-then-else rule on those property values to take actions based on the results. For an example, see Use Case 2. Or create a subscription based on the timer’s event to set the time-window rule. For an example, see Use Case 3.
* 
The multi-events subscription capability will be visible only when creating a new subscription. The legacy subscription, created with version ThingWorx 9.4 and later, will remain the same and cannot subscribe to more than one event.
With the ability of multi-events subscriptions, we introduce the batch ingestion concept, which delivers more than one event to a subscription at a time, grouped based on the timestamp of updated properties. For more information, see Thing Properties. For more information about Remote Thing, see Remote Thing Services.
* 
If you are currently using either the getSubscriptions or the getInstanceSubscriptions Java extensions API(s), you must use the getMultiEventSubscriptions or getInstanceMultiEventSubscriptions options instead. getSubscriptions and getInstanceSubscriptions do not support ThingWorx 9.5 and later subscription formats. This is applicable only if you have an entity containing a combination of subscriptions created before and after ThingWorx 9.5. The new APIs support the legacy (before ThingWorx 9.5) and the new (ThingWorx 9.5 and later) subscription format.
The + sign next to the Events dropdown list under the subscription’s Input tab enables this behavior. The list of the subscription’s events can be managed. For example, add/delete events and edit event info. Each event that is added gets a user-defined alias as the unique identifier. This alias should be used to access eventData through JavaScript. See below for more information about accessing eventData.
* 
When updating more than one property for the same timestamp, the new UpdatePropertyValuesBatched and UpdateSubscribedPropertyValuesBatched services are preferable over the legacy UpdatePropertyValues and UpdateSubscribedPropertyValues services. The new services allow multi-event subscriptions to execute exactly once per timestamp, receiving the new values for all changed properties in the same execution. This reduces the overall number of subscription executions compared to the legacy services and results in lower CPU consumption.
Accessing eventData
Accessing eventData information through JavaScript is done with the event’s alias. A developer can no longer access eventData by event.eventData.newValue but must use the events[] level and alias unique name to access a specific eventData, such as events["AliasName"].eventData.newValue.value.
* 
This guideline is valid only for new subscriptions created from ThingWorx 9.5 and later. Old subscriptions created before 9.5 can still access eventData by event.eventData.newValue.
It is possible that only part of the associated events trigger the subscription. Therefore, accessing one of the eventData that did not trigger the subscription can cause an error, so you must first check if accessing eventData is defined. For example:
try {
if (events["Me_DataChange_p1"].eventData.newValue.value ==44 {

} catch (error) {
logger.error(" p1 event is not defined " + error);
}
Or, here is another example.
if( events["Me_DataChange_p1"] !== undefined ){
if(events["Me_DataChange_p1"].eventData.newValue.value)==44;{

}
}
Ordered and Stateful Subscriptions
An ordered subscription is for use cases that require ordering and atomicity. It will be visible only when creating a new subscription. Subscriptions created with ThingWorx 9.4 and earlier will remain the same and cannot be executed sequentially.
Without this capability, subscription executions run asynchronously, in parallel, and cannot be stateful (cannot carry values between executions), which, for some use cases, may cause concurrency problems and wrong results, mainly if subscription rules are based on previous values. However, an ordered subscription enables running subscription executions sequentially based on an event’s timestamp order.
The Execute Events Sequentially checkbox under the Subscription info tab enables this behavior. In addition, once selected, a developer gains complete control over the contents of a dedicated JSON Object for each subscription, enabling them to store previous values between subscription executions, accumulate data, and do simple stateful aggregations. For more information about using thisSub.JSONState, see below.
* 
Ordered subscriptions require more CPU resources than unordered ones, so marking all subscriptions as ordered could impact the overall CPU consumption and result in a lower subscription execution rate. For subscriptions that don't need to keep state and can execute in parallel without side effects, it is recommended to leave the Execute Events Sequentially checkbox clear.
Using thisSub.JSONState
This Object, thisSub.JSONState is accessible through JavaScript as follows:
For native value, use thisSub.JSONState.X=5.
For infoTable, use JSONObject(), thisSub.JSONstate.myInfoTable = Y.toJSONObject().
To access it, use thisSub.state.myInfoTable.rows[0].value.
The data stored in the JSONState object should be used carefully and limited to just the necessary fields. For example, storing complete InfoTable objects using the toJSONObject() method rather than individual fields is not advised, as it consumes more CPU and requires more state memory storage.
To delete a value from the state object, use the JavaScript delete keyword delete thisSub.JSONState.myInfoTable.
To clear the whole state object, assign it with an empty JSON {}:thisSub.JSONState = {}.
* 
To prevent the loss of states data due to excessive memory consumption, the developer must delete individual state fields or clear the whole thisSub.JSONState object once the stored data is no longer needed.
The thisSub.JSONState is stored in memory and, therefore, is cleared on ThingWorx shutdown. In addition, the state is cleared when:
The entity declaring the subscription is edited and saved.
The entity declaring the subscription is disabled.
The subscription is disabled, such as using the DisableSubscription service.
The new ThingWorx node starts/stops in an HA cluster, and some subscriptions start executing on a different node.
* 
ThingWorx enforces memory size limits for thisSub.JSONState of a single subscription and the overall memory storage of all subscription states. These limits can be configured in the SubscriptionSettings section of platform-settings.json Configuration Details.
To learn more about monitoring the subscription state memory, see Subscription Performance. See Best Practices at a Glance for Building ThingWorx Solutions to learn about subscriptions best practices.
For a summary on how to use multi-event subscriptions and ordered subscriptions, view Multi-Event Subscriptions in ThingWorx 9.5.
The following section, Use Cases, contains code examples demonstrating the power of multi-events and ordered and stateful subscription capabilities, including time-window and property updates collision handling use cases.
Use Cases
Use Case 1 
// Usecase 1: alert will be triggered if voltage is higher than 118V
// Check if the voltage value exceeds 118 and trigger the appropriate action.
if (events["Me_DataChange_useCase1_Voltage"].eventData.newValue.value > 118){
logger.warn("Use-Case 1 subscription has been triggered");
}
Use Case 2 
// Usecase 2: alert will be triggered if voltage is higher than 118V and current is lower than 2A


//********************** Default initialization ****************************************
if (thisSub.JSONState.Voltage === undefined)
thisSub.JSONState.Voltage = me.useCase2_Voltage; // default value from VTQ
if (thisSub.JSONState.Current === undefined)
thisSub.JSONState.Current = 0; // default value 0
//*************************************************************************************

//************************* storing the values in JSONState from eventData ***************
var aliasArray = Object.keys(events.dataShape.fields);
for (var i=0; i < aliasArray.length; i ++ ){
if (aliasArray[i] === "Me_DataChange_useCase2_Current")
// Assign the new current value to the state
thisSub.JSONState.Current = events["Me_DataChange_useCase2_Current"].eventData.newValue.value;
if (aliasArray[i] === "Me_DataChange_useCase2_Voltage") {
// Assign the new voltage value to the state
thisSub.JSONState.Voltage = events["Me_DataChange_useCase2_Voltage"].eventData.newValue.value;
}
}
//******************************************************************************************

// Check if both voltage and current meet the conditions - voltage is higher than 118V and current is lower than 2A
if (thisSub.JSONState.Voltage > 118 &&
thisSub.JSONState.Current < 2) {
logger.warn("Use-Case 2 alert !!!, Voltage is: "+thisSub.JSONState.Voltage+" and Current is: "+thisSub.JSONState.Current);
}
else
logger.warn("Use-Case 2 NO alert, Voltage is: "+thisSub.JSONState.Voltage+" and Current is: "+thisSub.JSONState.Current);

Use Case 3 
// Usecase 3: alert will be triggered if voltage is higher than 118V and it has last more than 3 minutes.
// Timer will tick every X seconds


/*
When the DataChange event causing the subscription to run,
The value of Voltage will be checked:
1. If it would be above 118, timer would start running in case its undefined.
2. If it would be below or equal to 118, timer would back to undefined.
In case time has gotten to 3 minutes (180000MS), subscription would be triggered.
*/

// Getting the name of the event that triggered the subscription
let aliasArray = Object.keys(events.dataShape.fields);

if (aliasArray[0] === "Me_DataChange_useCase3_Voltage") {
// Valtage was changed
// We want to manage thisSub.JSONState.StartTime only. We don't save the Valtage in state

var Voltage = events[aliasArray[0]].eventData.newValue.value;

if( thisSub.JSONState.StartTime === undefined ) {
if( Voltage > 118 ){
thisSub.JSONState.StartTime = Date.now();
logger.warn("Voltage = " + Voltage + " Start time was set");
}
} else{
if( Voltage <= 118 ){
delete thisSub.JSONState.StartTime; // this will set to undefine
logger.warn("Voltage = " + Voltage + " Start time was unset");
}
}
}
else{
// We are in timer event
if( thisSub.JSONState.StartTime !== undefined && Date.now() - thisSub.JSONState.StartTime > 40000 )
logger.warn("Use-Case 3 timer was triggered - Alert !!! thisSub.JSONState.StartTime = " + thisSub.JSONState.StartTime);
else
logger.warn("Use-Case 3 timer was triggered - no Alert. thisSub.JSONState.StartTime = " + thisSub.JSONState.StartTime);
}


Use Case 4 
// Usecase 4: Input 32 bit integer will be translated into 32 status properties if there is any change,
// and alert on status property potentially will be triggered.


logger.warn("Use-Case 4 subscription has been triggered");
Use Case 5 
// Usecase 5: Alert will be triggered only on Error code which has severity level 1 or 2,
// where Error severity is defined in a relational table

// Get reference to the data table
var dataTable = Things["useCase5_DataTable"];

// Get the new value of ErrorCode from the event
var ErrorCodeValue = events["Me_DataChange_useCase5_ErrorCode"].eventData.newValue.value;

// Set parameters for querying the data table
var params = {
maxItems: 100, // Maximum number of entries to retrieve
source: undefined, // Filter entries by a specific source if needed
values: undefined, // Filter entries by specific column values if needed
orderBy: undefined // Order the retrieved entries by a specific column if needed
};

// Query the data table entries
var queryResult = dataTable.QueryDataTableEntries(params);
var rows = queryResult.rows;

// Iterate through the rows and check for matching ErrorCode and severity level
for (var i = 0; i < rows.length; i++) {
var thisRow = rows[i];

// Check if the ErrorCode matches the given ErrorCodeValue
if (thisRow.ErrorCode == ErrorCodeValue) {
// Check if the Severity is either 1 or 2
if (thisRow.Severity == 1 || thisRow.Severity == 2) {
// Log a warning message indicating the matched ErrorCode and its Severity
logger.warn("Error code: " + thisRow.ErrorCode + " has a severity of: " + thisRow.Severity + ". Use-Case 5 subscription has been triggered.");
}
}
}
Use Case 6 
// Usecase 6: Alert will be trigged if sum of product count is less than 10 in past 10 minutes
// Timer will tick every 1 seconds (1000MS)



// Getting the name of the event that triggered the subscription
var alias = events.dataShape.fields;

// Check if the alias matches "Me_DataChange_useCase6_ProductCount"
if (alias == "Me_DataChange_useCase6_ProductCount") {
// Check if the productCountDict exists in the state
if (thisSub.JSONState.productCountDict != undefined) {
// Delete entries older than 10 minutes
deleteOlderThan10Minutes();
// Add a new entry with the current timestamp and the new value from the event
thisSub.JSONState.productCountDict[Date.now().toString()] = events[alias].eventData.newValue.value;
} else {
// Initialize the productCountDict as an empty object and add a new entry
thisSub.JSONState.productCountDict = {};
thisSub.JSONState.productCountDict[Date.now().toString()] = events[alias].eventData.newValue.value;
}
} else {
// Check if the startTime is undefined and set it to the current time
if (thisSub.JSONState.startTime == undefined) {
thisSub.JSONState.startTime = Date.now();
}

// Check if the productCountDict exists in the state
if (thisSub.JSONState.productCountDict != undefined) {
// Delete entries older than 10 minutes
deleteOlderThan10Minutes();
// Check if an alert should be triggered and log a warning message
if (shouldTriggerAlert()) {
logger.warn("Use-Case 6 subscription has been triggered");
}
}
}

// Function to delete entries older than 10 minutes from productCountDict
function deleteOlderThan10Minutes() {
var keys = Object.keys(thisSub.JSONState.productCountDict);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
// Check if the time difference is greater than 10 minutes (600,000 milliseconds)
if (Date.now() - parseInt(key) > 600000) {
delete thisSub.JSONState.productCountDict[key];
}
}
}

// Function to check if an alert should be triggered
function shouldTriggerAlert() {
// Check if the sum of product count is less than 10 and if the time difference is greater than 10 minutes
if (productCountDictSUM() < 10 && Date.now() - thisSub.JSONState.startTime > 600000) {
return true;
} else {
return false;
}
}

// Function to calculate the sum of values in productCountDict
function productCountDictSUM() {
var keys = Object.keys(thisSub.JSONState.productCountDict);
var sum = 0;
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = thisSub.JSONState.productCountDict[key];
sum += value;
}
return sum;
}
Use Case 7 
// Usecase 7: alert will be triggered if average current (5 minutes window) is higher than 2.5A,
// and it will be checked very 30 seconds.
// Timer will tick every 30 seconds (30000MS)

// Getting the name of the event that triggered the subscription
var alias = events.dataShape.fields;

// Checking if the data alias matches the expected value
if (alias == "Me_DataChange_useCase7_Current") {
// If the data dictionary exists, update it with the new current value
if (thisSub.JSONState.currentDict != undefined) {
deleteOlderThan5Minutes();
thisSub.JSONState.currentDict[Date.now().toString()] = events[alias].eventData.newValue.value;
} else {
// If the data dictionary doesn't exist, create it and set the start time
thisSub.JSONState.currentDict = {};
thisSub.JSONState.currentDict[Date.now().toString()] = events[alias].eventData.newValue.value;
}
} else {
if(thisSub.JSONState.startTime == undefined){
thisSub.JSONState.startTime = Date.now();
}
// If the data alias doesn't match, perform data management and potential alert triggering
if (thisSub.JSONState.currentDict != undefined) {
// Delete entries in the data dictionary older than 5 minutes
deleteOlderThan5Minutes();

// Check if conditions for triggering the alert are met
if (shouldTriggerAlert()) {
logger.warn("Use-Case 7 subscription has been triggered");
}
}
}

// Function to delete data entries older than 5 minutes
function deleteOlderThan5Minutes() {
var keys = Object.keys(thisSub.JSONState.currentDict);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (Date.now() - parseInt(key) > 300000) {
delete thisSub.JSONState.currentDict[key];
}
}
}

// Function to determine if the alert should be triggered
// Only in case 5 minutes has been passed, and AVG is higher then 2.5
function shouldTriggerAlert() {
if (currentDictAVG() > 2.5 && Date.now() - thisSub.JSONState.startTime > 300000) {
return true;
} else {
return false;
}
}

// Function to calculate the average of currentDict
function currentDictAVG() {
var keys = Object.keys(thisSub.JSONState.currentDict);
var sum = 0;
var count = 0;
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = thisSub.JSONState.currentDict[key];
sum += value;
count++;
}
var average = sum / count;
return average;
}

////at any data change i sum the current
////when 30 seconds has been passed i calculate AVG
//
//
//
////change property event:
//// check if there is any value older then 5, if so - delete. (can implement by get keys)
//// add the value to the dict with key of timestamp
////timer event:
//// check if there is any value older then 5, if so - delete. (can implement by get keys)
//// AVG calculation, and condition check. - not only per 30 seconds, but for all the data at 5 minutes
//
//// build function to: change if there is any value older then 5, if so - delete. (can implement by get keys)
//
// ** the first timer event will be the startTime point of the algorithm
Use Case 8 
// Usecase 8: the alert will be triggered when most recent value is greater than the previous 10 values

// Check if valuesArray state is undefined and initialize it
if (thisSub.JSONState.valuesArray === undefined) {
thisSub.JSONState.valuesArray = {};
}

// Check if valuesArray length is less than 10
let indexCounter = Object.keys(thisSub.JSONState.valuesArray).length;
if ( indexCounter < 3) {
// Add the new value to valuesArray
thisSub.JSONState.valuesArray["i" + indexCounter] = events["Me_DataChange_useCase8_prop"].eventData.newValue.value;
} else {
// The valuesArray is full

var max = 0;
// Find the maximum value in valuesArray
for (let i = 0; i < Object.keys(thisSub.JSONState.valuesArray).length; i++) {
if (thisSub.JSONState.valuesArray["i" + i] > max)
max = thisSub.JSONState.valuesArray["i" + i];
}

// Check if the new value is greater than the maximum value
if (events["Me_DataChange_useCase8_prop"].eventData.newValue.value > max)
logger.warn("Use-Case 8 Alert!!! max is " + events["Me_DataChange_useCase8_prop"].eventData.newValue.value);

// Shift the values in valuesArray by one position
for (var j = 0; j < parseInt((Object.keys(thisSub.JSONState.valuesArray)).length) - 1; j++) {
thisSub.JSONState.valuesArray["i" + j] = thisSub.JSONState.valuesArray["i" + (j + 1)];
}
// Add the new value to valuesArray using the current indexCounter
thisSub.JSONState.valuesArray["i" + (indexCounter - 1)] = events["Me_DataChange_useCase8_prop"].eventData.newValue.value;
}

logger.warn("Use-Case 8 End. valuesArray = " + thisSub.JSONState.valuesArray);

Was this helpful?