Community Tip - Learn all about the Community Ranking System, a fun gamification element of the PTC Community. X

IoT Tips

Sort by:
This post is part of the series Forced Root Cause Monitoring via Mashups and Modal Popups To not feel lost or out of context, it's recommended to read the main post first. Before we start Create a new Project called "RootCausePopups" and save it. In the New Composer set the Project Context (top left box) to the "RootCausePopups" project. This will automatically add all of our new Entities into our project. Otherwise we would have to add each Entity manually on creation.
View full tip
This groovy script will return a list of DeviceGroups based off a given Asset's serialnumber. import com.axeda.drm.sdk.device.*; import com.axeda.drm.sdk.Context; Context sysContext = Context.create(); DeviceGroupFinder dgf = new DeviceGroupFinder(sysContext); DeviceFinder devFinder = new DeviceFinder(sysContext); devFinder.setSerialNumber("[your serialnumber here]"); Device myDevice= devFinder.find(); dgf.setDeviceId(myDevice.getId()); List<DeviceGroup> allGroups = dgf.findAll(); allGroups.each { group -> logger.debug(group.getName()); }
View full tip
The following script is a component of the Axeda Connected Configuration (CMDB) feature.  It is used to provide configuration data for controlling package deployments via Connected Content (SCM). ​ ConfigItem_CRU.groovy *Takes a POST request, not parameters import static com.axeda.sdk.v2.dsl.Bridges.* import com.axeda.drm.sdk.scripto.Request import com.axeda.services.v2.ConfigurationItem import com.axeda.services.v2.ConfigurationItemCriteria import com.axeda.services.v2.AssetConfiguration import com.axeda.services.v2.Asset import com.axeda.services.v2.ExecutionResult import groovy.json.JsonSlurper import net.sf.json.JSONObject import groovy.xml.MarkupBuilder /** * ConfigItem_CRU.groovy * ----------------------- * * Reads in json from an http post request and reads, adds, deletes or updates Configuration Items. * * * @note this parses a post and does not take any additional parameters. * * @author sara streeter <sstreeter@axeda.com> */ def contentType = "application/json" final def serviceName = "ConfigItem_CRU" def response = [:] def writer = new StringWriter() def xml = new MarkupBuilder(writer) try {     // BUSINESS LOGIC BEGIN     def assetId     def validationOnly     def validationResponse = ""     List<ConfigurationItem> configItemList     if (Request?.body != null && Request?.body !="") {         def slurper = new JsonSlurper()         def request = slurper.parseText(Request?.body)         assetId = request.result.assetId         validationOnly = request.result.validationOnly?.toBoolean()         if (request.result.items != null && request.result.items.size() > 0){             configItemList = request.result.items.inject([]) { target, item ->               if (item && item.path != "" && item.key != "" && item.path != null && item.key != null){                     ConfigurationItem configItem = new ConfigurationItem()                     configItem.path = item.path + item.key                     configItem.value = item.value                     target << configItem                 }                 target             }         }     }       if (assetId != null) {               def asset = assetBridge.find([assetId])[0]             AssetConfiguration config = assetConfigurationBridge.getAssetConfiguration(assetId, "")               def itemToDelete                        if (config == null) {                     createConfigXML(xml)                     AssetConfiguration configToCreate = assetConfigurationBridge.fromXml(writer.toString(), asset.id)                     ExecutionResult result = assetConfigurationBridge.create(configToCreate)                     AssetConfiguration config2 = assetConfigurationBridge.getAssetConfiguration(asset.id, "")                     config = config2                     itemToDelete = "/Item"                 }                 if (configItemList != null && configItemList?.size() > 0){                 List<ConfigurationItem> compareList = config.items                 def intersectingCompareItems = compareList.inject(["save": [], "delete": []]) { map, item ->                     // find whether to delete                     def foundItem = configItemList.findAll{ compare -> item?.path == compare?.path && item?.value == compare?.value  }                     map[foundItem.size() > 0 ? "save" : "delete"] << item                     map                 }               intersectingCompareItems.delete = intersectingCompareItems.delete.collect{it.path}               if (itemToDelete){                 intersectingCompareItems.delete.add(itemToDelete)               }                 def intersectingConfigItems = configItemList.inject(["old": [], "new": []]) { map, item ->                     // find whether it's old                     def foundItem = compareList.findAll{ compare -> item?.path == compare?.path && item?.value == compare?.value }                     map[foundItem.size() > 0 ? "old" : "new"] << item                     map                 }                 assetConfigurationBridge.deleteConfigurationItems(config, intersectingCompareItems.delete)                 assetConfigurationBridge.appendConfigurationItems(config, intersectingConfigItems.new)               def exResult = assetConfigurationBridge.validate(config)               if (exResult.successful){                     validationResponse = "success"                     if (!validationOnly){                         assetConfigurationBridge.update(config)                     }               }                 else {                     validationResponse = exResult.failures[0]?.details                 }             }             response = [                 assetId: assetId,                 items: config?.items?.collect { item ->                 def origpath = item.path                 def lastSlash = origpath.lastIndexOf("/")                 def key = origpath.substring(lastSlash + 1, origpath.length())                        def path = origpath.replace("/" + key, "")                 path += "/"                     [                         path: path,                         key: key,                         value: item.value                     ]                 },                 validationResponse: validationResponse             ]       }         else {             throw new Exception("Error: Asset Id must be provided.")         } } catch (Exception ex) {       logger.error ex   response = [           error:  [                   type: "Backend Application Error"                   , msg: ex.getLocalizedMessage()           ]   ] } return ['Content-Type': 'application/json', 'Content': JSONObject.fromObject(response).toString(2)] /** * Create the Success response. * * @param xml : The xml response.<br> * @param info : If this is set to "1" the info element will be included in the response.<br> * @param infos : Collection of information to include within the info element of the response.<br> */ private void createConfigXML(xml) {     xml.Item() }  
View full tip
I've had a lot of questions over the years working with Azure IoT, Kepware, and ThingWorx that I really struggled getting answers to. I was always grateful when someone took the time to help me understand, and now it is time to repay the favour.   People ask me many things about Azure (in a ThingWorx context), and one of the common ones has been about MQTT communications from Kepware to ThingWorx using IoT Hub. Recently the topic has come up again as more and more of the ThingWorx expert community start to work with Azure IoT. Today, I took the time to build, test, validate, and share an approach and utilities to do this in cases where the Azure Industrial IoT OPC UA integration is overkill or simply a step later in the project plan. Enjoy!   End to end Integration of Kepware to ThingWorx using MQTT over Azure IoT (YoutTube 45 minute deep-dive)   ThingWorx entities for import (ThingWorx 9.0)   This approach can be quite good for a simple demo if you have a Kepware Integrator or Kepware Enterprise license, but the use of IoT Gateway for many servers and tags can be quite costly.   Those looking to leverage Azure IoT Hub for MQTT integration to ThingWorx would likely also find this recorded session and shared utilities quite helpful.   Cheers, Greg
View full tip
This post is part of the series Forced Root Cause Monitoring via Mashups and Modal Popups To not feel lost or out of context, it's recommended to read the main post first. Required Entities In this simplified example we'll just use a Thing to set a status triggering the popup. This Thing will have two properties and one service: Properties trigger (Boolean) - to indicate if an error status is present or not, if so - trigger the popup selectedReason (Number) - to indicate the selected reason / root cause chosen in the modal popup Service clearTrigger - to reset the trigger to "false" once a reason has been selected The selectedReason will be logged into a ValueStream. In addition to the Thing and the ValueStream we will need a StateDefinition to pre-define potential root causes to be displayed in the popup. We will use three states to be used in a traffic-light fashion to indicate the severity of the issue in a custom color schema. To display the monitoring Mashup and the popup we will need two Mashups.
View full tip
This post is part of the series Forced Root Cause Monitoring via Mashups and Modal Popups To not feel lost or out of context, it's recommended to read the main post first. Create Entities AlertStateDefinition Create a new StateDefinition called "rcp_AlertStateDefinition" In the State Information tab, select Apply State: Numeric from the list on the right hand side Create a new State: Less than or equal to "1" Display Name: "Something good" Style: a new custom style with text color #f5b83d (orange) Create a new State: Less than or equal to "2" Display Name: "Something bad" Style: a new custom style with text color #f55c3d (red) Create a new State: Less than or equal to "3" Display Name: "Something ugly" Style: a new custom style with text color #ad1f1f (red) with a Font Bold Edit the "Default" State Set the Style: a new custom style with text color #36ad1f (green) We will not use this style, but in case we need a default configuration it will blend into the color schema Save the StateDefinition ValueStream Create a new ValueStream called "rcp_ValueStream" (choose a default ValueStream, not a RemoteValueStream) Save the ValueStream AlertThing Create a new Thing called "rcp_AlertThing" Based on a Generic Thing Base Thing Template Using the rcp_ValueStream Value Stream In the Properties and Alerts tab create the following Properties Name: "trigger" Base Type: BOOLEAN With a Default Value of "false" Check the "Persistent" checkbox Name: "selectedReason" BaseType: NUMBER Check the "Persistent" checkbox Check the "Logged" checkbox Advanced Settings: Data Change Type: ALWAYS In the Services tab create a new Service Name: "clearTrigger" No Inputs and no Outputs Service code me.trigger = false; When this service is executed, it will set the trigger Property to false Click Done to complete the Service creation Save the Thing
View full tip
This is part 3 out of 3 videos on Getting Started with ThingWorx Analytics During this video you will learn:   Executing a “Signals” Job Getting the results of the “Signals” Job Executing a “Training Model” Job Getting the results of the “Training Model” Job   Updated Link for access to this video:  Getting Started with ThingWorx Analytics: Part 3 of 3
View full tip
This script dumps all the alarms for a model or asset to JSON. Parameters (one or the other must be provided): modelName - (OPTIONAL) String - name of the model assetId - (OPTIONAL) String - id of the asset import com.axeda.common.sdk.id.Identifier import com.axeda.drm.sdk.Context import com.axeda.drm.sdk.audit.AuditCategory import com.axeda.drm.sdk.audit.AuditMessage import com.axeda.drm.sdk.scripto.Request import groovy.json.* import net.sf.json.JSONObject import java.net.URLDecoder import static com.axeda.sdk.v2.dsl.Bridges.* import com.axeda.services.v2.CustomObjectCriteria import com.axeda.services.v2.CustomObjectType import com.axeda.services.v2.CustomObject import com.axeda.services.v2.ExecutionResult import com.axeda.services.v2.ExtendedMap import com.axeda.drm.sdk.device.Model import com.axeda.drm.sdk.device.ModelFinder import com.axeda.drm.sdk.device.DeviceFinder import com.axeda.drm.sdk.device.Device import com.axeda.services.v2.ModelCriteria import com.axeda.services.v2.ModelType import com.axeda.services.v2.FindModelResult import com.axeda.services.v2.AssetCriteria import com.axeda.services.v2.FindAssetResult import com.axeda.services.v2.AlarmCriteria import com.axeda.sdk.v2.bridge.MobileLocationBridge import com.axeda.drm.sdk.mobilelocation.MobileLocationFinder import com.axeda.drm.sdk.mobilelocation.CurrentMobileLocationFinder import com.axeda.drm.sdk.mobilelocation.MobileLocation import com.axeda.drm.sdk.data.AlarmState import com.axeda.drm.sdk.data.AlarmFinder import com.axeda.drm.sdk.data.Alarm import com.axeda.platform.sdk.v1.services.ServiceFactory import com.axeda.drm.sdk.data.CurrentDataFinder import com.axeda.drm.sdk.device.DataItem import com.axeda.drm.sdk.data.HistoricalDataFinder import com.axeda.drm.sdk.data.DataValue import com.axeda.drm.sdk.data.DataValueList import com.axeda.platform.sdk.v1.services.extobject.ExtendedObjectSearchCriteria import com.axeda.common.date.DateRange import com.axeda.common.date.ExplicitDateRange /** * GetModel_Or_Asset_Alarms.groovy * ----------------------- * * Returns assets with organizations, alarms, and current mobile location. * * @params * modelName (OPTIONAL) Str - the name of the model to retrieve assets * assetId (OPTIONAL) Long - the id of the asset - one of the two is REQUIRED * * * @author sara streeter <sstreeter@axeda.com> * */ /** * initialize our global variables * json = the contents of our response * infoString = a stringBuilder used to collect debug information during the script * contentType = the content type we will return * scriptname = The name of this Script, used in multiple places */ def json = new groovy.json.JsonBuilder() def infoString = new StringBuilder() def contentType = "application/json" def scriptName = "GetModel_Or_Asset_Alarms.groovy" def root = [:] def timings = [:] timings.dataItemList = 0 timings.currentdata = 0 timings.histdata = 0 timings.wholescript = 0 timings.alarms = 0 timings.loop = 0 timings.filter = 0 timings.devices = 0 timings.geocode = 0 wholestart = System.currentTimeMillis() final def Context CONTEXT = Context.getSDKContext() def deviceList List<Device> devices try {     /* BUSINESS LOGIC GOES HERE */       def modelName = Request.parameters.modelName     def assetId     def alarms     AlarmFinder alarmFinder = new AlarmFinder(CONTEXT)       if (Request.parameters.assetId != null && Request.parameters.assetId != ""){         assetId = Request.parameters.assetId         DeviceFinder deviceFinder = new DeviceFinder(CONTEXT, new Identifier(assetId as Long));         def device = deviceFinder.find()         if (device){             alarmFinder.setDevice(device)             modelName = device.model.name         }     }     else if (modelName){               try{         modelName = new URLDecoder().decode(modelName)         }         catch(e){ logger.info(e.localizedMessage) }         if (modelName != null && modelName !=""){             ModelFinder modelFinder = new ModelFinder(CONTEXT)             modelFinder.setName(modelName)             Model model = modelFinder.find()                      if (model){                 modelName = model?.name                 alarmFinder.setModel(model)             }         }      }       alarms = alarmFinder.findAll()     // build the json from the models          root = [              "result": [              "model": modelName,              "assetId": assetId,              "alarms":alarms?.inject([]){ aList, alarm ->                    aList << [                         "deviceId": alarm.device?.id?.value,                        "deviceName": alarm.device.name,                        "deviceSerial": alarm.device.serialNumber,                         "name": alarm.name,                         "id": alarm.id.value,                         "state": alarm.state.name,                         "description": alarm.description,                         "severity": alarm.severity,                         "timestamp": alarm.date.time                    ]                                      aList               }             ]          ]     /* BUSINESS LOGIC ENDS HERE */ } catch (Exception e) {     def errorCode = "123456"     processException(scriptName,json,e,errorCode) } finally {     timings.wholescript = System.currentTimeMillis() - wholestart     root += [params: Request.parameters]     root += [timings: timings] } return ['Content-Type': 'application/json', 'Content': JSONObject.fromObject(root).toString(2)] /* * * ACTIVE CODE ENDS HERE * */ //---------------------------------------------------------------// /* * * HELPER METHODS START BELOW * */ /** * Wrap-up the response in our standard return map * @param contentType The global contentType variable * @param response The contents of the response (String ONLY) */ private def createReturnMap(String contentType, String response) {     return ["Content-Type": contentType,"Content":response] } /*     Processes the contents of an Exception and add it to the Errors collection     @param json The markup builder */ private def processException(String scriptName, JsonBuilder json, Exception e, String code) {     // catch the exception output     def logStringWriter = new StringWriter()     e.printStackTrace(new PrintWriter(logStringWriter))     logger.error("Exception occurred in ${scriptName}: ${logStringWriter.toString()}")     /*         Construct the error response         - errorCode Will be an element from an agreed upon enum         - errorMessage The text of the exception      */     json.errors  {         error {             errorCode   "${code}"             message     "[${scriptName}]: " + e.getMessage()             timestamp   "${System.currentTimeMillis()}"         }     }     return json } /*     Log a message. This will log a message and add it to info String     @param logger The injected logger     @param scriptName The name of the script being executed     @param info The infoString to append to     @param message The actual message to log */ private def logMessage(def logger, String scriptName, StringBuilder info, String message) {     logger.info(message)     info.append(message+"\n") } /*     Audit a message. This will store a message in the Audit log, based on the supplied category.     @param category The category for this audit message. One of: "scripting", "network", "device" or "data". Anything not recognized will be treated as "data".     @param message The actual message to audit     @param assetId If supplied, will associate the audit message with the asset at this ID */ private def auditMessage(String category, String message, String assetId) {     AuditCategory auditCategory = null     switch (category) {         case "scripting":             auditCategory = AuditCategory.SCRIPTING;             break;         case "network":             auditCategory = AuditCategory.NETWORK;             break;         case "device":             auditCategory = AuditCategory.DEVICE_COMMUNICATION;             break;         default:             auditCategory = AuditCategory.DATA_MANAGEMENT;             break;     }     if (assetId == null) {         new AuditMessage(Context.create(),"com.axeda.drm.rules.functions.AuditLogAction",auditCategory,[message]).store()     } else {         new AuditMessage(Context.create(),"com.axeda.drm.rules.functions.AuditLogAction",auditCategory,[message],new Identifier(Long.valueOf(assetId))).store()     } } def findOrCreateExtendedMap(String name){        // should take a name of Extended Map and output an object of type Extended Map, if it outputs null we throw an Exception        def outcome = [:]        outcome.extendedMap        ExtendedMap extendedMap = extendedMapBridge.find(name)        if (!extendedMap){             extendedMap = new ExtendedMap(name: name)            extendedMapBridge.create(extendedMap)        }        if (extendedMap) {         ExecutionResult result = new ExecutionResult()         result.setSuccessful(true)         result.setTotalCount(1)         outcome.result = result         outcome.extendedMap = extendedMap        }        else {            ExecutionResult result = new ExecutionResult()            result.setSuccessful(false)            result.setTotalCount(1)            outcome.result = result        }         return outcome    }    def retrieveModels(){       // retrieves the list populated by a separate script        def outcome = [:]        outcome.modelList        ModelCriteria modelCriteria = new ModelCriteria()        modelCriteria.type = ModelType.STANDALONE        FindModelResult modelResult = modelBridge.find(modelCriteria)        if (modelResult.models.size() > 0){         ExecutionResult result = new ExecutionResult()         result.setSuccessful(true)         result.setTotalCount(1)         outcome.result = result         outcome.modelList = modelResult.models        }        else {            ExecutionResult result = new ExecutionResult()            result.setSuccessful(false)            result.setTotalCount(1)            outcome.result = result        }         return outcome    }    def returnModelsWithAssets(List<com.axeda.services.v2.Model> modelList){        def outcome = [:]        outcome.modelList        outcome.message        if (!modelList || modelList?.size() ==0){            ExecutionResult result = new ExecutionResult()           result.setSuccessful(false)           result.setTotalCount(1)           outcome.result = result           outcome.message = "returnModelsWithAssets: Model list was not supplied or was of size zero."           return outcome        }        DeviceFinder deviceFinder = new DeviceFinder(CONTEXT)        ModelFinder modelFinder = new ModelFinder(CONTEXT)        List<com.axeda.drm.sdk.device.Model> sortedList = modelList.inject([]){ target, amodel ->             modelFinder.setName(amodel.modelNumber)            com.axeda.drm.sdk.device.Model bmodel = modelFinder.find()            deviceFinder.setModel(bmodel)            def numAssets = deviceFinder.findAll().size()            if (numAssets > 0 ){                   target << bmodel             }             target        }.sort{ amodel, bmodel ->  amodel.name <=> bmodel.name}        if (sortedList.size() > 0){         ExecutionResult result = new ExecutionResult()         result.setSuccessful(true)         result.setTotalCount(1)         outcome.result = result         outcome.modelList = sortedList        }        else {           ExecutionResult result = new ExecutionResult()           result.setSuccessful(false)           result.setTotalCount(1)           outcome.result = result       }         return outcome    }     def addMapEntry(String mapName, String key, String value){        def outcome = [:]         outcome.key         outcome.value         ExecutionResult result = extendedMapBridge.append(mapName, key, value)         outcome.result = result         if (result.successful){             outcome.key = key             outcome.value = value         }         return outcome    }
View full tip
This script creates a csv file from the audit log filtered by the User Access category, so dates of when users logged in or logged out. *** see update below *** Note:  The csv file has the same name as the Groovy script and does NOT have the .csv extension . To get the .csv extension, the Groovy script has to be renamed to AuditEntryToCSV.csv.groovy .  Suggestions on how to improve this are welcome. *** Update ***: The download works without the renamed groovy script by returning text instead of an input stream.  The script has been modified to illustrate this. Parameters: days - the number of days past to fetch audit logs model_name - the model name of the asset serial_number - the serial number of the asset import com.axeda.drm.sdk.device.ModelFinder import com.axeda.drm.sdk.Context import com.axeda.common.sdk.id.Identifier import com.axeda.drm.sdk.device.Model import com.axeda.drm.sdk.device.DeviceFinder import com.axeda.drm.sdk.device.Device import com.axeda.drm.sdk.audit.AuditCategoryList import com.axeda.drm.sdk.audit.AuditCategory import com.axeda.drm.sdk.audit.AuditEntryFinder import com.axeda.drm.sdk.audit.SortType import com.axeda.drm.sdk.audit.AuditEntry import groovy.xml.MarkupBuilder import com.axeda.platform.sdk.v1.services.ServiceFactory /* * AuditEntryToCSV.groovy * * Creates a csv file from the audit log filtered by the User Access category, so dates of when users logged in or logged out. * * @param days        -   (REQ):Str number of days to search. * @param model_name        -   (REQ):Str name of the model. * @param serial_number        -   (REQ):Str serial number of the device. * * @note - the csv file has the same name as the Groovy script and does NOT have the .csv extension . To get * the .csv extension, the Groovy script has to be renamed to AuditEntryToCSV.csv.groovy . * * @author Sara Streeter <sstreeter@axeda.com> */ def writer = new StringWriter() def xml = new MarkupBuilder(writer) try {    def ctx = Context.getUserContext()    ModelFinder modelFinder = new ModelFinder(ctx)    modelFinder.setName(parameters.model_name)    Model model = modelFinder.find()    DeviceFinder deviceFinder = new DeviceFinder(ctx)    deviceFinder.setSerialNumber(parameters.serial_number)    Device device = deviceFinder.find()    AuditCategoryList acl = new AuditCategoryList()    acl.add(AuditCategory.USER_ACCESS)    long now = System.currentTimeMillis()    Date today = new Date(now)    def paramdays = parameters.days ? parameters.days: 5    long days = 1000 * 60 * 60 * 24 * Integer.valueOf(paramdays)    AuditEntryFinder aef = new AuditEntryFinder(ctx)    aef.setCategories(acl)    aef.setToDate(today)    aef.setFromDate(new Date(now - (days)))    aef.setSortType(SortType.DATE)    aef.sortDescending()    List<AuditEntry> audits = aef.findAll() // use a Data Accumulator to store the information def dataStoreIdentifier = "FILE-CSV-audit_log" def daSvc = new ServiceFactory().dataAccumulatorService if (daSvc.doesAccumulationExist(dataStoreIdentifier, device.id.value)) {     daSvc.deleteAccumulation(dataStoreIdentifier, device.id.value) } // assemble the response    audits.each { AuditEntry audit ->            def row = [                audit?.id.value,                audit?.user?.username,                audit?.date,                audit?.category?.bundleKey,                audit?.message            ]         row = row.join(',')         row += '\n'         daSvc.writeChunk(dataStoreIdentifier, device.id.value, row);        } // stream the data accumulator to create the file    InputStream is = daSvc.streamAccumulation(dataStoreIdentifier, device.id.value) return ['Content-Type': 'text/csv', 'Content-Disposition':'attachment; filename=AuditEntryCSVFile.csv', 'Content': is.text] } catch (def ex) {    xml.Response() {        Fault {            Code('Groovy Exception')            Message(ex.getMessage())            StringWriter sw = new StringWriter();            PrintWriter pw = new PrintWriter(sw);            ex.printStackTrace(pw);            Detail(sw.toString())        }    } logger.info(writer.toString()) }
View full tip
Push update what is it? It is a mechanism that GetProperties supports so that the Server can push a value to a client mashup. This will allow you to see values update in your mashup real time without needing the refresh widget. Another great way to use the push updates is to propagate events that tie to specific content fetches. Let's say your mashup has three areas: KPIs, Alerts, Live values. Using some logic server side you can set up a 'tracker' Thing with properties that indicate that one of those areas has updated data. Bring these notifications as property values into the mashup using GetProperties and as the Server pushes updates to the mashup runtime, you can map it to a Validator or Expression widget (set to autoevaluate) which in turn can now run the necessary Service to fetch the updated information for the specific area.
View full tip
Background Getting a performance benchmark of your running application is an important thing to do when deploying and scaling up an application in production.  This not only helps focus in on performance issues quickly, but also allows for safely planning for scaling up and resource sizing based on real concrete data.   I recently created a tool and made a post about capturing and analysing ThingWorx utilisation statistics to do such an analysis, as well as identifying potential performance bottlenecks. Although they are rich and precise, utilisation statistics fall short in a number of areas however - specifically being able to count and time specific service executions, as well as identifying and sorting based on the host executing the service.   Tomcat Access Log Analysis As ThingWorx is a Tomcat web application, Tomcat logs details of the requests being made to the application server and ThingWorx REST API.  The default settings include the host (IP address), date/timestamp, and request URI; which can be decoded to reveal relevant details like the calling entities and service executions.   Adding 3 key additional variables (%s %B %D) to the server.xml access log value also gives us the HTTP response code, service execution time, and bytes returned from Tomcat.  This is super useful as we can now determine exact time of service executions, and run statistics on their execution totals and execution time.     Once you have an access log file looking like the one above, you can attempt to load it into the access_log sheet in the analysis Excel workbook that I created.  You do this by click on the access_log table, then selecting "Data > Get Data > Data Source Settings".  You'll then be prompted with the following or similar pop-up allowing you to navigate to your access_log file to select and then load.     It should be noted that you'll have to Refresh the table after selecting the new access_log.txt file so that it is read in and populates the table.  You can do this by right-clicking on the table and saying Refresh, or using the Data > Refresh button.   This workbook relies on a number of formulas to slice and dice the timestamp, and during my attempts at importing I had significant issues with this due to some of the ways that Excel does things automatically without any manual options.  You really need to make sure that the timestamps are imported and converted correctly, or something in the workbook will likely not work as intended.  One thing that I had to do was to add 1 second to round up 00:00:00 for the first entries as this was being imported as a date without the time part, and then the next lines imported as a date/time.   Depending on how many lines your file is, you'll likely also have to "Fill Down" the formulas on the right side of the sheet which may be empty in the table after importing your new data set.  I had the best results by selecting the cells in question on the last row, then going down to the bottom corner, pushing and holding Shift, clicking on the last cell bottom right, and then selecting Home > Fill > Down to pull the formulas down from the top.   Once the data is loaded, you'll be able to start poking around.  The filters and sorting by the named columns is really helpful as you can start out by doing things like removing a particular host, sorting by longest execution times, selecting execution times greater than 4 seconds, or only showing activity aimed at a particular entity or service.     You really need to make sure that the imported data worked fine and looks perfect, as the next steps will totally break if not.  With the data loaded, you can now go to the Summary Data table and right-click on one of the tables and select Refresh.  This is reload the data in into the pivot table and re-run their calculations.   Once the refresh is complete, you should see the table summary like shown here; there are Day, Hour, and Minute expand/collapse buttons.  You should also see the Day, Hour, Month fields showing in the Field Definitions on the right.  This is the part that is painful -- if the dates are in the wrong format and Excel is unable to auto-detect everything in the same way, then you will not get these automatically created fields.     With the data reloaded, and Pivot Tables re-built, you should be able to go over to the Dashboard sheet to start looking at and analysing the graphs.  This one is showing the Top 10 services organised into hourly buckets with cumulated service execution times.     I'm not going to go into all of the workbooks features, but you can also individually select a set of key services that you want to have a look at together across both the execution count and execution time dimensions.     Next you can see the coordinated view of both total service execution time over number or service executions.  This is helpful for looking for patterns where a service may be executing longer but being triggered the same amount of times, compared to both being executed and taking more time.  I've created a YouTube video (see bottom) which goes through using all of the features as well as providing other pointers to using it.     Getting into a finer level of detail, this "bonus" sheet provides a Pivot Table and Pivot Chart which allows for exploring minimum, maximum and average execution time for a specific service.  Comparing this with the utilisation subsystem metrics taken during the same period now provide much deeper insight as we can pinpoint there the peaks were, how long they lasted, and where the slow executions were in relation to other services being executed at that time (example: identifying many queries/data processing occurring simultaneously).     Without further ado, you can download and play with my ThingWorx Tomcat Access Log Analysis Excel Workbook, and check out the recorded demonstration and explanation for more details on loading and analysis use. [YouTube] ThingWorx Tomcat Access Logs - Service Performance Analysis
View full tip
Video Author:                     Polina Osipova Original Post Date:            June 10, 2016   Description: This is a video tutorial on configuring properties for a Thing, and using "Manage Bindings" to bind properties to a Thing.      
View full tip
When an Expression Rule of type MobileLocation calls a Groovy script, the script is provided with the implicit object mobileLocation.  This example shows how the mobileLocation object can be used. This Expression Rule calls the Groovy script 'getAddress' to retrieve the location and translate it into a street address: Type:  MobileLocation IF:      some condition e.g. true THEN:  SetDataItem("location", str(ExecuteCustomObject("getAddress"))) The 'getAddress' script uses the mobileLocation object to retrieve the asset's reported location, and then calls a REST service to translate a given latitude and longitude to a street address.  The street address is returned. import groovyx.net.http.RESTClient String rmdHostname =  "http://ws.geonames.org"; if (mobileLocation != null) { rmd = new RESTClient(rmdHostname); try {       def resp = rmd.get( path: 'findNearestAddress',                       query:[lat:mobileLocation.lat , lng:mobileLocation.lng] )        streetnum = resp.data.address.streetNumber.text()        street = resp.data.address.street.text()        town = resp.data.address.adminName2.text()        state = resp.data.address.adminCode1.text()        postalcode = resp.data.address.postalcode.text()            return streetnum + " " + street + " " +  town + " " + state + " " + postalcode } catch (groovyx.net.http.HttpResponseException e) {     e.printStackTrace();     } }
View full tip
This groovy script will return a list of users based off a given UserGroup and allows for filtering by username. import com.axeda.drm.sdk.Context import groovyx.net.http.HTTPBuilder import static groovyx.net.http.ContentType.* import static groovyx.net.http.Method.* import net.sf.json.JSONObject import groovy.json.* import com.axeda.drm.sdk.data.* import com.axeda.drm.sdk.device.* import com.axeda.drm.sdk.user.UserFinder import com.axeda.drm.sdk.user.User import com.axeda.drm.sdk.user.UserGroupFinder //-------------------------------------------------------------------------------------------------------------------- // Example of getting Users from a User Group and filtering by username // //-------------------------------------------------------------------------------------------------------------------- def response = [:] def result = [] try {     final def CONTEXT = Context.create(parameters.username)       UserFinder uFinder = new UserFinder(CONTEXT)     UserGroupFinder ugFinder = new UserGroupFinder(CONTEXT)     List userGroups = getUserGroupsList(ugFinder, "*Demo*")     List SmithsInDemoGroup = userGroups.collect{ usergroup ->         usergroup.getUsers().findResults{ user ->             if (user.username =~ /Smith/){                                      user                                      }                      }     }.flatten()     SmithsInDemoGroup.each{ u ->         result << u.fullName     }   response = [     result: [             items: result     ]   ] } catch (Exception e) {     def m = ""     e.message.each { ex -> m += ex }     response = [                 faultcode: 'Groovy Exception',                 faultstring: m             ]; } return ['Content-Type': 'application/json', 'Content': JSONObject.fromObject(response).toString(2)];   def getUserGroupsList(UserGroupFinder ugFinder, String name){     ugFinder.setName(StringQuery.like(name))     def userGroup = ugFinder.findOne()     List userGroups = new ArrayList();     userGroups.add(userGroup);     return userGroups }
View full tip
This project is a simple custom tab that allows you to search all models and see their assets with basic information.  It is packaged as an Axeda SDK v2 Artisan project. Further Reading Developing with Axeda Artisan (Axeda Platform v6.8 and later) Axeda Sample Application: Populating A Web Page with Data Items Extending the Axeda Platform UI - Custom Tabs and Modules
View full tip
Hiya,   I recently prepared a short demo which shows how to onboard and use Azure IoT devices in ThingWorx and added some usability tips and tricks to help others who might struggle with some of the things that I did.     The good news... I recorded and posted it to YouTube here.   •Connect Azure IoT Hub with ThingWorx (to be updated soon for 9.0 release) •Using the Azure IoT Dev Kit with ThingWorx •Getting the Azure IoT Hub Connector Up and Running (V3/8.5)   Enjoy, and don't hesitate to comment with your own tips and feedback.   Cheers,   Greg
View full tip
In this post I show how to use Federation in ThingWorx to execute services on a different ThingWorx platform instance. In the use case below I set up one ThingWorx instance in the Factory and another instance in the Cloud, whereby the latter is executing a service which is actually running on the former.   Please find the document in attachment.   HTH, Alessio Marchetti  
View full tip
When an Expression Rule of type Data calls a Groovy script, the script is provided with the implicit object dataItems.  This example shows how the dataItems object can be used.to get the dataitem information (value, name, type and update time) import com.axeda.drm.sdk.data.* import com.axeda.drm.sdk.device.DataItem try {         def deviceName = context.device.name         // implicit object dataItems passes a list of dataItem objects         def dataItemsList = dataItems         for(dio in dataItemsList) {                logger.info("Checking " + dio.name + " Value: " + dio.value)                if(dio.name == "updateTime") {                        logger.info("Found: " + dio.name + " Value: " + dio.value + " Type: " +    dio.perceptType + " Last Updated: " + new Date(dio.timeInMillis)) // perceptType = analog, digital or string                }         } } catch (Exception e) {         logger.error e.message }
View full tip
This article explains how to monitor concurrent user logins on the Axeda Platform. Its going to do this by creating an asset to monitor the solution. This asset will have a dataitem that tracks the users logged in. You can use this dataitem to trend usage during the day, or calculate the max per day. Or you could alarm if the number of users goes over some limit. The following needs to be created on your Platform : A model named “Monitor”, containing an analog DataItem named “userlogins” and an asset named “Metrics” This asset will receive the values. Expression Rule: Create two expression rules that update the number of users, triggered on user login or logout: Name : UserLoginMonitor Type: Userlogin and Userlogout (two rules required) IF:     true THEN: ExecuteCustomObject(“getUserLogins”, User.total) This calls a script and passes in User.total  = The total number of users that are logged into the system (Concurrently). Groovy Script (Custom Object) Now, the next step is to write a Groovy Script (Custom Object) You can copy and paste the following code into your Groovy script. Give the script a name : getUserLogins This script also has a parameter named logins import com.axeda.drm.sdk.device.DataItem; import com.axeda.drm.sdk.device.Device; import com.axeda.drm.sdk.Context import com.axeda.drm.sdk.device.DataItemFinder; import com.axeda.drm.sdk.device.ModelFinder; import com.axeda.drm.sdk.device.DeviceFinder; import com.axeda.drm.sdk.data.DataValueEntry import com.axeda.drm.sdk.device.Model; import com.axeda.drm.sdk.data.CurrentDataFinder; import com.axeda.drm.sdk.data.DataValue; def logins= parameters.logins def ctx = Context.create() def mod = loadModel("Monitor",ctx) def dev = loadDevice("Metrics",mod,ctx) DataItemFinder dif = new DataItemFinder(ctx) dif.setModel(mod) dif.setDataItemName("userlogins") DataItem di = dif.find() DataValueEntry dve = new DataValueEntry(ctx, dev, di, logins) dve.store()     public void setDataItem(String dataItemName, Integer dataItemValue, Device device, Context context)     {         DataItemFinder dif = new DataItemFinder(ctx);         dif.setDataItemName(dataItemName);         dif.setModel(device.getModel());         DataItem di = dif.find();         DataValueEntry dve = new DataValueEntry(ctx, dev, di, newValue)         dve.store()     }     public DataValue findCurrentDataItemValue(Device device, String dataItemName,Context ctx)     {         CurrentDataFinder cdFinder =  new CurrentDataFinder(ctx,device);         DataValue dv = cdFinder.find(dataItemName);         return dv;     }     public DataItem loadDataItem(Model model,String dataItemName, Context ctx)      {         DataItemFinder iFinder = new DataItemFinder(ctx);         iFinder.setDataItemName(dataItemName);         iFinder.setModel(model);         return iFinder.find();     }     public Model loadModel(String modelNumber, Context ctx)     {         ModelFinder mf = new ModelFinder(ctx);         mf.setName("Monitor");         return mf.find();     }     public Device loadDevice(String serialNumber,Model model, Context context ) {         DeviceFinder df = new DeviceFinder(context);         df.setSerialNumber(serialNumber);         df.setModel(model);         return df.find();     } Whenever a user logs into the Axeda Platform, the Metrics asset will show the concurrent number of logged in users in the Platform. This dataitem can be graphed to see the pattern of usage. If you wanted to take action based on the number of users, create an expression rule to alarm when a threshold is reached. This rule should be associated with the model "Monitor". Name : LoginsCheck Type: Data IF:     userlogins > 40 THEN: CreateAlarm("Login limit", 100, str(userlogins)+" users") Now an alarm is created each time too many users are logged in. An alarm can be used for notifications or viewed on the Monitor asset.
View full tip
This Groovy script gets the weather forecast for a given lat/long by calling an external web service. Use in an Expression rule like this: If: something Then: SetDataItem ("precipitation", round(ExecuteCustomObject ("GetPrecipitation", location) )) This sets the dataitem "precipitation" to the value returned by this script. Parameters: Variable Name               Display Name location                         localtion (lat, lon) import org.apache.commons.httpclient.methods.* import org.apache.commons.httpclient.* import java.text.SimpleDateFormat def location = parameters.location.toString() def locparts = location.split(',') def lat = locparts[0] def lon = locparts[1] def hostname = " www.weather.gov" def url = "/forecasts/xml/sample_products/browser_interface/ndfdXMLclient.php" String ndfdElement = "pop12" // see http://www.weather.gov/forecasts/xml/docs/elementInputNames.php def perceptTimeFormat = new SimpleDateFormat ("yyyy-MM-dd'T'HH:mm:ss"); def cal = Calendar.getInstance(); Date startDate = cal.getTime() cal.add(Calendar.HOUR,12) Date endDate = cal.getTime() def client = new HttpClient () HostConfiguration host = client.getHostConfiguration() host.setHost(hostname, 80, "http") GetMethod get = new GetMethod (url) NameValuePair [] params = new NameValuePair [6] params[0] = new NameValuePair ("lat", lat); params[1] = new NameValuePair ("lon", lon); params[2] = new NameValuePair ("product", 'time-series'); params[3] = new NameValuePair ("begin", perceptTimeFormat.format(startDate)); params[4] = new NameValuePair ("end", perceptTimeFormat.format(endDate)); params[5] = new NameValuePair (ndfdElement, ndfdElement); get.setQueryString(params) client.executeMethod(host, get); message = "Status:" + get.getStatusText() content = get.getResponseBodyAsString() get.releaseConnection() // parse result XML and compute average def dwml = new XmlSlurper ().parseText(content) readings = dwml.data.parameters."probability-of-precipitation".value.collect { Integer.parseInt(it.toString()) } average = readings.sum() / readings.size() //logger.info "Expected precipitation for $location is $readings" return readings[0]
View full tip
import com.axeda.drm.sdk.Context import com.axeda.drm.sdk.device.ModelFinder import com.axeda.drm.sdk.device.Model import com.axeda.drm.sdk.device.DeviceFinder import com.axeda.drm.sdk.data.CurrentDataFinder import com.axeda.drm.sdk.device.Device import com.axeda.drm.sdk.data.HistoricalDataFinder import net.sf.json.JSONObject /* * DataItemEachDevice.groovy * * Find the current data item and historical data items for all assets in a given model. * * @param model_name        -   (REQ):Str name of the model. * @param data_item_name    -   (REQ):Str name of the data item to query on. * @param from_time         -   (REQ):Long millisecond timestamp to begin query from. * @param to_time           -   (REQ):Long millisecond timestamp to end query at. * * @note from_time and to_time should be provided because it limits the query size. * * @author Sara Streeter <sstreeter@axeda.com> */ def response = [:] // measure the script run time def timeProfiles = [:] def scriptStartTime = new Date() try { // getUserContext is supported as of release 6.1.5 and higher     final def CONTEXT = Context.getUserContext() // confirm that required parameters have been provided     validateParameters(actual: parameters, expected: ["model_name", "data_item_name", "from_time", "to_time"]) // find the model     def modelFinder = new ModelFinder(CONTEXT)     modelFinder.setName(parameters.model_name)     Model model = modelFinder.findOne() // throw exception if no model found     if (!model) {         throw new Exception("No model found for ${parameters.model_name}.")     } // find all assets of that model     def assetFinder = new DeviceFinder(CONTEXT)     assetFinder.setModel(model)     def assets = assetFinder.findAll() // find the current and historical data values for each asset //note: since device will be set on the datafinders going forward, a dummy device is set on instantiation which is not actually stored     def currentDataFinder = new CurrentDataFinder(CONTEXT, new Device(CONTEXT, "placeholder", model))     def historicalDataFinder = new HistoricalDataFinder(CONTEXT, new Device(CONTEXT, "placeholder", model))     historicalDataFinder.startDate = new Date(parameters.from_time as Long)     historicalDataFinder.endDate = new Date(parameters.to_time as Long) // assemble the response     assets = assets.collect { Device asset ->         currentDataFinder.device = asset         def currentValue = currentDataFinder.find(parameters.data_item_name)         historicalDataFinder.device = asset         def valueList = historicalDataFinder.find(currentValue?.dataItem)         [                 id: asset.id.value,                 name: asset.name,                 serialNumber: asset.serialNumber,                 model: [id: asset.model.id.value, name: asset.model.name],                 current_data: currentValue.asString(),                 historical_data: valueList.collect { [timestamp: it.getTimestamp().format("yyyyMMdd HH:mm"), value: it.asString()] }         ]     }     response = [result: [items: assets]] } catch (def ex) {     logger.error ex     response += [             error: [                     type: "Backend Application Error", msg: ex.getLocalizedMessage()             ]     ] } finally { // create and output the running time profile     timeProfiles << createTimeProfile("DataItemEachDevice", scriptStartTime, new Date())     response += [params: parameters, meta: [:], timeProfiles: timeProfiles] } return ['Content-Type': 'application/json', 'Content': JSONObject.fromObject(response).toString(2)] private Map createTimeProfile(String label, Date startTime, Date endTime) {     [             (label): [                     startTime: [timestamp: startTime.time, readable: startTime.toString()],                     endTime: [timestamp: endTime.time, readable: endTime.toString()],                     profile: [                             elapsed_millis: endTime.time - startTime.time,                             elapsed_secs: (endTime.time - startTime.time) / 1000                     ]             ]     ] } private validateParameters(Map args) {     if (!args.containsKey("actual")) {         throw new Exception("validateParameters(args) requires 'actual' key.")     }     if (!args.containsKey("expected")) {         throw new Exception("validateParameters(args) requires 'expected' key.")     }     def config = [             require_username: false     ]     Map actualParameters = args.actual.clone() as Map     List expectedParameters = args.expected     config.each { key, value ->         if (args.options?.containsKey(key)) {             config[key] = args.options[key]         }     }     if (!config.require_username) { actualParameters.remove("username") }     expectedParameters.each { paramName ->         if (!actualParameters.containsKey(paramName) || !actualParameters[paramName]) {             throw new IllegalArgumentException(                     "Parameter '${paramName}' was not found in the query; '${paramName}' is a reqd. parameter.")         }     } } Sample Output: {   "result": {     "items": [{       "id": 4240,       "name": "ASVM_9",       "serialNumber": "ASVM_9",       "model": {         "id": 1535,         "name": "SimVM4"       },       "current_data": "142.0",       "historical_data": [{         "timestamp": "20120331 17:00", "value": "142.0"       }, {         "timestamp": "20120331 16:59", "value": "143.0"       }, {         "timestamp": "20120331 16:59", "value": "144.0"       }, {         "timestamp": "20120331 16:58", "value": "145.0"       }, {         "timestamp": "20120331 16:58", "value": "146.0"       }, {         "timestamp": "20120331 16:57", "value": "147.0"       }, {         "timestamp": "20120331 16:57", "value": "148.0"       }, {         "timestamp": "20120330 19:30",         "value": "0.0"       }]     }, {       "id": 4246,       "name": "ASVM_12",       "serialNumber": "ASVM_12",       "model": {         "id": 1535,         "name": "SimVM4"       },       "current_data": "138.0",       "historical_data": [{         "timestamp": "20120331 17:00",        "value": "138.0"       }, {         "timestamp": "20120331 17:00",        "value": "139.0"       }, {         "timestamp": "20120331 16:59",        "value": "140.0"       }, {         "timestamp": "20120331 16:59",        "value": "141.0"       }, {         "timestamp": "20120331 16:59",        "value": "142.0"       }, {         "timestamp": "20120331 16:59",        "value": "143.0"       }, {         "timestamp": "20120330 19:32",         "value": "0.0"       }]      //      // MORE ASSETS HERE      //     }]   },   "params": {     "username": "sstreeter",     "from_time": "1332272219000",     "data_item_name": "CurrentStock",     "sessionid": "JOQ5I7ofRXYA-RnA37Vk93bRUH718yoFF5 9p0JbCnfyoHolFprf",     "model_name": "SimVM4",     "to_time": "1335469008000"   },   "meta": {},   "timeProfiles": {     "DataItemEachDevice": {       "startTime": {         "timestamp": 1335469168725,         "readable": "Thu Apr 26 19:39:28 GMT 2012"       },       "endTime": {         "timestamp": 1335469180569,         "readable": "Thu Apr 26 19:39:40 GMT 2012"       },       "profile": {         "elapsed_millis": 11844,         "elapsed_secs": 11.844       }     }   } }
View full tip
Announcements