사물 구독
구독은 이벤트를 수신하고 이에 응답하는 서비스입니다. 구독은 일반적으로 사물인 소스를 포함합니다. 사물에는 작업에 응답하는 이벤트에 대한 구독이 있을 수 있습니다. 예를 들어, 엔티티에서 모터가 과열됨 이벤트가 개시되는 경우 모터 끄기 구독을 트리거하여 해당 이벤트를 구독할 수 있습니다. 사물은 사용하는 사물 템플릿사물 형태에서 구독을 상속할 수 있습니다.
구독은 표준 서비스와 비슷하지만 이벤트에 명시적으로 연결됩니다. 이를 통해 이벤트를 이벤트에 응답하는 코드에서 분리시킬 수 있습니다. 서비스와 마찬가지로 사용자 정의 비즈니스 로직을 구현하여 이벤트에 반응할 수 있습니다. 메일 서버 사물을 통해 이메일을 전송하거나, 데이터베이스에 작성하거나, 플랫폼에서 사용 가능한 서비스를 호출하여 모델의 기능을 활용할 수 있습니다. 구독에는 서비스와 같은 명시적인 반환 출력이 없습니다. 그러나 구독은 스레드 보안 컨텍스트에서 액세스할 수 있는 모델의 다른 서비스를 호출할 수 있습니다. 구독의 스레드 보안 컨텍스트는 발생한 이벤트의 동일한 스레드 보안 컨텍스트로 설정됩니다. 서비스를 구현하는 데 사용되는 동일한 JavaScript 편집 환경을 사용할 수 있습니다.
구독에는 이벤트에 의해 발급되고 이벤트 데이터라고 하는 데이터 패킷인 정의된 입력이 있습니다. 엔티티가 정의된 이벤트를 구독하는 경우 이벤트 데이터가 구독 함수에 전달됩니다. 이벤트 데이터는 이벤트 데이터 셰이프로 설명됩니다. 구독 구현 내에서 이벤트에서 전달된 데이터는 스크립트 함수에 대한 입력으로 작동합니다. 예를 들어, 엔티티가 사물 속성 데이터 변경 이벤트를 구독하는 경우 구독 스크립트 함수가 호출됩니다. 결과적으로 이벤트의 다른 관련 데이터와 함께 사물 속성 값이 이벤트 데이터의 일부로 함수에 전달됩니다.
구독을 통해 많은 엔티티가 동일한 이벤트를 구독할 수 있습니다. 각 엔티티는 전달된 이벤트 데이터로 구독에 대한 호출을 받습니다. 엔티티는 구독 스크립트에서 솔루션 요구사항을 충족하는 데 필요한 작업을 수행할 수 있습니다.
이 방법을 사용하면 다른 서비스에서 호출되는 서비스를 사용하는 것에 비해 다음과 같은 이점이 있습니다.
하나 또는 여러 구독에서 이벤트를 구독할 수 있습니다.
이벤트가 시스템 활동을 기반으로 호출되며 사용자 상호 작용이 필요하지 않습니다.
둘 이상의 사물이 이벤트를 구독하는 경우 여러 서비스를 연결하는 대신 구독을 사용할 수 있습니다.
구독은 둘 이상의 이벤트를 여러 엔티티에서 구독할 수 있습니다.
* 
이벤트 트리거 및 구독은 비동기적으로 실행됩니다. 예를 들어, 속성 업데이트 작업이 완료되면 속성 업데이트 API 요청이 즉시 응답을 받습니다. 데이터 변경 이벤트에 응답하는 후속 구독이 완료될 때까지 기다리지 않습니다.
여러 구독
사용자 정의 이름의 구독을 고유 식별자로 사용합니다. 엔티티에는 사물의 이벤트에 대한 여러 개의 구독이 있을 수 있습니다. 예를 들어, 엔티티에서 모터가 과열됨 이벤트가 개시되는 경우 모터 끄기 구독과 작업 요청서 만들기 구독 모두를 이벤트와 함께 사용하여 엔진에 대해 유지 관리를 점검할 수 있습니다. 해당 이벤트에 대해 임의 개수의 다른 구독을 만들 수도 있습니다.
사물 템플릿 또는 사물 형태가 이벤트에 대한 구독을 구현하는 경우 해당 사물 템플릿 또는 사물 형태를 사용하는 사물은 동일한 이벤트에 대한 구독을 만들 수도 있으며, 이러한 이벤트가 개시될 때 추가 작업을 수행하기 위한 해결책이 필요하지 않습니다.
배포 구독
배포 구독을 사용하면 이벤트가 여러 구독 인스턴스를 트리거할 때 모든 ThingWorx 노드에서 구독 실행을 배포할 수 있습니다. 예를 들어, 동일한 타이머 또는 스케줄러 이벤트를 구독하는 사물이 많이 있습니다. 이렇게 하면 고가용성 환경의 모든 ThingWorx 노드에서 타이머/스케줄러 기반 구독 실행을 수평적으로 확장하여 리소스 사용률과 성능을 향상시킬 수 있습니다. 구독 정보 탭 아래의 배포됨 확인란을 사용하면 이 동작이 활성화됩니다. 배포됨 확인란의 선택을 취소하면 타이머 또는 스케줄러 이벤트가 생성되는 동일한 노드에서 타이머 및 스케줄러에 대한 구독이 실행됩니다. 관련 구성에 대한 자세한 내용은 다음을 참조하십시오.
온프레미스의 경우 AKKA에 대한 SSL/TLS 구성을 참조하십시오.
Docker 환경의 경우 ThingWorx용 AKKA TLS 통신 구성을 참조하십시오.
여러 이벤트 구독
여러 이벤트 구독 기능을 사용하면 고객은 단일 구독을 위한 서로 다른 사물에서도 둘 이상의 이벤트를 구독할 수 있습니다. 이는 주로 복잡한 구독에 필요합니다. 예를 들어, 고객은 여러 다른 속성 변경으로 인해 트리거될 수 있는 구독을 생성하고 이러한 속성 값에 대해 간단한 if-then-else 규칙을 실행하여 결과에 따라 작업을 수행할 수 있습니다. 예를 보려면 사용 사례 2를 참조하십시오. 또는 타이머의 이벤트를 기반으로 구독을 생성하여 기간 규칙을 설정합니다. 예를 보려면 사용 사례 3를 참조하십시오.
* 
여러 이벤트 구독 기능은 새 구독을 생성할 때만 표시됩니다. ThingWorx 9.4 이상 버전으로 생성된 레거시 구독은 동일하게 유지되며 둘 이상의 이벤트를 구독할 수 없습니다.
여러 이벤트 구독 기능을 통해 업데이트된 속성의 타임스탬프를 기준으로 그룹화하여 한 번에 둘 이상의 이벤트를 구독에 전달하는 배치 수집 개념을 도입했습니다. 자세한 내용은 사물 속성을 참조하십시오. 원격 사물에 대한 자세한 내용은 원격 사물 서비스를 참조하십시오.
* 
현재 getSubscriptions 또는 getInstanceSubscriptions Java 확장 API를 사용하고 있는 경우 getMultiEventSubscriptions 또는 getInstanceMultiEventSubscriptions 옵션을 대신 사용해야 합니다. getSubscriptionsgetInstanceSubscriptions는 ThingWorx 9.5 이상 구독 형식을 지원하지 않습니다. 이는 ThingWorx 9.5 이전과 이후에 생성된 구독 조합을 포함하는 엔티티가 있는 경우에만 적용 가능합니다. 새 API는 레거시(ThingWorx 9.5 이전)와 신규(ThingWorx 9.5 이상) 구독 형식을 지원합니다.
구독의 입력 탭 아래 이벤트 드롭다운 목록 옆에 있는 + 기호를 사용하면 이 동작이 활성화됩니다. 구독 이벤트 목록을 관리할 수 있습니다. 예를 들어, 이벤트를 추가/삭제하고 이벤트 정보를 편집합니다. 추가된 각 이벤트는 사용자 정의 별칭을 고유 식별자로 가져옵니다. 이 별칭은 JavaScript를 통해 eventData에 액세스하는 데 사용해야 합니다. eventData에 액세스에 대한 자세한 내용은 아래를 참조하십시오.
eventData 액세스
JavaScript를 통해 eventData 정보에 액세스하는 작업은 이벤트 별칭을 사용하여 수행됩니다. 개발자는 더 이상 event.eventData.newValue를 통해 eventData에 액세스할 수 없지만 events["AliasName"].eventData.newValue.value와 같은 특정 eventData에 액세스하려면 events[] 수준 및 별칭 고유 이름을 사용해야 합니다.
연관된 이벤트의 일부만 구독을 트리거할 수 있습니다. 따라서 구독을 트리거하지 않은 eventData 중 하나에 액세스하면 오류가 발생할 수 있으므로 먼저 eventData에 액세스가 정의되어 있는지 확인해야 합니다. 예를 들면, 다음과 같습니다.
try {
if (events["Me_DataChange_p1"].eventData.newValue.value ==44 {

} catch (error) {
logger.error(" p1 event is not defined " + error);
}
또는 여기에 또 다른 예가 있습니다.
if( events["Me_DataChange_p1"] !== undefined ){
if(events["Me_DataChange_p1"].eventData.newValue.value)==44;{

}
}
순서 지정 및 상태 저장 구독 베타
순서 지정 구독은 순서와 원자성이 필요한 사용 사례를 위한 것입니다. 이는 새 구독을 생성할 때만 표시됩니다. ThingWorx 9.4 이전 버전으로 생성된 구독은 동일하게 유지되며 순차적으로 실행될 수 없습니다.
이 기능이 없으면 구독 실행은 비동기식으로 병렬로 실행되며 상태 저장이 불가능합니다(실행 간에 값을 전달할 수 없음). 이로 인해 일부 사용 사례에서는 구독 규칙이 주로 이전 값을 기반으로 하는 경우 동시성 문제와 잘못된 결과가 발생할 수 있습니다. 그러나 순서 지정 구독을 사용하면 이벤트의 타임스탬프 순서에 따라 구독 실행을 순차적으로 실행할 수 있습니다.
구독 정보 탭 아래의 Execute sequentially 확인란을 사용하면 이 동작이 활성화됩니다. 또한 개발자가 선택하면 각 구독에 대한 전용 JSON 객체의 콘텐츠를 완벽하게 제어할 수 있으므로 구독 실행 간에 이전 값을 저장하고 데이터를 누적하며 간단한 상태 저장 집계를 수행할 수 있습니다. thisSub.JSONState 사용에 대한 자세한 내용은 아래를 참조하십시오.
thisSub.JSONState 사용
thisSub.JSONState 객체는 다음과 같이 JavaScript를 통해 액세스할 수 있습니다.
기본 값의 경우 thisSub.JSONState.X=5를 사용합니다.
인포테이블의 경우 JSONObject(), thisSub.JSONstate.myInfoTable = Y.toJSONObject()를 사용합니다.
액세스하려면 thisSub.state.myInfoTable.rows[0].value를 사용합니다.
상태 객체에서 값을 삭제하려면 JavaScript delete 키워드 delete thisSub.JSONState.myInfoTable을 사용합니다.
전체 상태 객체를 지우려면 빈 JSON {}:thisSub.JSONState = {}를 지정합니다.
* 
thisSub.JSONState의 유연성은 개발자가 너무 많은 데이터를 저장할 수 있다는 위험을 가중시키며 ThingWorx의 실패를 유발합니다. 따라서 개발자는 객체를 사용할 때마다 객체 정리 프로세스를 포함해야 합니다.
thisSub.JSONState는 메모리에 저장되므로 ThingWorx 종료 시 지워집니다. 또한 다음과 같은 경우 상태가 지워집니다.
구독을 선언하는 엔티티가 편집 및 저장되는 경우
구독을 선언하는 엔티티가 비활성화되는 경우
DisableSubscription 서비스 사용 등으로 구독이 비활성화되는 경우
새 ThingWorx 노드는 HA 클러스터에서 시작/중지되며 일부 구독이 다른 노드에서 실행되기 시작하는 경우
사용 사례
아래의 사용 사례는 여러 이벤트 기능과 순서 지정 및 상태 저장 구독 기능을 보여줍니다.
사용 사례 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);
도움이 되셨나요?