--- title: "Scripting | Examples | N3uron KB V1.21" slug: "scripting-examples" description: "Scripting is a N3uron module designed to run custom scripts based on events like timers, tag changes, conditions, system start-up and system shutdown…" updated: 2025-11-12T22:26:14Z published: 2025-11-12T22:26:14Z canonical: "docs.n3uron.com/scripting-examples" --- > ## Documentation Index > Fetch the complete documentation index at: https://docs.n3uron.com/llms.txt > Use this file to discover all available pages before exploring further. # Examples ## [](/v121/docs/scripting-examples#_Toc36806232)Inserting tag values into a MySQL database Scripting can be used to connect to MySQL by using the [](https://www.npmjs.com/package/mysql)[](https://www.npmjs.com/package/mysql)[mysql](https://www.npmjs.com/package/mysql)[](https://www.npmjs.com/package/mysql)[](https://www.npmjs.com/package/mysql)[](https://www.npmjs.com/package/mysql)internal library. More information about internal libraries can be found at [](/v1-21/docs/configuration-11)[Libraries](/v121/docs/scripting-configuration)[](/v1-21/docs/configuration-11). The following example shows how to insert tag changes into the database. This assumes a database and a table are already created with the following schema: ![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/image-1650455759376.png) In order to insert the events, two scripts will be created, one serving as a wrapper over the raw MySQL connection (as a static library), and one startup script that will subscribe to events and export them to the database. The following libraries configuration is used: ![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/image-1650460145153.png) The mysql_connector static library contains the following script: ```javascript // Import the MySQL internal library const mysql = $.lib("mysql"); // Export the MySqlConnection class module.exports = class MySqlConnection { // Constructor of the class taking a MySQL client instance constructor(client) { this._client = client; // Error event handler that logs a message to the logger client.on("error", (err) => { $.logger.warn( "Error on client: %s (%d - %s)", err.message, err.errno, err.sqlMessage ); // Additionally, if the error is fatal, the client is dropped if (err.fatal) { this._client = null; } }); } // This function must be called when the client is no longer being used // in order to terminate the connection dispose() { return new Promise((resolve, reject) => { if (this._client) { // If the client is online, end the client this._client.end((err) => { if (err) { reject(err); } else { resolve(); } this._client = null; }); } else { reject(new Error("Connection already terminated")); } }); } // Checks whether the client is online or not online() { return this._client !== null; } // This function executes a query and resolve to a promise // containing a tuple with the first element being the result // of the query, and the second element the fields returned from // the query query(query, ...params) { return new Promise((resolve, reject) => { if (this._client) { this._client.query(query, params, (err, result, fields) => { if (err) { //If the error is fatal, set the client to null if (err.fatal) { this._client = null; } reject(err); } else { resolve([result, fields]); } }); } else { reject(new Error("Connection is terminated")); } }); } // Static method used to asynchronously create a new client, which is necessary since creating a connection // is an asynchronous operation static Connect(connOpts) { return new Promise((resolve, reject) => { // Create the client with the provided options const client = mysql.createConnection(connOpts); // Connect the client to the DB client.connect((err) => { if (err) { reject(err); } else { // If the connection is OK, create a MySqlConnection instance using the client // and resolve the promise with that instance const connection = new MySqlConnection(client); resolve(connection); } }); }); } }; ``` The startup script that relies on the library and is responsible for sending the event uses the following code: ```javascript const MySqlConnection = $.lib("mysql_connector"); // Define the options used to connect to MySQL const OPTIONS = { host: "localhost", port: 3306, user: "admin", password: "n3uron", database: "n3_scripting", }; // Define insertion period const PERIOD = 10000; // Define the tags that will be inserted into the DB const TAGS = ["/Simulated/String", "/Simulated/Bool", "/Simulated/Number"]; // Subscribe to the tags const subscription = await $.api.tag.subscribe(TAGS, onTagEvent); // Buffer that holds the events between insertions const events = []; // Database client let client = null; //Set a 10 seconds timer before pushong the events setTimeout(pushEvents, PERIOD); //Handler that gets called whenever the tags have a new event function onTagEvent(tag, data) { //Save the event to the buffer events.push({ tag, value: data.value, quality: data.quality, ts: data.ts }); } // Async function called every PERIOD milliseconds to insert data into the DB async function pushEvents() { // If the client is null or offline, connect the client if (!client || (client && !client.online())) { try { await connect(); } catch (ex) { // If the connection fails, schedule a new timer $.logger.warn("Error connecting: %s", ex.message); return setTimeout(pushEvents, PERIOD); } } // If the client is connected, remove the events from the buffer // and transform them into a format that can be used by MySQL const ev = events.splice(0).map((el) => { // Set number_ev, string_ev and bool_ev either to the event // or to null if the type does not match const number_ev = typeof el.value === "number" ? el.value : null; const string_ev = typeof el.value === "string" ? el.value : null; const bool_ev = typeof el.value === "boolean" ? el.value : null; // Return a 6 element tuple representing the values that will be inserted into the DB return [el.tag, number_ev, string_ev, bool_ev, el.quality, new Date(el.ts)]; }); try { $.logger.debug("Inserting %d events", ev.length); // Execute an INSERT query using ? as placeholder for the values await client.query( `INSERT into tag_events (tag, number_value, string_value, bool_value, quality, ts) VALUES ?`, ev ); $.logger.debug("Inserted OK", ev.length); } catch (ex) { $.logger.warn("Error during query: %s", ex.message); // If the query fails and the client is offline, null the client if (!client.online()) { client = null; } } finally { // After finishing the query (whether it was successful or not) // schedule a new timer setTimeout(pushEvents, PERIOD); } } // Creates a new MySQL client async function connect() { $.logger.debug( "Connecting to MySQL using %s@%s:%d", OPTIONS.user, OPTIONS.host, OPTIONS.port ); client = await MySqlConnection.Connect(OPTIONS); $.logger.debug("Connected OK"); } ``` ## Reading tag values from Microsoft SQLServer database This example reads data from a Microsoft SQL Server database and saves it to Scripting source tags. This is done using the [](https://www.npmjs.com/package/mssql)[](https://www.npmjs.com/package/mssql)[mssql](https://www.npmjs.com/package/mssql)[](https://www.npmjs.com/package/mssql)[](https://www.npmjs.com/package/mssql) NPM library, which is integrated in N3uron (and thus can be used as an internal library). This example assumes the target table has the following schema, and that it's data it's being updated from an external source: ![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/image-1650535206332.png) In this example, the table has the following data: ![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/image-1650535421130.png) This data will be saved to Scripting source tags using an alias, this allows the tags in N3uron to have a different structure to those in Microsoft SQL Server. A configuration of a source tag can be seen in the next screenshot: ![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/image-1650541622388.png) This example will use two scripts (a periodic script and a static library) as well as an internal library. The following two libraries are used: ![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/image-1650541849404.png) The mssql_connector static library has the following script: ```javascript // Import the Microsft SQL Server internal library const mssql = $.lib("mssql"); // Export the SqlServerConnection class module.exports = class SqlServerConnection { // Constructor of the class taking a SqlServerConnection client instance constructor(client) { this._client = client; // Error event handler that logs a message to the logger and sets the client to null client.on("error", (err) => { $.logger.warn("Error on client: %s", err.message); // Drop the client after an error this._client = null; }); } // This function must be called when the client is no longer being used // in order to terminate the connection dispose() { if (this._client) { this._client.close(); this._client = null; } else { throw new Error("Connection is already closed"); } } // Checks whether the client is online or not online() { return this._client !== null; } // This function executes a query and resolve to a promise // containing a tuple with the first element being the result // of the query, and the second element the fields returned from // the query async query(query, ...params) { if (this._client) { // Creates a new request on the connection, and executes the query return await new mssql.Request(this._client).query(query); } else { throw new Error("Connection is offline"); } } // Static method used to asynchronously create a new client, which is necessary since creating a connection // is an asynchronous operation static async Connect(connOpts) { //Create the client with the provided options and connect with DB const client = await mssql.connect(connOpts); //If the connection is OK, create a SqlServerConnection instance //using the client and return it return new SqlServerConnection(client); } }; ``` The periodic script has a 10-second trigger, and it's used to retrieve data from the database and save it to the source tags. It has the following code: ```javascript const SqlServerConnection = $.lib("mssql_connector"); // Define the options used to connect to Microsoft SQL Server const OPTIONS = { server: "10.1.3.169", port: 1433, user: "sa", password: "n3uron$!", database: "n3_scripting_manual", }; // If the local variables are not initialized, initialize them if ($.local.init === undefined) { await init(); } // Retrieve the data, and exit the script after the async call is finish await getData(); // Async function used to retrieve the data async function getData() { // If the client is not online, attempt to reconnect if ($.local.client === null || !$.local.client.online()) { const client = await connect(); // If the client connects OK, save it to $.local.client and keep going if (client !== null) { $.local.client = client; } else { // If the client is null, it means the connection was unsuccessful, so the query can't be executed return; } } try { // Try to retrieve the data using a SELECT query const queryResult = await $.local.client.query( "SELECT tag, value FROM dbo.data" ); // For every row in the result, destructure the row object into the tag and value variables // and execute a tag alias update for (const { tag, value } of queryResult) { $.api.tag.updateAlias(tag, value); } } catch (ex) { // If the query has an error, log the error $.logger.warn("Error during query: %s", ex.message); } } // Creates and returns a new Microsoft SQL Server client async function connect() { $.logger.debug( "Connecting to Microsoft SQL Server using %s@%s:%d", OPTIONS.user, OPTIONS.server, OPTIONS.port ); try { const client = await SqlServerConnection.Connect(OPTIONS); $.logger.debug("Connected OK"); return client; } catch (ex) { $.logger.warn("Error connecting client: %s", ex.message); return null; } } // Initializes the required local variables async function init() { $.local.init = true; // The client must be saved in a local variable to only has one instance // that persists between script executions $.local.client = null; } ``` ## [](/v121/docs/scripting-examples#_Toc36806232)Creating a table and inserting tag values into a Microsoft SQL database In this example, we define the table schema directly within the script and insert tag values into a Microsoft SQL Server database. This is done using the [](https://www.npmjs.com/package/mssql)[](https://www.npmjs.com/package/mssql)[mssql](https://www.npmjs.com/package/mssql)[](https://www.npmjs.com/package/mssql)[](https://www.npmjs.com/package/mssql) NPM library, which is integrated in N3uron (and thus can be used as an internal library). This example will use two scripts (a periodic script and a static library) as well as an internal library. The following two libraries are used: ![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/image-1724147064803.png) The mssql_connector static library has the following script: ```javascript // Import the Microsft SQL Server internal library const mssql = $.lib("mssql"); // Export the SqlServerConnection class module.exports = class SqlServerConnection { // Constructor of the class taking a SqlServerConnection client instance constructor(client) { this._client = client; // Error event handler that logs a message to the logger and sets the client to null client.on("error", (err) => { $.logger.warn("Error on client: %s", err.message); // Drop the client after an error this._client = null; }); } // This function must be called when the client is no longer being used // in order to terminate the connection dispose() { if (this._client) { this._client.close(); this._client = null; } else { throw new Error("Connection is already closed"); } } // Checks whether the client is online or not online() { return this._client !== null; } // This function executes a query and resolve to a promise // containing a tuple with the first element being the result // of the query, and the second element the fields returned from // the query async query(query, ...params) { if (this._client) { // Creates a new request on the connection, and executes the query return await new mssql.Request(this._client).query(query); } else { throw new Error("Connection is offline"); } } // Static method used to asynchronously create a new client, which is necessary since creating a connection // is an asynchronous operation static async Connect(connOpts) { //Create the client with the provided options and connect with DB const client = await mssql.connect(connOpts); //If the connection is OK, create a SqlServerConnection instance //using the client and return it return new SqlServerConnection(client); } }; ``` This periodic script, triggered every 10 seconds, establishes the connection settings to the database, creates the table if it doesn't already exist, and inserts data from the tags. The script includes the following code: ```javascript const SqlServerConnection = $.lib("mssql_connector"); // Define the options used to connect to Microsoft SQL Server const OPTIONS = { database: "Test", server: "127.0.0.1", connectionTimeout: 3000, port: 1433, authentication:{ type:"default", options:{ userName: "sa", domain: null, password: "n3uron$!", } }, pool: { max: 10, min: 0, idleTimeoutMillis: 100000 }, options: { encrypt: false, trustServerCertificate: false // change to true for local dev / self-signed certs } }; // Define insertion period const PERIOD = 10000; // Define the tags that will be inserted into the DB const TAGS = ["/VoltageBR", "/VoltageRY", "/VoltageYB"]; // Subscribe to the tags const subscription = await $.api.tag.subscribe(TAGS, onTagEvent); // Buffer that holds the events between insertions const events = []; // Database client let client = null; // Set a 10-second timer before pushing the events setTimeout(pushEvents, PERIOD); // Handler that gets called whenever the tags have a new event function onTagEvent(tag, data) { // Save the event to the buffer events.push({ tag, value: data.value, quality: data.quality, ts: data.ts }); } // Async function to ensure the table exists before starting data insertion async function ensureTableExists() { const createTableQuery = ` IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='TAG_EVENTS' AND xtype='U') CREATE TABLE TAG_EVENTS ( id INT PRIMARY KEY IDENTITY(1,1), tag NVARCHAR(255) NOT NULL, number_value FLOAT NULL, string_value NVARCHAR(MAX) NULL, bool_value BIT NULL, quality TINYINT NOT NULL, -- Changed to TINYINT to store values from 0-255 ts DATETIME2 NOT NULL );`; $.logger.debug("Create table query: %s", createTableQuery); try { await client.query(createTableQuery); $.logger.debug("Ensured that table TAG_EVENTS exists."); } catch (ex) { $.logger.warn("Error ensuring table exists: %s", ex.message); } } // Async function called every PERIOD milliseconds to insert data into the DB async function pushEvents() { // If the client is null or offline, connect the client if (!client || (client && !client.online())) { try { $.logger.debug("Connecting"); await connect(); $.logger.debug("ConnectOk"); $.logger.debug("Creating Table"); await ensureTableExists(); // Ensure the table exists after connecting $.logger.debug("Table Created"); } catch (ex) { // If the connection fails, schedule a new timer $.logger.warn("Error connecting: %s", ex.message); return setTimeout(pushEvents, PERIOD); } } // If the client is connected, remove the events from the buffer // and transform them into a format that can be used by SQL Server const ev = events.map((el) => { // Set number_ev, string_ev and bool_ev either to the event // or to null if the type does not match const number_ev = typeof el.value === "number" ? el.value : null; const string_ev = typeof el.value === "string" ? el.value : null; const bool_ev = typeof el.value === "boolean" ? el.value : null; // Return a 6-element array representing the values that will be inserted into the DB const values = [ `'${el.tag}'`, number_ev !== null ? number_ev : 'NULL', string_ev !== null ? `'${string_ev}'` : 'NULL', bool_ev !== null ? bool_ev : 'NULL', el.quality, `'${new Date(el.ts).toISOString()}'` ]; return `(${values.join(", ")})`; }); try { $.logger.debug("Inserting %d events", ev.length); // Execute an INSERT query const query = `INSERT INTO TAG_EVENTS (tag, number_value, string_value, bool_value, quality, ts) VALUES ${ev.join(",")}`; $.logger.debug("Query %s", query); await client.query(query); $.logger.debug("Inserted OK", ev.length); } catch (ex) { $.logger.warn("Error during query: %s", ex.message); // If the query fails and the client is offline, null the client if (!client.online()) { client = null; } } finally { // After finishing the query (whether it was successful or not) // schedule a new timer setTimeout(pushEvents, PERIOD); } } // Creates a new SQL Server client async function connect() { $.logger.debug( "Connecting to SQL Server using %s@%s:%d", OPTIONS.user, OPTIONS.server, OPTIONS.port ); client = await SqlServerConnection.Connect(OPTIONS); $.logger.debug("Connected OK"); } // Initializes the required local variables async function init() { $.local.init = true; // The client must be saved in a local variable to only have one instance // that persists between script executions $.local.client = null; } ``` The following screenshot shows the configuration of the Scripting task, where the script is triggered at startup or whenever the Scripting module is restarted. After initialization, the script performs data insertion into the database every 10 seconds. The execution period and tag subscriptions are hardcoded within the script, but these settings can be parameterized if needed using [Parameters](https://docs.n3uron.com/docs/scripting-api#parameters).![MS_SQL_Scripting](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/MS_SQL_Scripting.png) ## Writing data to XLSX files This example reads data from Historian, using the history API call, and stores it to a template XLSX file that can be used for further data processing. This example will use the following XLSX file: ![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/image-1664880280391.png) The Processed Data worksheet contains an average function over all the rows (skipping the first row, which are going to be used as the headers) of column B of _Data, while the _Data worksheet is empty, and it will be automatically filled by N3uron based on data from Historian. In order to have access to the XLSX writer library, an internal library instance must be created: ![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/image-1664880662916.png) Finally, the following script is used to retrieve events from Historian, and insert them as well as the headers: ```javascript // Load the XLSX library const Workbook = $.lib("xlsx"); const tag = "/Memory/Historical"; // Retrieve 1 hour of data const end = Date.now(); const start = end - 3600000; const history = await $.api.tag.history(tag, start, end, { mode: "raw" }); // Open the template workbook const workbookTemplatePath = String.raw`C:\Dev\Manual.xlsx`; $.logger.debug("Opening workbook..."); const workbook = await Workbook.Open(workbookTemplatePath); // Retrieve the _Data worksheet $.logger.debug("Workbook opened OK, retrieving worksheet..."); const sheet = await workbook.getSheet("_Data"); $.logger.debug("Worksheet retrieved OK, inserting data..."); // Write the headers in the first row sheet.writeData([["Timestamp", tag]], 0, 0); // Parse the data as necessary const cellData = []; for (const [ts, value] of history.data) { cellData.push([ts, value]); } // Write the data, setting the startRow to 1 in order to not overwrite the headers sheet.writeData(cellData, 1, 0); // Save the workbook to a different path to avoid overwriting the template const workbookOutPath = String.raw`C:\Dev\Manual_modified.xlsx`; $.logger.debug("Data inserted OK, saving workbook..."); await workbook.save(workbookOutPath); $.logger.debug("Workbook saved OK"); ``` ## Monitoring External Device Connectivity with Ping-Based Checks This monitoring example illustrates how to assess the connectivity of external devices—such as routers or other networked components that are not fieldbus devices—by implementing a ping-based connectivity check. By regularly pinging these devices, you can ensure they are reachable and operational within your network. - **Step 1:** **Create a Tag** and assign its **type** to **Boolean** and its **source** to the **Scripting module**.![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/Ping01.png) - **Step 2:** **Create a Task** within the instantiated Scripting module, set its **Action to Periodic**, and configure its execution period. The period can be defined either as a **time interval** in milliseconds or using a **Cron expression**, depending on your specific requirements. ![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/Ping02.png) - **Step 3:** **Create two parameters:** one of **type String** to specify the **IP address** of the device you want to monitor, and another of **type** **TagPath** to select the path of the tag, which will represent the connectivity status of that device. ![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/Ping03(1).png) - **Step 4**: **Copy and paste** the following code into the **Javascript code**:![](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/Ping04.png) **Sample code:**JavaScriptJavaScript ```javascript // Import the 'child_process' module to execute system commands like 'ping' const { exec } = require('child_process'); // Import the 'os' module to identify the operating system const os = require('os'); /** * Sends a single ping to the specified host to check connectivity. * * Constructs and executes a ping command based on the operating system. * Resolves with true if the host is reachable, otherwise false. * * host - The target IP address or hostname. * timeout - The duration to wait for a response in seconds. * returns Indicates if the host is reachable. */ function pingHost(host = '8.8.8.8', timeout = 5) { return new Promise((resolve) => { // Determine the current OS platform (e.g., 'win32', 'linux', 'darwin') const platform = os.platform(); // Build the appropriate ping command based on the OS let pingCommand = ''; if (platform === 'win32') { // For Windows: // '-n 1' sends one echo request // '-w ' sets the timeout in milliseconds pingCommand = `ping -n 1 -w ${timeout * 1000} ${host}`; } else { // For Unix-like systems: // '-c 1' sends one packet // '-W ' sets the timeout in seconds pingCommand = `ping -c 1 -W ${timeout} ${host}`; } // Execute the ping command exec(pingCommand, (error, stdout) => { if (error) { // If the ping command fails, log a warning and resolve as not connected $.logger.warn(`Ping command failed: ${error.message}`); resolve(false); return; } // Analyze the ping output to determine connectivity const isConnected = parsePingOutput(platform, stdout); resolve(isConnected); }); }); } /** * Analyzes the output of the ping command to determine connectivity. * * Checks for specific strings in the ping output that indicate a successful ping. * * platform - The operating system platform. * stdout - The output from the ping command. * returns True if the host is reachable, false otherwise. */ function parsePingOutput(platform, stdout) { if (platform === 'win32') { // On Windows, a successful ping includes 'TTL=' return stdout.includes('TTL='); } else { // On Unix-like systems, a successful ping includes '1 received' or 'bytes from' return ( stdout.includes('1 received') || stdout.includes('bytes from') ); } } /** * Updates the specified tag with the connectivity status. * * Writes true or false to the given tag path with appropriate quality values. * Logs the outcome of the write operation. * * tagPath - The tag identifier to update. * status - The connectivity status. */ async function writeConnectivityStatus(tagPath, status) { try { // Determine the quality value based on connectivity status const quality = status ? 192 : 24; // Update the tag with the status and current timestamp await $.api.tag.update(tagPath, status, quality, Date.now()); // Log the successful update $.logger.info(`Connectivity status (${status}) written to tag: ${tagPath}`); } catch (error) { // Log any errors that occur during the tag update $.logger.error(`Failed to write to tag ${tagPath}: ${error.message}`); throw error; } } /** * Executes the connectivity check and updates the corresponding tag. * * Retrieves the target IP address and tag path from parameters, * performs the ping, and writes the result to the tag. */ async function performConnectivityCheck() { try { // Get the target device's IP address and tag path from parameters const ip_address = $.parameters.ip_address; // Device IP address const tagPath = $.parameters.tagPath; // Tag path for connectivity status // Log the initiation of the connectivity check $.logger.info(`Pinging ${ip_address} to check connectivity...`); // Perform the ping to determine if the device is reachable const isConnected = await pingHost(ip_address, 5); // Update the tag with the connectivity status await writeConnectivityStatus(tagPath, isConnected); } catch (error) { // Log any unexpected errors during the connectivity check $.logger.error(`Connectivity check error: ${error.message}`); } } // Start the connectivity check process performConnectivityCheck(); ``` ## [](/v121/docs/scripting-examples#_Toc36806232)Building a Real-Time Dashboard with N3uron Scripting Assistant in ChatGPT This example was created using the **N3uron Scripting Assistant in ChatGPT**. It demonstrates how to build a **real-time web dashboard** for a solar power plant by leveraging the **N3uron Scripting module** together with the [Web Vision PV Demo Project](https://docs.n3uron.com/docs/web-vision-introduction) as its data source. This **dashboard** presents live **Key Performance Indicators (KPIs)** for both the **plant level** and each **station leve**l, updating automatically as tag values change in real time. It features a professional dark-themed interface with dynamic gauges, quality indicators, and a responsive design that provides operators with **up-to-date information** directly from any **web browser**. [![n3uron-chatgpt-assistant](https://cdn.document360.io/54093ab5-6b22-4542-a265-04377931f11a/Images/Documentation/n3uron-chatgpt-assistant.png)](https://chatgpt.com/g/g-68ebf91f3a9081918d8872b5253bc607-n3uron-scripting-assistant) [**N3uron Scripting Assistant**](https://chatgpt.com/g/g-68ebf91f3a9081918d8872b5253bc607-n3uron-scripting-assistant) [Embedded content](https://www.youtube.com/embed/4JN0kmbwECM?&wmode=opaque&rel=0) **Prompt Used (watch the video):** ```none I need to create a real-time web dashboard for our solar plant monitoring using N3uron's Scripting module. Here's a screenshot of our tag structure [attach image of tag tree]. Our setup: - Plant: BLUELAKE - Plant Group: PVG001 - 10 Stations: PST_01 through PST_10 I need two main sections: 1. GENERAL SECTION (plant-level KPIs): - Active Power Total (kW) - Active Power 5min Average (kW) - Active Power Hour Average (kW) - Active Energy Day (kWh) - Active Energy Total (kWh) - Reactive Power Total (VAr) - POA Irradiance (W/m²) - POA Irradiance 5min Average (W/m²) - Performance Ratio (0-100%, show as gauge) - Capacity Factor (0-100%, show as gauge) - Nominal Power (kW) 2. STATIONS SECTION (one section per station with these KPIs): - Active Power Total (kW) - Active Power 5min Average (kW) - Active Energy (kWh) - Reactive Power Total (VAr) - POA Irradiance (W/m²) - POA Irradiance 5min Average (W/m²) - Performance Ratio (0-100%, show as gauge) - Nominal Power (kW) Requirements: - Real-time updates when tag values change - Show quality status for each value - Navigation sidebar to jump between sections - Professional dark theme - Accessible via browser on port 8101 Can you provide the complete code with step-by-step setup instructions? ``` **Goal:** Create a self-contained dashboard hosted by the N3uron Scripting module that: - Reads real-time KPI tags from the WebVision PV Demo Project. - Displays all plant and station KPIs through a modern web interface. - Automatically updates when tag values change. - Shows tag quality status for every value. - Runs **locally** on port **8101**. | Scripts | Description | | --- | --- | | model | Defines data structures and helper functions for KPIs. | | ui | Creates the front-end interface (HTML, CSS, and JavaScript). | | Dashboard Task (Startup) | Subscribes to tag changes and serves live data via HTTP. | | Dashboard Task (Shutdown) | Cleans up all connections and subscriptions. | **Step 1 — Create Static Library: model** Handles KPI definition, path collection, and JSON payload assembly. Scripting module instance → Libraries → Add Static Library → Name: model → paste: ```javascript // lib: model - absolute paths only // Exports: buildSectionCards, collectPaths, makePayload function isNum(x){ return typeof x === "number" && isFinite(x); } function fmt(val, decimals){ if (!isNum(val)) return "-"; var d = typeof decimals==="number"?decimals:0; return Number(val).toFixed(d); } function buildSectionCards(getV, getQ, sectionDef){ var out = []; for (var i=0;i ${t}

