This History Merge program object can take two histories in a Niagara Station and combine them.
Important: This is an educational article and program object. It is not to be used as-is with history data or stations you cannot afford to lose.
Consider using the Niagara SeriesTransform functionality if your history data is important.
What is the N4HistoryMerge program object?
N4HistoryMerge is a program object. It’s purpose is to solve issues where histories have been moved or reconfigured in a station, which has resulted in two separate history records – for example, one ‘old’ history before the change of configuration and one ‘new’ history which the user would like to contain both old and new records.
The program object can be copied onto a station and can then be used to combine two histories on the station into one. The source and destination histories will be opened, the records placed in order, and then the destination will be overwritten with the whole series of records. The original records for the destination history will be copied into a new [historyName]_cfg0 history in case of issues with the merge.
Will it solve all my Niagara history problems?
No! It will only solve a problem where you have two histories from the same source and you would prefer one. If you would like to combine histories for the purposes of comparing them or making simple changes, I would strongly advise using SeriesTransform, which has better functions for this task, and does not affect or alter the histories themselves.
N4HistoryMerge is a simple program object, which means the code is visible to allow it to be better understood and modified to suit your needs. It is provided to allow you to better understand using program objects and working with histories. This is not a ‘released’ product, and no station or history integrity guarantee can be given to you if you are using it.
Important: Always make sure you have full backups (including the history .hdb files) before you use any program object!
Okay, so I understand it’s for educational purposes. How do I use it?
Copying the N4HistoryMerge program object to a station
A .bog file with N4HistoryMerge can be found here:
Copy this somewhere in your Niagara directory - You will need to be able to find it with your WorkBench.
Find and open the .bog file. If unfamiliar, a .bog file is just a Niagara container for components. It’ll open as a WireSheet:
Copy the Program object and paste it anywhere in your station. I’ve pasted it at the root, but you can create a folder if you like:
Right-click on the program object and go to views->Program Editor
Here you can read the code for the program.
Using the program
To use the program as is, go to the property sheet view. Use the drop-down next to the folder icon and choose History Ord Chooser (otherwise it won’t guess you want a history)
For the Source History, choose the source (or ‘old’ history)
For the Destination History, choose the destination (the ‘new’ history). This history will contain all the records from both, and the original history will be moved to [historyName]_cfg0.
You can enable debug to print the output from the program to the Application Director. If you want to see what the program has done, switch this on and look in the application director after issuing the merging command.
Important! The next step will merge the histories. Do not continue unless you are sure of what you are doing!
To merge the histories, right-click on the MergeHistories program object and choose Actions->Merge. There will be no warning! It will just merge the histories at this point!
If there was no problem, the histories should now be merged in the history matching the name of the ‘Destination History’.
The sources will still be present – the ‘old’ history will still be in the database as usual, and the ‘new’ history will have been copied to [historyName]_cfg0. If these aren’t needed, you can remove them as usual in the History->Database Maintenance view.
N4HistoryMerge Source:
/*
* Things that still need to be done :
* 1.) X Check to make sure both history types are the same (numeric, boolean, etc)
* 2.) Don't allow duplicate records for same time stamp (option to allow? is there a use case?)
* 3.) X Put on seperate thread in case of large histories.
*
*/
public void onStart() throws Exception
{
// start up code here
}
public void onExecute() throws Exception
{
// execute code (set executeOnChange flag on inputs)
}
/*
* This action will merge the data from two histories of the same type into
* one history.
*/
public void onMerge() throws Exception
{
Thread thread = new Thread(this, "HISTORYTEST");
thread.start();
}
private BHistoryRecord[] combineHistories(BHistory src, BHistory dest)
{
//th
int srcCount = hSC.getRecordCount(src);
int destCount = hSC.getRecordCount(dest);
int total = srcCount + destCount;
debug("srcCount = " + srcCount + ", destCount = " + destCount + ", total = " + total);
BHistoryRecord[] compositeList = new BHistoryRecord[total];
int totalRecordCount = 0;
// get cursor for each history to get all history records
Cursor srcCursor = hSC.scan(src); // get all records in the src history
Cursor destCursor = hSC.scan(dest); // get all records in the dest history
// iterate through both lists, comparing the first records from each list and appending the
// earliest record to a composite list
BHistoryRecord srcRecord, destRecord;
srcRecord = nextRecord(srcCursor);
destRecord = nextRecord(destCursor);
int count = 0;
while( !(srcRecord == null) || !(destRecord == null)) // as long as we still have a history
{
if((srcRecord == null) && (destRecord == null))
debug("Both src and dest cursors equal null");
debug("Iteration number " + count);
count++;
// if(destRecord.equals(null)) // if destCursor is out of records
if(destRecord == null) // if destCursor is out of records
{
debug("destCursor returned null");
debug("Adding record from " + srcRecord.getTimestamp() + " from srcCursor");
// compositeList[totalRecordCount] = srcRecord;
BHistoryRecord recordCopy = (BHistoryRecord)srcRecord.newCopy();
compositeList[totalRecordCount] = recordCopy;
totalRecordCount++;
srcRecord = nextRecord(srcCursor);
}
if(srcRecord == null) // if the srcCursor is out of records
{
debug("srcCursor returned null");
// compositeList[totalRecordCount] = destRecord;
BHistoryRecord recordCopy = (BHistoryRecord)destRecord.newCopy();
compositeList[totalRecordCount] = recordCopy;
totalRecordCount++;
destRecord = nextRecord(destCursor);
continue;
}
if(srcRecord.getTimestamp().getMillis() <= destRecord.getTimestamp().getMillis())
{
debug("Adding history from " + srcRecord.getTimestamp() + " to composite list from srcCursor");
// compositeList[totalRecordCount] = srcRecord;
BHistoryRecord recordCopy = (BHistoryRecord)srcRecord.newCopy();
compositeList[totalRecordCount] = recordCopy;
totalRecordCount++;
srcRecord = nextRecord(srcCursor);
}
else
{
debug("Adding history from " + destRecord.getTimestamp() + " to composite list from destCursor");
// compositeList[totalRecordCount] = destRecord;
BHistoryRecord recordCopy = (BHistoryRecord)destRecord.newCopy();
compositeList[totalRecordCount] = recordCopy;
totalRecordCount++;
destRecord = nextRecord(destCursor);
}
}
// Set the contents of the composite to the destination history
return compositeList;
}
private void fillNewHistory(BHistoryRecord[] records)
{
BHistoryConfig config = new BHistoryConfig( BHistoryId.make("NewHistory", "merged"),
BTypeSpec.make("history:NumericTrendRecord"),
BCapacity.makeByRecordCount(records.length));
if(!hSC.exists(config.getId()))
hSC.createHistory(config);
BHistory history = (BHistory)hSC.getHistory(config.getId());
hSC.clearAllRecords(config.getId());
for(int i = 0; i < records.length; i++)
{
debug("Records ["+ i + "] = " + records[i]);
hSC.append(history, records[i]);
}
}
// Backup the existing destination history, recreate it, and then with all of the
// combined records.
private void recreateDestHistory(BHistoryRecord[] records)
{
hSC.recreateHistory(destHistory.getConfig(), true);
for(int i = 0; i < records.length; i++)
{
debug("Records ["+ i + "] = " + records[i]);
hSC.append(destHistory, records[i]);
}
}
// Return the next record from the cursor. Return null if no records left.
private BHistoryRecord nextRecord(Cursor cursor)
{
BHistoryRecord nextRecord = null;
if(cursor.next())
nextRecord = (BHistoryRecord)cursor.get();
return nextRecord;
}
public void onStop() throws Exception
{
// shutdown code here
}
public void run()
{
setStatus(BStatus.ok);
setFaultCause("");
// get both histories from the history database
//db = ((BHistoryService)Sys.getService(BHistoryService.TYPE)).getDatabase();
try
{
srcHistory = (BHistory)(getSrcHistory().get());
}
catch(Exception e)
{
debug("Error resolving srcHistory.");
e.printStackTrace();
setStatus(BStatus.fault);
setFaultCause("Could not resolve ord for Source History to a valid history.");
return;
}
try
{
destHistory = (BHistory)(getDestHistory().get());
}
catch(Exception e)
{
debug("Error resolving destHistory.");
e.printStackTrace();
setStatus(BStatus.fault);
setFaultCause("Could not resolve ord for Destination History to a valid history.");
return;
}
if(!srcHistory.getRecordType().equals(destHistory.getRecordType()))
{
setStatus(BStatus.fault);
setFaultCause("History record types do not match");
return;
}
// iterate through both histories, comparing timestamps and adding them to a centralized
// array in chronological order
BHistoryRecord[] comboList = combineHistories(srcHistory, destHistory);
// append all records from the centralized array to the new history.
//fillNewHistory(comboList);
// backup and then recreate the destHistory with the combined contents.
recreateDestHistory(comboList);
}
private void debug(String msg)
{
if(getDebug())
System.out.println("HistoryMerge Debug: " + msg);
}
BHistory srcHistory, destHistory;
//db is older AX mechanism, replaced with hSC
//BHistoryDatabase db;
BHistoryService service = (BHistoryService)Sys.getService(BHistoryService.TYPE);
HistoryDatabaseConnection hSC = service.getDatabase().getDbConnection(null);