物件訂閱
訂閱是用於接收事件並對其進行回應的服務。訂閱包含來源,通常為物件。物件可以訂閱透過某一動作進行回應的事件。例如,如果實體觸發馬達過熱 事件,則可透過觸發關閉馬達 訂閱來訂閱該事件。物件可以繼承來自它們所使用之「物範本」「物形式」的訂閱。
訂閱類似於標準服務,但明確連結至某事件。如此可將事件從回應事件的程式碼分離。如同服務,您可以實行自訂企業邏輯,以對事件做出回應。可以透過電子郵件伺服器「物件」傳送電子郵件、寫入到資料庫,或呼叫平台中的任何可用服務來利用模型功能。訂閱沒有像服務一樣的明確傳回輸出。但是,訂閱可以呼叫執行緒安全性前後關聯有權存取之模型中的其他任何服務。訂閱的執行緒安全性前後關聯會設定為所觸發之事件的相同執行緒安全性前後關聯。可以與實行服務使用相同的 JavaScript 編輯環境。
訂閱擁有已定義的輸入,這是事件所發佈的資料封裝,稱為事件資料。如果實體訂閱了已定義事件,則事件資料會傳遞給訂閱函數。事件資料由事件資料形式進行描述。在訂閱實行中,從事件傳遞的資料將作為指令集函數的輸入。例如,如果實體訂閱了物件內容資料變更事件,則會呼叫訂閱指令集函數。因此,物件內容值以及來自事件的其他相關資料會作為事件資料的一部份傳遞給函數。
透過訂閱,許多實體都可以訂閱相同事件。每個實體都會使用傳遞的事件資料接收對訂閱的呼叫。實體可以採取訂閱指令集中的任何動作,以達到解決方案需求。
相較於使用從其他服務呼叫的服務,使用此技巧的一些優點包括:
事件可由一或多個訂閱進行訂閱。
事件會根據系統活動來呼叫,而且不需要使用者互動。
如果有多個物件訂閱事件,您可使用訂閱,而非鏈結多個服務。
訂閱可從不同實體訂閱多個事件。
* 
事件觸發器與訂閱會以非同步方式執行。例如,內容更新 API 請求會在內容更新動作完成時收到立即回應。它不會等待回應「資料變更事件」的後續訂閱完成。
多個訂閱
訂閱有使用者定義的名稱作為唯一識別元。實體可以對物件上的事件進行多次訂閱。例如,如果某個實體觸發馬達過熱 事件,則可使用關閉馬達 訂閱與建立工單 訂閱來訂閱該事件,以對引擎進行維修檢查。也可以針對該事件建立任何數目的其他訂閱。
如果「物範本」或「物形式」實行對事件的訂閱,則使用該物範本或物形式的物件也可以建立對於相同事件的訂閱,而不需要因應措施來在觸發這些事件時採取其他動作。
分散式訂閱
分散式訂閱允許在事件觸發多個訂閱實例時,將訂閱分散到所有 ThingWorx 節點上執行。例如,許多物件訂閱相同的計時器或排程器事件。這樣一來,在高可用性環境下的所有 ThingWorx 節點上執行基於計時器/排程器的訂閱時,可實現此類執行的水平可擴充性,進而優化資源利用率并提高效能。「訂閱資訊」標籤下的「分散式」核取方塊可啟用此行為。如果清除「已分配」核取方塊,則會在產生計時器或排程器事件的同一節點中執行計時器與排程器訂閱。如需相關組態的詳細資訊,請參閱下列內容:
針對內部部署,請參閱針對 AKKA 配置 SSL/TLS
針對 Docker 環境,請參閱針對 ThingWorx 配置 Akka TLS 通訊
多事件訂閱
多事件訂閱功能可讓客戶針對單一訂閱,訂閱多個事件 (即使是來自不同物件)。這主要在複雜訂閱時需要。例如,客戶可以建立可從多個不同內容變更觸發的訂閱,並對這些內容值執行簡單的 if-then-else 規則,以根據結果採取動作。如需範例,請參閱使用案例 2。或根據計時器事件建立訂閱,以設定時間範圍規則。如需範例,請參閱使用案例 3
* 
多事件訂閱功能僅在建立新訂閱時才可見。使用 ThingWorx 9.4 版及更新版本建立的舊有訂閱將保持不變,且無法訂閱多個事件。
透過多事件訂閱的功能,我們引入了批次擷取概念,其可將多個事件一次遞送給訂閱,並根據更新內容的時間戳記分組。如需詳細資訊,請參閱物件內容。如需有關「遠端物件」的詳細資訊,請參閱遠端物件服務
* 
如果當前正在使用 getSubscriptionsgetInstanceSubscriptions JAVA 延伸 API,則必須改用 getMultiEventSubscriptionsgetInstanceMultiEventSubscriptions 選項。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[] 層級與別名具唯一性名稱才能存取特定 eventData,例如 events["AliasName"].eventData.newValue.value
可能只有部份關聯事件會觸發訂閱。因此,存取其中一個未觸發訂閱的 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;{

}
}
已排序且有狀態的訂閱 BETA
已排序訂閱適用於需要排序與不可部份完成的使用案例。只有在建立新訂閱時,此項才可見。使用 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
欲從狀態物件中刪除值,請使用 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);
這是否有幫助?