Подписки на вещь
Подписки являются сервисами, которые получают
события и реагируют на них. Подписка содержит источник, обычно это
Вещь. Вещь может иметь подписку на событие, на которое отвечает действием. Например, если сущность инициирует событие
Двигатель перегревается, она может подписаться на это событие, запуская подписку
Выключить двигатель. Вещи могут наследовать подписки от
шаблонов вещи и
профилей вещи, которые они используют.
Подписка подобна стандартному сервису, но она в явном виде связана с событиями. Это позволяет отсоединить событие от кода, который отвечает на него. Чтобы реагировать на события, можно, как и для сервиса, применить пользовательскую бизнес-логику. Можно использовать возможности модели, отправляя электронные письма через вещь почтового сервера, записывая в базу данных или вызывая любые сервисы, доступные в платформе. У подписки нет явных возвращаемых выходных данных, как у сервиса. Однако подписка может вызывать любой другой сервис в модели, к которой имеет доступ контекст безопасности потока. Контекст безопасности потока для подписки задается, как тот же контекст безопасности потока инициированного события. Можно использовать ту же среду редактирования JavaScript, которая используется для реализации сервиса.
Подписки имеют определенные входные данные, которые являются пакетом данных, выпущенным событием; их называют данными события. Если сущность подписана на определенное событие, данные этого события передаются в функцию подписки. Данные события описываются в
структуре данных события. В реализации подписки данные, которые передаются из события, действуют как входные данные для функции сценария. Например, если сущность подписана на событие изменения данных свойства вещи, вызывается функция сценария подписки. В результате значение свойства вещи вместе с другими соответствующими данными из события передается в функцию как часть данных события.
Многие сущности могут подписаться на одно и то же событие. Каждая сущность получает вызов подписки с переданными данными события. Сущность может выполнить любое действие из сценария подписки для реализации требований к решению.
Некоторые преимущества использования этой методики в сравнении с использованием сервиса, который вызывается из другого сервиса, включают следующие:
• Можно подписаться на событие по одной или нескольким подпискам.
• События вызываются на основе задач системы, и вмешательство пользователя не требуется.
• Если на событие подписывается несколько вещей, можно использовать подписку вместо объединения нескольких сервисов.
• В одной подписке можно подписаться на несколько событий из разных сущностей.
|
|
Триггер событий и подписки выполняются в асинхронном режиме. Например, запрос API на обновления свойства получает немедленный отклик по завершении действия по обновлению свойства. В нем не ожидается, чтобы завершились последующие подписки, которые реагируют на событие изменения данных.
|
Несколько подписок
Подписки имеют в качестве уникального идентификатора наименование, определенное пользователем. Сущности могут иметь несколько подписок на одно событие вещи. Например, если сущность инициирует событие Двигатель перегревается, можно создать для события подписку Выключить двигатель и подписку Создать заказ на работу, чтобы выполнить обслуживание двигателя. Для этого события можно также создать любое количество других подписок.
Если шаблон вещи или профиль вещи реализует подписку на событие, то вещи, которые используют этот шаблон вещи или профиль вещи, могут также создать подписки на то же событие и не требуются другие пути для выполнения дополнительных действий для этих событий.
Распределенные подписки
Распределенные подписки позволяют распределять выполнение подписки между всеми узлами ThingWorx, когда событие инициируется многими экземплярами подписок. Например, многие вещи подписаны на одно и то же событие таймера или планировщика. Это обеспечивает горизонтальную масштабируемость выполнения подписки на основе таймера или планировщика во всех узлах ThingWorx в средах высокой доступности для повышения производительности и эффективности использования ресурсов. Такое поведение активируется флажком Распределенные на вкладке Информация о подписке. Если флажок Распределенные не установлен, подписки на таймеры и планировщики выполняются в том же узле, в котором генерируется событие таймера или планировщика. Дополнительные сведения о связанной конфигурации см. в следующих разделах:
Подписки на несколько событий
Возможность подписки на несколько событий позволяет клиентам подписаться в одной подписке на несколько событий, даже из разных вещей. В основном это требуется для сложных подписок. Например, клиенты могут создавать подписки, которые будут инициироваться несколькими изменениями разных свойств, и выполнять для этих значений простое правило if-then-else, чтобы выполнять действия на основе результатов. Пример см. в разделе
Пример использования 2. Или создать подписку на основе события таймера, чтобы задать правило временного окна. Пример см. в разделе
Пример использования 3.
|
|
Возможность подписки на несколько событий будет видимой только при создании новой подписки. Унаследованная подписка, созданная в ThingWorx версии 9.4 и более ранней, останется неизменной и не позволит подписаться на несколько событий.
|
Благодаря возможности создавать подписки на несколько событий, мы вводим концепцию пакетного приема данных, которая предоставляет в подписку несколько событий за один раз, сгруппированных на основе метки времени обновленных свойств. Дополнительные сведения см. в разделе
Свойства вещей. Дополнительные сведения об удаленной вещи см. в разделе
Сервисы удаленных вещей.
|
|
Если в текущий момент используются интерфейсы API расширений Java getSubscriptions или getInstanceSubscriptions, необходимо использовать опции getMultiEventSubscriptions или getInstanceMultiEventSubscriptions. getSubscriptions и getInstanceSubscriptions не поддерживают форматы подписки ThingWorx 9.5 и более поздних версий. Это применимо только при наличии сущности, содержащей комбинацию подписок, созданных в версиях до и после ThingWorx 9.5. Новые интерфейсы API поддерживают наследованные (до ThingWorx 9.5) и новые (ThingWorx 9.5 и более поздних версий) форматы подписки.
|
Это поведение активирует знак + рядом с выпадающим списком События на вкладке Входные данные подписки. Можно управлять списком событий подписки. Например, добавлять и удалять события и править информацию о событии. Каждое добавленное событие получает определяемый пользователем псевдоним в качестве уникального идентификатора. Этот псевдоним должен использоваться для доступа к eventData через JavaScript. Дополнительные сведения о доступе к eventData см. ниже.
|
|
При обновлении нескольких свойств для одной и той же метки времени новые сервисы UpdatePropertyValuesBatched и UpdateSubscribedPropertyValuesBatched предпочтительнее унаследованных сервисов UpdatePropertyValues и UpdateSubscribedPropertyValues. Новые сервисы позволяют подпискам на несколько событий выполняться точно один раз за метку времени, получая новые значения для всех измененных свойств в одном и том же выполнении. Это уменьшает общее число выполнений подписки по сравнению с унаследованными сервисами и приводит к снижению нагрузки на ЦП.
|
Доступ к данным о событии
Доступ к информации eventData через JavaScript осуществляется с использованием псевдонима события. Разработчик больше не может получать доступ к eventData с помощью event.eventData.newValue, но должен использовать уровень events[] и уникальный псевдоним для доступа к конкретным данным события, таким как events["AliasName"].eventData.newValue.value.
|
|
Эта рекомендация действительна только для новых подписок, созданных в ThingWorx 9.5 и более поздних версиях. Старые подписки, созданные до версии 9.5, по-прежнему могут получать доступ к данным событий с помощью event.eventData.newValue.
|
Подписка может инициироваться только частью связанных событий. Таким образом, доступ к одному из элементов eventData, которые не инициируют подписку, может привести к ошибке, поэтому необходимо сначала проверить, определен ли доступ к eventData. Например:
try {
if (events["Me_DataChange_p1"].eventData.newValue.value ===44) {
…
} catch (error) {
logger.error("p1 event is not defined: " + error.message);
}
Или другой пример.
if( events["Me_DataChange_p1"] !== undefined ){
if(events["Me_DataChange_p1"].eventData.newValue.value)===44) {
…
}
}
Упорядоченные и отслеживающие состояние подписки
Упорядоченная подписка используется для случаев, когда требуется упорядочение и изолированность. Такая возможность будет видимой только при создании новой подписки. Подписки, созданные в ThingWorx 9.4 и более ранних версиях, остаются прежними и не могут выполняться последовательно.
Без этой возможности подписки выполняются асинхронно, параллельно и не могут выполняться с отслеживанием состояния (невозможно переносить значения между выполнениями), что в некоторых случаях может привести к проблемам конкурентной обработки и ошибочным результатам, обычно когда правила подписки основаны на предыдущих значениях. Однако упорядоченная подписка позволяет последовательно выполнять подписки в соответствии с порядком меток времени события.
Такое поведение активируется флажком Выполнять события последовательно на вкладке Информация о подписке. Кроме того, после установки флажка разработчик получает полный контроль над содержимым выделенного объекта JSON для каждой подписки, что позволяет сохранять предыдущие значения между выполнением подписки, накапливать данные и просто отслеживать состояния. Дополнительные сведения об использовании thisSub.JSONState см. ниже.
|
|
Упорядоченные подписки требуют больше ресурсов ЦП, чем неупорядоченные, поэтому маркировка всех подписок как упорядоченных может повлиять на общее потребление ЦП и снизить скорость выполнения подписок. Для подписок, которые не требуют сохранения состояния и могут выполняться параллельно без побочных эффектов, рекомендуется оставить флажок Выполнять события последовательно снятым.
|
Использование thisSub.JSONState
Этот объект thisSub.JSONState доступен с помощью JavaScript следующим образом:
• Для собственного значения используйте thisSub.JSONState.X=5.
• Для таблицы данных используйте JSONObject(), thisSub.JSONstate.myInfoTable = Y.toJSONObject().
Для доступа используйте thisSub.state.myInfoTable.rows[0].value.
Данные, хранящиеся в объекте JSONState, следует использовать осторожно и ограничиваться только необходимыми полями. Например, не рекомендуется сохранять полные объекты таблицы данных, используя метод toJSONObject(), а не отдельные поля, поскольку при этом требуется больше ресурсов ЦП и больший объем хранилища памяти состояний.
Чтобы удалить значение из объекта состояния, используйте ключевое слово delete JavaScript: delete thisSub.JSONState.myInfoTable.
Чтобы очистить объект состояния целиком, назначьте ему пустой JSON {}: thisSub.JSONState = {}.
|
|
Чтобы предотвратить потерю данных состояний из-за чрезмерного потребления памяти, разработчик должен удалить отдельные поля состояний или очистить весь объект thisSub.JSONState после того, как сохраненные данные становятся ненужными.
|
Значение thisSub.JSONState сохраняется в памяти и, следовательно, очищается при выключении ThingWorx. Кроме того, состояние очищается в следующих случаях:
• Сущность, объявляющая подписку, изменяется и сохраняется.
• Сущность, объявляющая подписку, выключается.
• Подписка выключается, например с помощью сервиса DisableSubscription.
• Новый узел ThingWorx запускается или останавливается в кластере высокой доступности, а некоторые подписки начинают выполняться на другом узле.
|
|
ThingWorx принудительно ограничивает размер памяти для thisSub.JSONState одной подписки и общий объем памяти для всех состояний подписки. Эти пределы можно сконфигурировать в SubscriptionSettings, см. раздел Сведения о конфигурации platform-settings.json.
|
Дополнительные сведения о мониторинге памяти состояний подписки см. в разделе
Производительность подписки. Чтобы узнать об оптимальных методах создания решений ThingWorx, см. раздел
Оптимальные методы построения решений ThingWorx.
Сводку об использовании подписок на несколько событий и заказанных подписок можно найти в разделе
Подписки на несколько событий в ThingWorx 9.5.
В следующем разделе "Примеры использования" приведены примеры кода, демонстрирующие возможность использования нескольких событий, а также возможности заказанной и сохраняющей состояния подписки, включая временное окно и обновления свойств.
Примеры использования
Пример использования 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");
}
Пример использования 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);
Пример использования 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);
}
Пример использования 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");
Пример использования 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.");
}
}
}
Пример использования 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;
}
Пример использования 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
Пример использования 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);