Dashboard

${t}

/data/health
`; } module.exports = { page }; ``` **Step 3 — Create Task: Dashboard (Startup action)** Initializes the dashboard, subscribes to KPI tags using the Scripting API, starts an HTTP server on port 8101, and broadcasts real-time updates to clients via Server-Sent Events (SSE). Scripting → Model → New Task → Name: Dashboard Actions → New Action → Type: Startup → Enabled: Yes → paste: ```javascript // Dashboard Startup — HTTP + SSE + Tag subscription // Uses static libs 'model' and 'ui'. Absolute tag paths. var http = require("http"); var model = $.lib("model"); var ui = $.lib("ui"); // Listen on all interfaces if remote clients will connect: var HTTP_HOST = "0.0.0.0"; // "127.0.0.1" for local only var HTTP_PORT = 8101; // GENERAL (plant-level KPIs) — KPI_VG var GENERAL = [ { type:"number", label:"ACTIVE_POWER_TOTAL", path:"/PVSIM/BLUELAKE/PVG001/KPI_VG/ACTIVE_POWER_TOTAL", units:"kW", decimals:2 }, { type:"number", label:"ACTIVE_POWER_5MIN_AVG", path:"/PVSIM/BLUELAKE/PVG001/KPI_VG/ACTIVE_POWER_5MIN_AVG", units:"kW", decimals:2 }, { type:"number", label:"ACTIVE_POWER_HOUR_AVG", path:"/PVSIM/BLUELAKE/PVG001/KPI_VG/ACTIVE_POWER_HOUR_AVG", units:"kW", decimals:2 }, { type:"number", label:"ACTIVE_ENERGY_DAY", path:"/PVSIM/BLUELAKE/PVG001/KPI_VG/ACTIVE_ENERGY_DAY", units:"kWh", decimals:2 }, { type:"number", label:"ACTIVE_ENERGY_TOTAL", path:"/PVSIM/BLUELAKE/PVG001/KPI_VG/ACTIVE_ENERGY", units:"kWh", decimals:2 }, { type:"number", label:"REACTIVE_POWER_TOTAL", path:"/PVSIM/BLUELAKE/PVG001/KPI_VG/REACTIVE_POWER_TOTAL", units:"VAr", decimals:2 }, { type:"number", label:"POA_IRRADIANCE", path:"/PVSIM/BLUELAKE/PVG001/KPI_VG/POA_IRRADIANCE", units:"W/m²", decimals:2 }, { type:"number", label:"POA_IRRADIANCE_5MIN_AVG", path:"/PVSIM/BLUELAKE/PVG001/KPI_VG/POA_IRRADIANCE_5MIN_AVG", units:"W/m²", decimals:2 }, { type:"gauge", label:"PERFORMANCE_RATIO", path:"/PVSIM/BLUELAKE/PVG001/KPI_VG/PERFORMANCE_RATIO", units:"%", min:0, max:100, decimals:2 }, { type:"gauge", label:"CAPACITY_FACTOR", path:"/PVSIM/BLUELAKE/PVG001/KPI_VG/CAPACITY_FACTOR", units:"%", min:0, max:100, decimals:2 }, { type:"number", label:"NOMINAL_POWER", path:"/PVSIM/BLUELAKE/PVG001/KPI_VG/NOMINAL_POWER", units:"kW", decimals:0 } ]; // STATIONS — KPI_ST (PST_01 ... PST_10) function st(name){ var b="/PVSIM/BLUELAKE/PVG001/"+name+"/KPI_ST/"; return [ { type:"number", label:"ACTIVE_POWER_TOTAL", path:b+"ACTIVE_POWER_TOTAL", units:"kW", decimals:2 }, { type:"number", label:"ACTIVE_POWER_5MIN_AVG", path:b+"ACTIVE_POWER_5MIN_AVG", units:"kW", decimals:2 }, { type:"number", label:"ACTIVE_ENERGY", path:b+"ACTIVE_ENERGY", units:"kWh", decimals:2 }, { type:"number", label:"REACTIVE_POWER_TOTAL", path:b+"REACTIVE_POWER_TOTAL", units:"VAr", decimals:2 }, { type:"number", label:"POA_IRRADIANCE", path:b+"POA_IRRADIANCE", units:"W/m²", decimals:2 }, { type:"number", label:"POA_IRRADIANCE_5MIN_AVG", path:b+"POA_IRRADIANCE_5MIN_AVG", units:"W/m²", decimals:2 }, { type:"gauge", label:"PERFORMANCE_RATIO", path:b+"PERFORMANCE_RATIO", units:"%", min:0, max:100, decimals:2 }, { type:"number", label:"NOMINAL_POWER", path:b+"NOMINAL_POWER", units:"kW", decimals:2 } ]; } var STATIONS = [ { name:"PST_01", cards: st("PST_01") }, { name:"PST_02", cards: st("PST_02") }, { name:"PST_03", cards: st("PST_03") }, { name:"PST_04", cards: st("PST_04") }, { name:"PST_05", cards: st("PST_05") }, { name:"PST_06", cards: st("PST_06") }, { name:"PST_07", cards: st("PST_07") }, { name:"PST_08", cards: st("PST_08") }, { name:"PST_09", cards: st("PST_09") }, { name:"PST_10", cards: st("PST_10") } ]; // --- Internal state --- $.taskLocal.title = "BLUELAKE – PVG001"; $.taskLocal.clients = []; // SSE clients $.taskLocal.values = {}; // path -> latest numeric/string/boolean $.taskLocal.qual = {}; // path -> latest quality $.taskLocal.bump = null; // debounce timer id $.taskLocal.sub = null; // tag subscription handle function getV(p){ return $.taskLocal.values.hasOwnProperty(p) ? $.taskLocal.values[p] : null; } function getQ(p){ return $.taskLocal.qual.hasOwnProperty(p) ? $.taskLocal.qual[p] : 0; } function scheduleBroadcast(){ if ($.taskLocal.bump) return; $.taskLocal.bump = setTimeout(function(){ $.taskLocal.bump = null; var payload = model.makePayload($.taskLocal.title, GENERAL, STATIONS, getV, getQ); var data = "data: " + JSON.stringify(payload) + "\n\n"; for (var i=$.taskLocal.clients.length-1;i>=0;i--){ var res = $.taskLocal.clients[i]; try { res.write(data); } catch(_){} } }, 120); // batch rapid changes } function onTagEvent(tag, data, initial){ $.taskLocal.values[tag] = data.value; $.taskLocal.qual[tag] = data.quality; if (initial) return; // initial snapshot handled by /data scheduleBroadcast(); } // Subscribe to all referenced paths var ALL = model.collectPaths({ general: GENERAL, stations: STATIONS }); $.logger.info("Dashboard subscribing to %d tags", ALL.length); $.taskLocal.sub = await $.api.tag.subscribe(ALL, onTagEvent); // Warm-up read for initial display try{ for (var i=0;i= 0) $.taskLocal.clients.splice(idx,1); }); return; } // Not found res.writeHead(404, {"Content-Type":"text/plain; charset=utf-8"}); res.end("Not found"); }catch(ex){ $.logger.warn("HTTP handler error: %s", ex.message); try{ res.writeHead(500, {"Content-Type":"text/plain"}); res.end("Server error"); }catch(_){} } }); server.on("clientError", function(err, socket){ try{ socket.end("HTTP/1.1 400 Bad Request\r\n\r\n"); }catch(_){} }); server.listen(HTTP_PORT, HTTP_HOST, function(){ $.logger.info("Dashboard listening on http://%s:%d", HTTP_HOST, HTTP_PORT); }); $.taskLocal.server = server; ``` **Step 4 — Create Task: Dashboard (Shutdown action)** When the Scripting module shuts down, the Dashboard Shutdown action runs first to clean up resources. Actions → New Action → Type: Shutdown → Enabled: Yes → paste: ```javascript // Dashboard Shutdown — unsubscribe, close SSE clients, stop server try{ if ($.taskLocal && $.taskLocal.sub){ $.api.tag.unsubscribe($.taskLocal.sub); $.taskLocal.sub=null; } }catch(e){ $.logger.warn("unsubscribe error: %s", e.message); } try{ if ($.taskLocal && $.taskLocal.clients){ for (var i=$.taskLocal.clients.length-1;i>=0;i--){ var res=$.taskLocal.clients[i]; try{ if(res && !res.writableEnded) res.end(); }catch(_){ } } $.taskLocal.clients=[]; } }catch(_){} try{ if ($.taskLocal && $.taskLocal.server){ var s=$.taskLocal.server; $.taskLocal.server=null; s.close(); } }catch(e){ $.logger.warn("server close error: %s", e.message); } try{ if ($.taskLocal && $.taskLocal.bump){ clearTimeout($.taskLocal.bump); $.taskLocal.bump=null; } }catch(_){} $.logger.info("Dashboard shutdown complete."); ``` # Step 5 — Start it up 1. Click Save. 2. The Scripting module will restart. 3. Open the dashboard in a new browser tab. - Local server: http://localhost:8101 - Remote: http://:8101 (make sure HTTP_HOST is set to "0.0.0.0" to listen on all network interfaces and that the firewall allows port 8101).