After working with Ray, I threw together a repository with a script that allows either CLI or mango to fire data into mango 4 by stating the xid, user, pass/bearer, host, and port.
https://github.com/riagominota/degreedays-mangoclient
This repo however, can be rejigged to work with other third party api sources you like.
After that, Ray has kindly sent back his config here for future people to use as a guideline for their own work:
MS Windows environment NodeJS Mango Client Steps for DegreeDays.net NodeJS API:
On your PC where Mango is installed:
Install Node.js 18.12.1 (x64) which includes npm:
https://nodejs.org/dist/v18.12.1/node-v18.12.1-x64.msi
The Following are Optional for NodeJS programming development environment purposes:
Install Visual Studio Code (optional):
https://code.visualstudio.com/sha/download?build=stable&os=win32-x64-user
Install WSL Subsystem for Linux (optional):
https://learn.microsoft.com/en-us/windows/wsl/
From Windows PowerShell: wsl --install
Install WSL Visual Studio code Extension (optional):
https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl
- Create a new Empty Project folder, then from a command prompt CD into that folder,
(Important!) Run: "npm init" without quotes to initialize a new nodeJS package.json,
and then run the following npm installs from the command line in that folder:
npm install mango-client
npm install luxon
npm install form-data
npm install node-fetch@2
- In the same project folder, create a new file called "ddMango.js", and place the following code in the file, then save:
//Begin Code *********************************************************
/**
* Date: 2/1/23 - DD/MM/YYYY
* Author: Matt 'Fox' Fox
* Description: Logs into mango, and fires data into system for a given datapoint.
* Call the file by running `node ddMango.js --xid=... --user=... --pass=...` - ideal for evented calls
* Optionally, host and port can be added if not localhost or 8080 etc.
*/
const process = require('process');
const MangoClient = require('mango-client');
const { DateTime } = require("luxon");
const REQ_ARG_LIST = ["xid","user","pass"];
const ARG_LIST = ["bearer","host","port"]; // A bearer token can be used instead if desired
let ARG_VALS = {xid:"",user:"",pass:"",bearer:"",host:"localhost",port:8080};
// Args are provided in the form of --xid=... --user=... --pass=...
//This means we can call this from mango itself
const hasArgVal = key => {
// Return true if the key exists and a value is defined
for( let str of process.argv)
if ( str.includes( `--${ key }` ) ) return true;
return false;
}
const getArgVal = arg => {
let val = null;
if( hasArgVal(arg) ){
val = process.argv.find( element => element.startsWith( `--${arg}=` ) );
val = val.replace( `--${arg}=` , '' );
}
// Return null if the key does not exist and a value is not defined
return val;
}
// Ensure all args are properly populated before firing the script...
const VerifyArgs = () =>{
for(let k in ARG_LIST)
{
if( hasArgVal(ARG_LIST[k]) )
{
ARG_VALS[ ARG_LIST[k] ] = getArgVal(ARG_LIST[k]);
}
}
for(let k in REQ_ARG_LIST)
{
if( hasArgVal(REQ_ARG_LIST[k]) )
{
ARG_VALS[ REQ_ARG_LIST[k] ] = getArgVal(REQ_ARG_LIST[k]);
}
else
{
if(REQ_ARG_LIST[k]=="pass" && ARG_VALS.bearer!==""){continue;}
console.log(`ERROR: argument --${REQ_ARG_LIST[k]} has not been passed as an argument to this script. Exiting.`);
process.exit(1);
}
}
}
VerifyArgs();
// Prepare new Mango Client Instance to receive ARG_VALS
const MangoClientInstance = new MangoClient({...ARG_VALS});
if(ARG_VALS.bearer!=="")
MangoClientInstance.setBearerAuthentication(ARG_VALS.user,ARG_VALS.bearer);
else
MangoClientInstance.setBasicAuthentication(ARG_VALS.user,ARG_VALS.pass);
// Save point values Array of:
// {xid,value,dataType,timestamp,annotation}
// to Mango Client instance.
const insertValuesV4 = (xid,values) => {
return MangoClientInstance.restRequest({
path: `/rest/latest/point-values/${xid}`,
method: 'PUT',
data: values
}).then(response => {
return response.data;
});
}
// Get degree days data here... then let's send it to mango.
// You will need to parse the date into a timestamp using luxon
const crypto = require('crypto'), // built-in module, no need to npm install
fetch = require('node-fetch'); // install with npm install node-fetch@2
//Test DegreeDays API account info for Cape Cod MA, will need your own DegreeDays.net Account
const accountKey = 'test-test-test';
const securityKey = 'test-test-test-test-test-test-test-test-test-test-test-test-test';
const endpoint = 'http://apiv1.degreedays.net/json';
const location = {
type: 'PostalCodeLocation',
//Cape Cod MA Test API zip code
postalCode: '02532',
countryCode: 'US'
};
const breakdown = {
type: 'DailyBreakdown',
period: {
type: 'LatestValuesPeriod',
numberOfValues: 1 //This number (days) can be increased for a larger set of historic data.
}
};
const locationDataRequest = {
type: 'LocationDataRequest',
location: location,
dataSpecs: {
myHDD: {
type: 'DatedDataSpec',
calculation: {
type: 'HeatingDegreeDaysCalculation',
baseTemperature: {
unit: 'F',
value: 65
}
},
breakdown: breakdown
}
}
};
const fullRequest = {
securityInfo: {
endpoint: endpoint,
accountKey: accountKey,
timestamp: new Date().toISOString(),
random: Buffer.from(crypto.randomBytes(12)).toString('hex')
},
request: locationDataRequest
};
const fullRequestJson = JSON.stringify(fullRequest);
const signatureBytes =
crypto.createHmac('sha256', securityKey).update(fullRequestJson).digest();
function base64urlEncode(unencoded) {
return Buffer.from(unencoded).toString('base64')
.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
fetch(endpoint, {
method: 'POST',
body: 'request_encoding=base64url' +
'&signature_method=HmacSHA256' +
'&signature_encoding=base64url' +
'&encoded_request=' + base64urlEncode(fullRequestJson) +
'&encoded_signature=' + base64urlEncode(signatureBytes),
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
}
})
.then(response => {
if (!response.ok) {
throw new Error(response.status + ': ' + response.statusText);
}
return response.json();
})
.catch(err => console.error('Problem connecting to API:', err))
.then(parsedObject => handleResponse(parsedObject))
.catch(err => console.error('Problem handling API response:', err));
function handleResponse(fullResponse) {
const response = fullResponse.response;
if (response.type === 'Failure') {
console.log('Request failure:', response);
} else {
// console.log('Station ID: ', response.stationId);
const hddData = response.dataSets.myHDD;
if (hddData.type === 'Failure') {
console.log('Failure for HDD DataSet:', hddData);
} else {
// console.log('HDD:');
for (var v of hddData.values) {
// let degreeDaysValue = console.log(v.d + ': ' + v.v);
let degreeDaysValue = v.v;
let degreeDaysDate = DateTime.fromISO(v.d).toUnixInteger()*1000
// console.log(degreeDaysValue);
// console.log(degreeDaysDate);
// FIRE!! (Insert Values into Mango XID)
insertValuesV4(ARG_VALS.xid,
{
value:degreeDaysValue,
dataType:"NUMERIC",
timestamp:degreeDaysDate
}
);
}
}
}
}
// End Code ************************************************
- Now log into Mango as admin, and create a new user - being sure to set the role to match read and set point permissions of the datapoint you want to write to. In my case it was 'DegreeDaysUser'
Create a new Scripting Data Source ie. "DegreeDays.net API", and Paste the following script in the data source:
souce: https://forum.mango-os.com/topic/3818/invoke-shell-commands-from-a-scripting-environment
//Begin Code
com.serotonin.m2m2.rt.maint.work.ProcessWorkItem.queueProcess("C:/Program Files/nodejs/node C:/NodeJSApps/degreedaysAPI/ddMango.js --xid=DP_DegreeDaysHDD --user=dduser --pass=ddpassword --host=localhost --port=8080", 10);
//End Code
***PLEASE NOTE: the full paths to the node binary (C:/Program Files/nodejs/node.exe) as well as the full path to the ddMango.js script e.g. C:/NodeJSApps/degreedaysAPI/ddMango.js
Make sure these match your specific folder path environment. Also change the ddMango.js script --xid,--user,--pass,--host,--port
argument values to match your environment.
-
On this same Scripting data source, select "Polling" and then set the polling to use the desired schedule (In this case CRON Pattern: 0 30 0 ? * * *
was used)
The CRON pattern above represents running this script everyday at 12:30AM. Validate script, then save.
-
(Important!) Now re-open this same Scripting Data Source you just created, and then create a new DataPoint with the following XID:
DP_DegreeDaysHDD (or whatever suits your naming conventions...)
-
Change the new datapoint to Data Type "Numeric", and Logging Type: "All Data", also fill out the point name, and Variable name field with names of your choosing, then assign the "DegreeDaysUser" role to have Read, edit, set permissions, then save the point.
To Test to see if the script works:
- Click "Validate" for the script again on this scripting data source, you should see the newly added DP_DegreeDaysHDD datapoint update with values.
- Also check the next day after 12:30AM to see if CRON fired the script to update the DP_DegreeDaysHDD value as well.
Thanks for your contribution Ray!
-Fox