Suscripciones de cosas
Las suscripciones son servicios que reciben y responden a
eventos. Una suscripción tiene un origen, normalmente una
cosa. Una cosa puede tener una suscripción a un evento que responde con una acción. Por ejemplo, si una entidad activa un evento
El motor se está sobrecalentando, se puede suscribir a ese evento activando una suscripción
Apagar motor. Las cosas pueden heredar suscripciones de las
plantillas de cosa y las
definiciones de cosa que utilizan.
Una suscripción es similar a un servicio estándar, pero está vinculada explícitamente a un evento. De este modo, se puede desvincular el evento del código que responde a él. Como ocurre con un servicio, se puede implementar lógica empresarial personalizada para reaccionar al evento. Las capacidades del modelo se pueden aprovechar enviando mensajes de correo electrónico a través de una cosa de servidor de correo, escribiendo en una base de datos o llamando a cualquier servicio disponible en la plataforma. Una suscripción no tiene una salida de devolución explícita como un servicio. Sin embargo, una suscripción puede llamar a cualquier otro servicio del modelo al que tenga acceso el contexto de seguridad del subproceso. El contexto de seguridad del subproceso de una suscripción se define en el mismo contexto de seguridad del subproceso del evento que se ha activado. Es posible utilizar el mismo entorno de edición de JavaScript que se utiliza para implementar servicios.
Las suscripciones tienen una entrada definida, que es el paquete de datos que el evento emite y al que se hace referencia como datos de evento. Si la entidad se suscribe a un evento definido, los datos del evento se transfieren a la función de suscripción. El evento
Definición de datos permite describir los datos del evento. Dentro de la implementación de la suscripción, los datos que se pasan del evento actúan como entrada para la función de script. Por ejemplo, si una entidad se suscribe a un evento de cambio de datos de la propiedad de cosa, se llama a la función de script de suscripción. Como resultado, el valor de la propiedad de cosa, junto con otros datos pertinentes del evento, se pasan a la función como parte de los datos del evento.
Muchas entidades, mediante una suscripción, se pueden suscribir al mismo evento. Cada entidad recibe una llamada a la suscripción con los datos del evento transferidos. Una entidad puede realizar cualquier acción del script de suscripción para conseguir los requisitos de la solución.
Algunas de las ventajas de utilizar esta técnica frente a la utilización de un servicio al que se llama desde otro servicio son las siguientes:
• Una o varias suscripciones pueden suscribirse a un evento.
• Los eventos se invocan en función de la actividad del sistema y no es necesaria ninguna interacción del usuario.
• Si hay más de una cosa suscrita a un evento, se puede utilizar una suscripción en lugar de encadenar varios servicios.
• Una suscripción puede suscribirse a más de un evento y desde distintas entidades.
|
Los activadores de eventos y las suscripciones se ejecutan de forma asíncrona. Por ejemplo, una solicitud de API de actualización de propiedades recibe una respuesta inmediata cuando se completa la acción de actualización de la propiedad. No se espera a que se completen las subscripciones posteriores que responden al evento de cambio de datos.
|
Varias suscripciones
Las suscripciones tienen un nombre definido por el usuario como identificador único. Las entidades pueden tener varias suscripciones a un evento en una cosa. Por ejemplo, si una entidad activa un evento El motor se está sobrecalentando, se puede suscribir a ese evento con una suscripción Apagar motor y una suscripción Crear orden de trabajo para que se realice una verificación de mantenimiento del motor. También se puede crear cualquier cantidad de otras suscripciones para ese evento.
Si una plantilla de cosa o una definición de cosa implementa una suscripción a un evento, las cosas que utilizan dicha plantilla de cosa o definición de cosa también pueden crear suscripciones al mismo evento, sin requerir una solución alternativa para realizar acciones adicionales cuando se activan esos eventos.
Suscripciones distribuidas
Las suscripciones distribuidas permiten la distribución de ejecuciones de suscripciones en todos los nodos ThingWorx cuando un evento activa muchas instancias de suscripciones. Por ejemplo, muchas cosas que se suscriben al mismo evento de temporizador o programador. De este modo, se permite la escalabilidad horizontal de la ejecución de suscripciones basada en temporizador/programador en todos los nodos ThingWorx en entornos de alta disponibilidad para una mejor utilización de los recursos y mejor rendimiento. La casilla Distribuido de la ficha Información de suscripción permite activar este comportamiento. Si se despeja la casilla Distribuido, las suscripciones a temporizadores y programadores se ejecutarán en el mismo nodo en el que se genera el evento de temporizador o programador. Para obtener más información sobre la configuración relacionada, consulte lo siguiente:
Suscripciones multievento
La función de suscripción multievento permite a los clientes suscribirse a más de un evento, incluso desde distintas cosas, para una sola suscripción. Esto se requiere principalmente para suscripciones complejas. Por ejemplo, los clientes pueden crear suscripciones que se podrían activar a partir de múltiples cambios de propiedades diferentes y ejecutaría una regla if-then-else simple en esos valores de propiedad para realizar acciones basadas en los resultados. Para obtener un ejemplo, consulte el
caso de uso 2. O bien, cree una suscripción basada en el evento del temporizador para definir la regla de ventana de tiempo. Para obtener un ejemplo, consulte el
caso de uso 3.
|
La función de suscripción multievento estará visible solo al crear una nueva suscripción. La suscripción heredada, creada con ThingWorx 9.4 y versiones posteriores, permanecerá igual y no se puede suscribir a más de un evento.
|
Con la capacidad de las suscripciones multievento, se presenta el concepto de ingesta por lotes, que entrega más de un evento a una suscripción a la vez, agrupado en función de la fecha y hora de las propiedades actualizadas. Para obtener más información, consulte
Propiedades de cosa. Para obtener más información sobre la cosa remota, consulte
Servicios de cosa remota.
|
Si actualmente se utiliza la API getSubscriptions o las extensiones getInstanceSubscriptions de Java, se deben utilizar las opciones getMultiEventSubscriptions o getInstanceMultiEventSubscriptions en su lugar. Los objetos getSubscriptions y getInstanceSubscriptions no son compatibles con los formatos de suscripción de ThingWorx 9.5. Esto solo es aplicable si existe una entidad que contiene una combinación de suscripciones creadas antes y después de ThingWorx 9.5. Las nuevas API soportan el formato de suscripción heredado (antes de ThingWorx 9.5) y el nuevo (ThingWorx 9.5 y versiones posteriores).
|
El signo + junto a la lista desplegable Eventos de la ficha Entrada de la suscripción activa este comportamiento. Se puede gestionar la lista de eventos de la suscripción. Por ejemplo, añada/borre eventos y edite la información del evento. Cada evento que se añade obtiene un alias definido por el usuario como identificador único. Este alias se debe utilizar para acceder a eventData a través de JavaScript. Consulte a continuación para obtener más información sobre el acceso a eventData.
|
Al actualizar más de una propiedad para la misma fecha y hora, los nuevos servicios UpdatePropertyValuesBatched y UpdateSubscribedPropertyValuesBatched son preferibles sobre los servicios UpdatePropertyValues y UpdateSubscribedPropertyValues. Los nuevos servicios permiten que las suscripciones a varios eventos se ejecuten exactamente una vez por fecha y hora, recibiendo los nuevos valores de todas las propiedades modificadas en la misma ejecución. De este modo, se reduce el número total de ejecuciones de suscripción en comparación con los servicios heredados, lo que da lugar a un menor consumo de la CPU.
|
Acceso a eventData
El acceso a la información de eventData a través de JavaScript se hace con el alias del evento. Un desarrollador ya no puede acceder a eventData por event.eventData.newValue, sino que debe utilizar el nivel events[] y el nombre único de alias para acceder a un objeto eventData específico, como events["AliasName"].eventData.newValue.value.
|
Esta directriz solo es válida para las nuevas suscripciones creadas a partir de ThingWorx 9.5 y versiones posteriores. Las suscripciones anteriores a la 9.5 pueden seguir teniendo acceso a datos del evento mediante event.eventData.newValue.
|
Es posible que solo una parte de los eventos asociados active la suscripción. Por lo tanto, acceder a uno de los objetos eventData que no ha activado la suscripción puede causar un error, por lo que primero se debe comprobar si se ha definido el acceso a eventData. Por ejemplo:
try {
if (events["Me_DataChange_p1"].eventData.newValue.value ==44 {
…
} catch (error) {
logger.error(" p1 event is not defined " + error);
}
O bien, aquí se proporciona otro ejemplo.
if( events["Me_DataChange_p1"] !== undefined ){
if(events["Me_DataChange_p1"].eventData.newValue.value)==44;{
…
}
}
Suscripciones en orden y con estado
Una suscripción en orden es para casos de uso que requieren orden y atomicidad. Solo estará visible al crear una nueva suscripción. Las suscripciones creadas con ThingWorx 9.4 y versiones anteriores seguirán igual y no se podrán ejecutar secuencialmente.
Sin esta función, las ejecuciones de suscripción se realizan de forma asíncrona, en paralelo y no pueden tener estados (no se pueden transportar valores entre ejecuciones), lo que, en algunos casos de uso, puede provocar problemas de simultaneidad y resultados erróneos, principalmente si las reglas de suscripción se basan en valores anteriores. Sin embargo, una suscripción en orden permite realizar las ejecuciones de suscripción secuencialmente en función del orden de fecha y hora de un evento.
La casilla Ejecutar eventos secuencialmente de la ficha Información de suscripción permite activar este comportamiento. Además, una vez seleccionado, un desarrollador obtiene el control completo sobre el contenido de un objeto JSON dedicado para cada suscripción, lo que le permite almacenar los valores anteriores entre las ejecuciones de la suscripción, acumular datos y realizar agregaciones con estados simples. Para obtener más información sobre el uso de thisSub.JSONState, consulte la sección siguiente.
|
Las suscripciones ordenadas requieren más recursos de CPU que recursos no ordenados, por lo que marcar todas las suscripciones como ordenadas podría afectar al consumo general de la CPU y dar lugar a una tasa de ejecución de la suscripción más baja. Para las suscripciones que no necesitan mantener el estado y pueden ejecutarse en paralelo sin efectos secundarios, se recomienda dejar despejada la casilla Ejecutar eventos secuencialmente.
|
Uso de thisSub.JSONState
Este objeto thisSub.JSONState es accesible a través de JavaScript de la siguiente manera:
• Para obtener un valor nativo, utilice thisSub.JSONState.X=5.
• Para una infotable, utilice JSONObject(), thisSub.JSONstate.myInfoTable = Y.toJSONObject().
Para acceder a él, utilice thisSub.state.myInfoTable.rows[0].value.
Los datos almacenados en el objeto JSONState se deben utilizar con cuidado y limitarse solo a los campos necesarios. Por ejemplo, no es recomendable almacenar objetos completos de InfoTable utilizando el método toJSONObject() en lugar de campos individuales, ya que consume más recursos de la CPU y requiere más almacenamiento en la memoria de estado.
Para borrar un valor del objeto de estado, utilice la palabra clave delete de delete thisSub.JSONState.myInfoTable de JavaScript
Para despejar todo el objeto de estado, asígnelo con un elemento JSON {}:thisSub.JSONState = {} vacío.
|
Para evitar la pérdida de datos de estados debido al consumo excesivo de memoria, el desarrollador debe borrar los campos de estado individuales o despejar todo el objeto thisSub.JSONState una vez que los datos almacenados ya no son necesarios.
|
El objeto thisSub.JSONState se almacena en memoria y, por lo tanto, se borra al apagar ThingWorx. Además, el estado se borra en los siguientes casos:
• Se edita y guarda la entidad que declara la suscripción.
• La entidad que declara la suscripción está desactivada.
• La suscripción está desactivada, por ejemplo, el uso del servicio DisableSubscription.
• Se inicia o detiene el nuevo nodo de ThingWorx en un clúster de alta disponibilidad y algunas suscripciones comienzan a ejecutarse en otro nodo.
|
ThingWorx aplica los límites de tamaño de memoria para thisSub.JSONState de una suscripción única y el almacenamiento de memoria general de todos los estados de suscripción. Estos límites se pueden configurar en la sección SubscriptionSettings de Detalles de configuración de platform-settings.json.
|
Para obtener más información sobre la supervisión de la memoria del estado de la suscripción, consulte
Rendimiento de la suscripción. Consulte
Vistazo a las prácticas recomendadas para crear soluciones de ThingWorx para obtener información sobre las prácticas recomendadas para suscripciones.
Para obtener un resumen de cómo utilizar las suscripciones multievento y las suscripciones ordenadas, consulte
Multi-Event Subscriptions in ThingWorx 9.5.
En la siguiente sección, Casos de uso, se incluyen ejemplos de código que muestran la potencia de las suscripciones multievento y las capacidades de suscripción ordenadas y con estado, como casos de uso sobre la gestión de colisiones en las actualizaciones de ventana de tiempo y propiedad.
Casos prácticos
Caso práctico 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");
}
Caso práctico 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);
Caso práctico 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);
}
Caso práctico 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");
Caso práctico 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.");
}
}
}
Caso práctico 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;
}
Caso práctico 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
Caso práctico 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);