Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions backend/shared/model/south-connector.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const OIBUS_SOUTH_TYPES = [
'opcua',
'oracle',
'osisoft-pi',
'osisoft-pi-webapi',
'postgresql',
'sftp',
'sqlite'
Expand Down Expand Up @@ -118,3 +119,17 @@ export interface SouthCache {
itemId: string;
maxInstant: Instant;
}

export interface AvailablePoint {
id: string;
name: string;
description?: string;
// PI Web API specific properties
webId?: string;
pointClass?: string;
pointType?: string;
path?: string;
engineeringUnits?: string;
// Generic index signature for connector-specific properties
[key: string]: string | number | boolean | undefined;
}
24 changes: 24 additions & 0 deletions backend/shared/model/south-settings.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,7 @@ export type SouthSettings =
| SouthOPCUASettings
| SouthOracleSettings
| SouthPISettings
| SouthPIWebAPISettings
| SouthPostgreSQLSettings
| SouthSFTPSettings
| SouthSQLiteSettings;
Expand Down Expand Up @@ -830,6 +831,28 @@ export interface SouthPIItemSettings {
piQuery?: string;
}

export interface SouthPIWebAPISettingsThrottling {
maxReadInterval: number;
readDelay: number;
overlap: number;
maxInstantPerItem: boolean;
}

export interface SouthPIWebAPISettings {
throttling: SouthPIWebAPISettingsThrottling;
url: string;
dataServerWebId: string;
username: string;
password: string | null;
acceptUnauthorized: boolean;
timeout: number;
retryInterval: number;
}

export interface SouthPIWebAPIItemSettings {
pointWebId: string;
}

export interface SouthPostgreSQLItemSettings {
query: string;
dateTimeFields: Array<SouthPostgreSQLItemSettingsDateTimeFields> | null;
Expand Down Expand Up @@ -864,6 +887,7 @@ export type SouthItemSettings =
| SouthOPCUAItemSettings
| SouthOracleItemSettings
| SouthPIItemSettings
| SouthPIWebAPIItemSettings
| SouthPostgreSQLItemSettings
| SouthSFTPItemSettings
| SouthSQLiteItemSettings;
64 changes: 64 additions & 0 deletions backend/src/service/south.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import pino from 'pino';

// South imports
import {
AvailablePoint,
SouthConnectorCommandDTO,
SouthConnectorDTO,
SouthConnectorItemCommandDTO,
Expand All @@ -29,6 +30,7 @@ import sqliteManifest from '../south/south-sqlite/manifest';
import opcManifest from '../south/south-opc/manifest';
import oledbManifest from '../south/south-oledb/manifest';
import piManifest from '../south/south-pi/manifest';
import piWebAPIManifest from '../south/south-pi-webapi/manifest';
import sftpManifest from '../south/south-sftp/manifest';
import ConnectionService from './connection.service';
import { OIBusContent } from '../../shared/model/engine.model';
Expand Down Expand Up @@ -75,6 +77,8 @@ import {
SouthOracleSettings,
SouthPIItemSettings,
SouthPISettings,
SouthPIWebAPIItemSettings,
SouthPIWebAPISettings,
SouthPostgreSQLItemSettings,
SouthPostgreSQLSettings,
SouthSettings,
Expand All @@ -96,6 +100,7 @@ import SouthOPC from '../south/south-opc/south-opc';
import SouthOPCUA from '../south/south-opcua/south-opcua';
import SouthOracle from '../south/south-oracle/south-oracle';
import SouthPI from '../south/south-pi/south-pi';
import SouthPIWebAPI from '../south/south-pi-webapi/south-pi-webapi';
import SouthPostgreSQL from '../south/south-postgresql/south-postgresql';
import SouthSFTP from '../south/south-sftp/south-sftp';
import SouthSQLite from '../south/south-sqlite/south-sqlite';
Expand All @@ -121,6 +126,7 @@ export const southManifestList: Array<SouthConnectorManifest> = [
modbusManifest,
oianalyticsManifest,
piManifest,
piWebAPIManifest,
sftpManifest
];

Expand Down Expand Up @@ -295,6 +301,17 @@ export default class SouthService {
logger,
southBaseFolders
);
case 'osisoft-pi-webapi':
return new SouthPIWebAPI(
settings as SouthConnectorEntity<SouthPIWebAPISettings, SouthPIWebAPIItemSettings>,
addContent,
this.encryptionService,
this.southConnectorRepository,
this.southCacheRepository,
this.scanModeRepository,
logger,
southBaseFolders
);
case 'postgresql':
return new SouthPostgreSQL(
settings as SouthConnectorEntity<SouthPostgreSQLSettings, SouthPostgreSQLItemSettings>,
Expand Down Expand Up @@ -424,6 +441,53 @@ export default class SouthService {
return await south.testItem(testItemToRun, testingSettings, callback);
}

async browseItems<S extends SouthSettings, I extends SouthItemSettings>(
id: string,
command: SouthConnectorCommandDTO<S, I>,
logger: pino.Logger,
nameFilter?: string,
maxPoints?: number
): Promise<Array<AvailablePoint>> {
let southConnector: SouthConnectorEntity<S, I> | null = null;
if (id !== 'create') {
southConnector = this.southConnectorRepository.findSouthById(id);
if (!southConnector) {
throw new Error(`South connector ${id} not found`);
}
}
const manifest = this.getInstalledSouthManifests().find(southManifest => southManifest.id === command.type);
if (!manifest) {
throw new Error(`South manifest ${command.type} not found`);
}

const testConnectorToRun: SouthConnectorEntity<SouthSettings, SouthItemSettings> = {
id: southConnector?.id || 'test',
...command,
settings: await this.encryptionService.encryptConnectorSecrets<S>(
command.settings,
southConnector?.settings || null,
manifest.settings
),
name: southConnector ? southConnector.name : `${command!.type}:browse-items`,
items: []
};

/* istanbul ignore next */
const mockedAddContent = async (_southId: string, _content: OIBusContent): Promise<void> => Promise.resolve();
const south = this.runSouth(testConnectorToRun, mockedAddContent, logger, {
cache: 'baseCacheFolder',
archive: 'baseArchiveFolder',
error: 'baseErrorFolder'
});

// Check if the connector has getAvailablePoints method
if ('getAvailablePoints' in south && typeof south.getAvailablePoints === 'function') {
return await south.getAvailablePoints(nameFilter, maxPoints);
} else {
throw new Error(`Connector type ${command.type} does not support browsing available items`);
}
}

findById<S extends SouthSettings, I extends SouthItemSettings>(southId: string): SouthConnectorEntity<S, I> | null {
return this.southConnectorRepository.findSouthById(southId);
}
Expand Down
136 changes: 136 additions & 0 deletions backend/src/south/south-pi-webapi/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { SouthConnectorManifest } from '../../../shared/model/south-connector.model';

const manifest: SouthConnectorManifest = {
id: 'osisoft-pi-webapi',
category: 'api',
modes: {
subscription: false,
lastPoint: false,
lastFile: false,
history: true
},
settings: [
{
key: 'throttling',
type: 'OibFormGroup',
translationKey: 'south.osisoft-pi-webapi.throttling.title',
class: 'col',
newRow: true,
displayInViewMode: false,
validators: [{ key: 'required' }],
content: [
{
key: 'maxReadInterval',
type: 'OibNumber',
translationKey: 'south.osisoft-pi-webapi.throttling.max-read-interval',
validators: [{ key: 'required' }, { key: 'min', params: { min: 0 } }],
defaultValue: 3600,
unitLabel: 's',
displayInViewMode: true
},
{
key: 'readDelay',
type: 'OibNumber',
translationKey: 'south.osisoft-pi-webapi.throttling.read-delay',
validators: [{ key: 'required' }, { key: 'min', params: { min: 0 } }],
defaultValue: 200,
unitLabel: 'ms',
displayInViewMode: true
},
{
key: 'overlap',
type: 'OibNumber',
translationKey: 'south.osisoft-pi-webapi.throttling.overlap',
validators: [{ key: 'required' }, { key: 'min', params: { min: 0 } }],
defaultValue: 0,
unitLabel: 'ms',
displayInViewMode: true
},
{
key: 'maxInstantPerItem',
type: 'OibCheckbox',
translationKey: 'south.osisoft-pi-webapi.throttling.max-instant-per-item',
defaultValue: false,
validators: [{ key: 'required' }],
displayInViewMode: true
}
]
},
{
key: 'url',
type: 'OibText',
translationKey: 'south.osisoft-pi-webapi.url',
defaultValue: 'https://pi.dev.metroscope.io/piwebapi/',
validators: [{ key: 'required' }],
newRow: true,
displayInViewMode: true
},
{
key: 'dataServerWebId',
type: 'OibText',
translationKey: 'south.osisoft-pi-webapi.data-server-web-id',
defaultValue: 'F1DSC4n4q_2uRUWxuLuRsKCl8QVk0tUEktU0VSVkVSLVRF',
validators: [{ key: 'required' }],
newRow: true,
displayInViewMode: true
},
{
key: 'username',
type: 'OibText',
translationKey: 'south.osisoft-pi-webapi.username',
validators: [{ key: 'required' }],
newRow: true,
displayInViewMode: true
},
{
key: 'password',
type: 'OibSecret',
translationKey: 'south.osisoft-pi-webapi.password',
class: 'col-4',
displayInViewMode: false
},
{
key: 'acceptUnauthorized',
type: 'OibCheckbox',
translationKey: 'south.osisoft-pi-webapi.accept-unauthorized',
defaultValue: false,
validators: [{ key: 'required' }],
class: 'col-4',
displayInViewMode: true
},
{
key: 'timeout',
type: 'OibNumber',
translationKey: 'south.osisoft-pi-webapi.timeout',
defaultValue: 30,
unitLabel: 's',
class: 'col-3',
validators: [{ key: 'required' }, { key: 'min', params: { min: 1 } }, { key: 'max', params: { max: 300 } }],
displayInViewMode: true
},
{
key: 'retryInterval',
type: 'OibNumber',
translationKey: 'south.osisoft-pi-webapi.retry-interval',
defaultValue: 10000,
unitLabel: 'ms',
class: 'col-3',
validators: [{ key: 'required' }, { key: 'min', params: { min: 100 } }, { key: 'max', params: { max: 30000 } }],
displayInViewMode: true
}
],
items: {
scanMode: 'POLL',
settings: [
{
key: 'pointWebId',
type: 'OibText',
translationKey: 'south.osisoft-pi-webapi.point-web-id',
validators: [{ key: 'required' }],
displayInViewMode: false,
class: 'col-12 d-none' // Hidden field - set programmatically
}
]
}
};
export default manifest;
Loading