diff --git a/backend/src/repository/south-connector.repository.ts b/backend/src/repository/south-connector.repository.ts index addee5b6dd..200942ab2c 100644 --- a/backend/src/repository/south-connector.repository.ts +++ b/backend/src/repository/south-connector.repository.ts @@ -34,24 +34,7 @@ export default class SouthConnectorRepository { overlap: result.overlap }, settings: JSON.parse(result.settings) - })) - .filter(result => - [ - 'ads', - 'folder-scanner', - 'modbus', - 'mqtt', - 'mssql', - 'mysql', - 'odbc', - 'oianalytics', - 'opcua', - 'oracle', - 'postgresql', - 'slims', - 'sqlite' - ].includes(result.type) - ); + })); } /** diff --git a/backend/src/service/south.service.ts b/backend/src/service/south.service.ts index 5c57185069..613ba5f836 100644 --- a/backend/src/service/south.service.ts +++ b/backend/src/service/south.service.ts @@ -5,7 +5,6 @@ import RepositoryService from './repository.service'; // South imports import SouthFolderScanner from '../south/south-folder-scanner/south-folder-scanner'; import SouthOPCUA from '../south/south-opcua/south-opcua'; -// import SouthOPCHDA from '../south/south-opchda/south-opchda'; import SouthMQTT from '../south/south-mqtt/south-mqtt'; import SouthMSSQL from '../south/south-mssql/south-mssql'; import SouthMySQL from '../south/south-mysql/south-mysql'; @@ -16,14 +15,15 @@ import SouthSQLite from '../south/south-sqlite/south-sqlite'; import SouthADS from '../south/south-ads/south-ads'; import SouthModbus from '../south/south-modbus/south-modbus'; import SouthOIAnalytics from '../south/south-oianalytics/south-oianalytics'; +import SouthSlims from '../south/south-slims/south-slims'; +import SouthOPCHDA from '../south/south-opchda/south-opchda'; import { SouthConnectorDTO, SouthConnectorItemDTO, SouthConnectorManifest } from '../../../shared/model/south-connector.model'; import SouthConnector from '../south/south-connector'; -import SouthSlims from '../south/south-slims/south-slims'; + import oianalyticsManifest from '../south/south-oianalytics/manifest'; import slimsManifest from '../south/south-slims/manifest'; import opcuaManifest from '../south/south-opcua/manifest'; -// import opchdaManifest from '../south/south-opchda/manifest'; import mqttManifest from '../south/south-mqtt/manifest'; import modbusManifest from '../south/south-modbus/manifest'; import folderScannerManifest from '../south/south-folder-scanner/manifest'; @@ -34,12 +34,13 @@ import postgresqlManifest from '../south/south-postgresql/manifest'; import oracleManifest from '../south/south-oracle/manifest'; import odbcManifest from '../south/south-odbc/manifest'; import sqliteManifest from '../south/south-sqlite/manifest'; +import opchdaManifest from '../south/south-opchda/manifest'; const southList: Array<{ class: typeof SouthConnector; manifest: SouthConnectorManifest }> = [ { class: SouthFolderScanner, manifest: folderScannerManifest }, { class: SouthMQTT, manifest: mqttManifest }, { class: SouthOPCUA, manifest: opcuaManifest }, - // { class: SouthOPCHDA, manifest: opchdaManifest }, + { class: SouthOPCHDA, manifest: opchdaManifest }, { class: SouthMSSQL, manifest: mssqlManifest }, { class: SouthMySQL, manifest: mysqlManifest }, { class: SouthODBC, manifest: odbcManifest }, diff --git a/backend/src/south/south-opchda/manifest.ts b/backend/src/south/south-opchda/manifest.ts index 61c89599b1..ead7ad03bb 100644 --- a/backend/src/south/south-opchda/manifest.ts +++ b/backend/src/south/south-opchda/manifest.ts @@ -18,7 +18,8 @@ const manifest: SouthConnectorManifest = { type: 'OibText', label: 'Remote agent URL', defaultValue: 'http://ip-adress-or-host:2224', - validators: [{ key: 'required' }] + validators: [{ key: 'required' }], + displayInViewMode: true }, { key: 'retryInterval', @@ -30,20 +31,31 @@ const manifest: SouthConnectorManifest = { validators: [{ key: 'required' }, { key: 'min', params: { min: 100 } }, { key: 'max', params: { max: 30_000 } }] }, { - key: 'serverUrl', + key: 'host', type: 'OibText', label: 'Server URL (from the agent)', - defaultValue: 'opchda://domain.name/Matrikon.OPC.Simulation', + defaultValue: 'localhost', + class: 'col-4', newRow: true, validators: [{ key: 'required' }], - displayInViewMode: false + displayInViewMode: true + }, + { + key: 'serverName', + type: 'OibText', + label: 'Server name', + defaultValue: 'Matrikon.OPC.Simulation', + class: 'col-4', + newRow: false, + validators: [{ key: 'required' }], + displayInViewMode: true }, { key: 'readTimeout', type: 'OibNumber', label: 'Read timeout (s)', defaultValue: 180, - class: 'col-3', + class: 'col-2', newRow: false, validators: [{ key: 'required' }, { key: 'min', params: { min: 100 } }, { key: 'max', params: { max: 3_600_000 } }], displayInViewMode: false @@ -53,7 +65,7 @@ const manifest: SouthConnectorManifest = { type: 'OibNumber', label: 'Max return values', defaultValue: 1000, - class: 'col-3', + class: 'col-2', newRow: false, validators: [{ key: 'required' }], displayInViewMode: false @@ -76,8 +88,9 @@ const manifest: SouthConnectorManifest = { key: 'aggregate', type: 'OibSelect', label: 'Aggregate', - options: ['Raw', 'Average', 'Minimum', 'Maximum', 'Count'], - defaultValue: 'Raw', + pipe: 'aggregates', + options: ['raw', 'average', 'minimum', 'maximum'], + defaultValue: 'raw', validators: [{ key: 'required' }], displayInViewMode: true }, @@ -85,10 +98,12 @@ const manifest: SouthConnectorManifest = { key: 'resampling', type: 'OibSelect', label: 'Resampling', - options: ['None', 'Second', '10 Seconds', '30 Seconds', 'Minute', 'Hour', 'Day'], - defaultValue: 'None', - displayInViewMode: true, - validators: [{ key: 'required' }] + pipe: 'resampling', + options: ['none', 'second', '10Seconds', '30Seconds', 'minute', 'hour', 'day'], + defaultValue: 'none', + validators: [{ key: 'required' }], + conditionalDisplay: { field: 'aggregate', values: ['average', 'minimum', 'maximum', 'count'] }, + displayInViewMode: true } ] } diff --git a/backend/src/south/south-opchda/south-opchda.spec.ts b/backend/src/south/south-opchda/south-opchda.spec.ts index 219cefff92..0693fa5f89 100644 --- a/backend/src/south/south-opchda/south-opchda.spec.ts +++ b/backend/src/south/south-opchda/south-opchda.spec.ts @@ -60,8 +60,8 @@ const items: Array> = [ connectorId: 'southId', settings: { nodeId: 'ns=3;s=Random', - aggregate: 'Raw', - resampling: 'None' + aggregate: 'raw', + resampling: 'none' }, scanModeId: 'scanModeId1' }, @@ -72,8 +72,7 @@ const items: Array> = [ connectorId: 'southId', settings: { nodeId: 'ns=3;s=Counter', - aggregate: 'Raw', - resampling: 'None' + aggregate: 'raw' }, scanModeId: 'scanModeId1' }, @@ -84,8 +83,8 @@ const items: Array> = [ connectorId: 'southId', settings: { nodeId: 'ns=3;s=Triangle', - aggregate: 'Raw', - resampling: 'None' + aggregate: 'average', + resampling: '10Seconds' }, scanModeId: 'scanModeId2' } @@ -108,7 +107,8 @@ const configuration: SouthConnectorDTO = { retryInterval: 1000, maxReturnValues: 0, readTimeout: 60, - serverUrl: 'opchda://localhost/Matrikon.OPC.Simulation' + host: 'localhost', + serverName: 'Matrikon.OPC.Simulation' } }; let south: SouthOPCHDA; @@ -126,7 +126,8 @@ describe('South OPCHDA', () => { expect(fetch).toHaveBeenCalledWith(`${configuration.settings.agentUrl}/api/opc/${configuration.id}/connect`, { method: 'PUT', body: JSON.stringify({ - url: configuration.settings.serverUrl + host: configuration.settings.host, + serverName: configuration.settings.serverName }), headers: { 'Content-Type': 'application/json' @@ -148,7 +149,8 @@ describe('South OPCHDA', () => { expect(fetch).toHaveBeenCalledWith(`${configuration.settings.agentUrl}/api/opc/${configuration.id}/connect`, { method: 'PUT', body: JSON.stringify({ - url: configuration.settings.serverUrl + host: configuration.settings.host, + serverName: configuration.settings.serverName }), headers: { 'Content-Type': 'application/json' @@ -197,7 +199,7 @@ describe('South OPCHDA', () => { await south.testConnection(); expect(logger.info).toHaveBeenCalledWith('Connected to remote OPC server. Disconnecting...'); expect(logger.info).toHaveBeenCalledWith( - `Testing OPC OIBus Agent connection on ${configuration.settings.agentUrl} with "${configuration.settings.serverUrl}"` + `Testing OPC OIBus Agent connection on ${configuration.settings.agentUrl} with host "${configuration.settings.host}" and server name "${configuration.settings.serverName}"` ); }); @@ -258,9 +260,32 @@ describe('South OPCHDA', () => { expect(fetch).toHaveBeenCalledWith(`${configuration.settings.agentUrl}/api/opc/${configuration.id}/read`, { method: 'PUT', body: JSON.stringify({ + host: configuration.settings.host, + serverName: configuration.settings.serverName, + aggregate: 'raw', + resampling: 'none', startTime, endTime, - items + items: [ + { name: 'item1', nodeId: 'ns=3;s=Random' }, + { name: 'item2', nodeId: 'ns=3;s=Counter' } + ] + }), + headers: { + 'Content-Type': 'application/json' + } + }); + + expect(fetch).toHaveBeenCalledWith(`${configuration.settings.agentUrl}/api/opc/${configuration.id}/read`, { + method: 'PUT', + body: JSON.stringify({ + host: configuration.settings.host, + serverName: configuration.settings.serverName, + aggregate: 'average', + resampling: '10Seconds', + startTime, + endTime, + items: [{ name: 'item3', nodeId: 'ns=3;s=Triangle' }] }), headers: { 'Content-Type': 'application/json' @@ -270,7 +295,6 @@ describe('South OPCHDA', () => { expect(result).toEqual('2020-03-01T00:00:00.000Z'); expect(south.addValues).toHaveBeenCalledWith([{ timestamp: '2020-02-01T00:00:00.000Z' }, { timestamp: '2020-03-01T00:00:00.000Z' }]); - await south.historyQuery(items, startTime, endTime); expect(logger.debug).toHaveBeenCalledWith(`No result found. Request done in 0 ms`); }); @@ -293,8 +317,6 @@ describe('South OPCHDA', () => { await south.historyQuery(items, startTime, endTime); expect(logger.error).toHaveBeenCalledWith(`Error occurred when querying remote agent with status 400: bad request`); - - await south.historyQuery(items, startTime, endTime); expect(logger.error).toHaveBeenCalledWith(`Error occurred when querying remote agent with status 500`); }); }); diff --git a/backend/src/south/south-opchda/south-opchda.ts b/backend/src/south/south-opchda/south-opchda.ts index 35264d83c1..52f2c19b87 100644 --- a/backend/src/south/south-opchda/south-opchda.ts +++ b/backend/src/south/south-opchda/south-opchda.ts @@ -4,7 +4,7 @@ import { SouthConnectorDTO, SouthConnectorItemDTO } from '../../../../shared/mod import EncryptionService from '../../service/encryption.service'; import RepositoryService from '../../service/repository.service'; import pino from 'pino'; -import { Instant } from '../../../../shared/model/types'; +import { Aggregate, Instant, Resampling } from '../../../../shared/model/types'; import { DateTime } from 'luxon'; import { QueriesHistory } from '../south-interface'; import { SouthOPCHDAItemSettings, SouthOPCHDASettings } from '../../../../shared/model/south-settings.model'; @@ -42,7 +42,8 @@ export default class SouthOPCHDA extends SouthConnector implements QueriesHistor const fetchOptions = { method: 'PUT', body: JSON.stringify({ - url: this.connector.settings.serverUrl + host: this.connector.settings.host, + serverName: this.connector.settings.serverName }), headers }; @@ -60,7 +61,7 @@ export default class SouthOPCHDA extends SouthConnector implements QueriesHistor async testConnection(): Promise { this.logger.info( - `Testing OPC OIBus Agent connection on ${this.connector.settings.agentUrl} with "${this.connector.settings.serverUrl}"` + `Testing OPC OIBus Agent connection on ${this.connector.settings.agentUrl} with host "${this.connector.settings.host}" and server name "${this.connector.settings.serverName}"` ); const headers: Record = {}; @@ -68,7 +69,8 @@ export default class SouthOPCHDA extends SouthConnector implements QueriesHistor const fetchOptions = { method: 'PUT', body: JSON.stringify({ - url: this.connector.settings.serverUrl + host: this.connector.settings.host, + serverName: this.connector.settings.serverName }), headers }; @@ -92,45 +94,74 @@ export default class SouthOPCHDA extends SouthConnector implements QueriesHistor */ async historyQuery(items: Array>, startTime: Instant, endTime: Instant): Promise { let updatedStartTime = startTime; - const startRequest = DateTime.now().toMillis(); + const itemsByAggregates = new Map>>(); + items.forEach(item => { + if (!itemsByAggregates.has(item.settings.aggregate)) { + itemsByAggregates.set( + item.settings.aggregate, + new Map< + Resampling, + Array<{ + nodeId: string; + name: string; + }> + >() + ); + } + const resampling = item.settings.resampling ? item.settings.resampling : 'none'; + if (!itemsByAggregates.get(item.settings.aggregate!)!.has(resampling)) { + itemsByAggregates.get(item.settings.aggregate)!.set(resampling, [{ name: item.name, nodeId: item.settings.nodeId }]); + } else { + const currentList = itemsByAggregates.get(item.settings.aggregate)!.get(resampling)!; + currentList.push({ name: item.name, nodeId: item.settings.nodeId }); + itemsByAggregates.get(item.settings.aggregate)!.set(resampling, currentList); + } + }); - const headers: Record = {}; - headers['Content-Type'] = 'application/json'; + for (const [aggregate, aggregatedItems] of itemsByAggregates.entries()) { + for (const [resampling, resampledItems] of aggregatedItems.entries()) { + this.logger.debug(`Requesting ${resampledItems.length} items with aggregate ${aggregate} and resampling ${resampling}`); + const startRequest = DateTime.now().toMillis(); + const headers: Record = {}; + headers['Content-Type'] = 'application/json'; + const fetchOptions = { + method: 'PUT', + body: JSON.stringify({ + host: this.connector.settings.host, + serverName: this.connector.settings.serverName, + aggregate, + resampling, + startTime, + endTime, + items: resampledItems + }), + headers + }; + const response = await fetch(`${this.connector.settings.agentUrl}/api/opc/${this.connector.id}/read`, fetchOptions); + if (response.status === 200) { + const result: { recordCount: number; content: Array; maxInstantRetrieved: Instant } = (await response.json()) as { + recordCount: number; + content: OIBusDataValue[]; + maxInstantRetrieved: string; + }; + const requestDuration = DateTime.now().toMillis() - startRequest; - const fetchOptions = { - method: 'PUT', - body: JSON.stringify({ - url: this.connector.settings.url, - startTime, - endTime, - items - }), - headers - }; - const response = await fetch(`${this.connector.settings.agentUrl}/api/opc/${this.connector.id}/read`, fetchOptions); - if (response.status === 200) { - const result: { recordCount: number; content: Array; maxInstantRetrieved: Instant } = (await response.json()) as { - recordCount: number; - content: OIBusDataValue[]; - maxInstantRetrieved: string; - }; - const requestDuration = DateTime.now().toMillis() - startRequest; - - if (result.content.length > 0) { - await this.addValues(result.content); - if (result.maxInstantRetrieved > updatedStartTime) { - updatedStartTime = result.maxInstantRetrieved; + if (result.content.length > 0) { + await this.addValues(result.content); + if (result.maxInstantRetrieved > updatedStartTime) { + updatedStartTime = result.maxInstantRetrieved; + } + } else { + this.logger.debug(`No result found. Request done in ${requestDuration} ms`); + } + } else if (response.status === 400) { + const errorMessage = await response.text(); + this.logger.error(`Error occurred when querying remote agent with status ${response.status}: ${errorMessage}`); + } else { + this.logger.error(`Error occurred when querying remote agent with status ${response.status}`); } - } else { - this.logger.debug(`No result found. Request done in ${requestDuration} ms`); } - } else if (response.status === 400) { - const errorMessage = await response.text(); - this.logger.error(`Error occurred when querying remote agent with status ${response.status}: ${errorMessage}`); - } else { - this.logger.error(`Error occurred when querying remote agent with status ${response.status}`); } - return updatedStartTime; } diff --git a/frontend/src/app/south/south-detail/south-detail.component.html b/frontend/src/app/south/south-detail/south-detail.component.html index bf5e3b3bde..5e8f4ae59e 100644 --- a/frontend/src/app/south/south-detail/south-detail.component.html +++ b/frontend/src/app/south/south-detail/south-detail.component.html @@ -37,11 +37,7 @@

- +