This blog series is about an open source project "Script scheduler" I worked on recently, it allows to run scheduled and triggered scripts (jobs).
The series contains the following parts:
- Part 1 - Introduction and simple example "Periodic HelloSinusoid" (Displaying sinusoid data in the console based on a periodic interval)
- Part 2 - Advanced example: "Periodic YouLess" (IOT algorithm for creating Event Frames based on Watt Peak in an Energy Monitor device installed in our company building)
- Part 3 - Advanced example: "Triggered HelloSinusoid" (Writing PIWebAPI channel triggered Sinusoid data to CSV)
- Part 4 - Technical deep dive how everything sticks together, setup your own environment, add your own scripts, etc.
Triggered HelloSinusoid example
This more advanced example uses the PIWebAPI channel to trigger on changes of a data. It writes or appends all received data to a CSV file.
Below is the full Typescript code:
The code that is new is explained in the following steps:
1: The script starts with importing external dependencies that are used in the code:
New in this algorithm are:
- Line 7 is required because this is a triggered job and the defined class should implement the interface.
- Line 12 is required because we manual need to add basic authentication headers and the username / password is defined in this config file
- Line 14 is required for modernizing node.js so async/await can be used on the fs library.
- Line 15 is required for using a json to csv parser library
2: The webSocketOptions object:
rejectUnauthorized will bypass self signed certificates and an Authorization header will make the connection to PIWebApi possible with Basic Authentication.
3: The getChannelUrl method:
The triggered processor will use this string to open a WebSocket connection. The PIWebAPI service has a method to create this based on a PI Point path. The promise of the service is returned to the caller of this method.
This could also be a hardcoded url but then it should be wrapped in a Promise object which is directly resolved. This is natively supported because TypeScript is transpiled to ES6 which is almost 100% supported in Node.js.
4: Using the run function to parse the updates received from the websocket channel:
First some constants are set up:
- Line 41 for a new line in the csv.
- Line 41 for a string with the current date.
- Line 44 for declaring a new filename for each day.
This run method is called by the triggered processor when a new message is received with the WebSockets channel. This message is passed to the job.attrs.data object when the run function is called. In line 46 it is parsed to a JSON object. But we have to tell TypeScript first that this is an any because otherwise it expect it to be a complexer format. After it is declared as any, we can redeclare it as string and parse it with the JSON.Parse function. (Typings are ok in TypeScript but this is not so pretty!)
The channelData is a complex object with multiple layers with the support for multiple items with multiple changes. The first and only array item is Sinusoid so in line 48 Items.Items is passed to a typed array of the class TimedValue.
5: Using the run function to write to a CSV file:
The values need to be written to a CSV file so first an if check is done in line 51 to see if this file already exists. Because a modernised version of fs is used this asynchronous action can be awaited.
The csv.encode function translates a JSON object to a comma separated string with all the attributes of the object in a row. An object array like TimedValue is parsed to multiple rows.
Lines 52 and 53 are executed when a file exists, no headers are written and the new line(s) are appended to the existing ones.
Lines 55 and 56 are executed when no file exists, the headers and the new lines are written to a new created file.
6: The algorithm in action:
The value object in line 48 is an array of 2 values received from the PIWebAPI channel update.
After some updates the csv file looks like the screenshot shown above. Notice the generated file name with the date, the automatic generated headers based on the attributes in the TimedValue class and the first 2 rows matching the JSON values from the first screenshot.