diff --git a/plugins/EcoStruxureIT-DCE/v1/configValidation.json b/plugins/EcoStruxureIT-DCE/v1/configValidation.json new file mode 100644 index 00000000..fc472506 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/configValidation.json @@ -0,0 +1,18 @@ +{ + "steps": [ + { + "displayName": "Connect to Data Center Expert", + "dataStream": { "name": "rootDeviceGroup" }, + "required": true, + "error": "Could not sign in to Data Center Expert. Check the Server URL is reachable from SquaredUp (or the on-premise agent), the username and password are correct, and enable Ignore certificate errors if the server uses a self-signed certificate.", + "success": "Connected to Data Center Expert." + }, + { + "displayName": "Check device access", + "dataStream": { "name": "devices" }, + "required": false, + "error": "Signed in, but no devices were returned. Check the user has access to at least one device group in Data Center Expert.", + "success": "Devices are accessible." + } + ] +} diff --git a/plugins/EcoStruxureIT-DCE/v1/custom_types.json b/plugins/EcoStruxureIT-DCE/v1/custom_types.json new file mode 100644 index 00000000..75acf9c7 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/custom_types.json @@ -0,0 +1,23 @@ +[ + { + "name": "Device", + "sourceType": "Device", + "icon": "server", + "singular": "Device", + "plural": "Devices" + }, + { + "name": "Device Group", + "sourceType": "Device Group", + "icon": "folder-tree", + "singular": "Device Group", + "plural": "Device Groups" + }, + { + "name": "Sensor", + "sourceType": "Sensor", + "icon": "temperature-half", + "singular": "Sensor", + "plural": "Sensors" + } +] diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/alarmHistory.json b/plugins/EcoStruxureIT-DCE/v1/dataStreams/alarmHistory.json new file mode 100644 index 00000000..1e8e72df --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/alarmHistory.json @@ -0,0 +1,174 @@ +{ + "name": "alarmHistory", + "displayName": "Alarm History", + "description": "Resolved alarms that started within the timeframe, optionally filtered by device", + "tags": [ + "Alerts", + "Device" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v1/alarms/history", + "getArgs": [ + { + "key": "startDateTime", + "value": "{{ String(new Date(timeframe.start).getTime()) }}" + }, + { + "key": "endDateTime", + "value": "{{ String(new Date(timeframe.end).getTime()) }}" + } + ], + "postRequestScript": "alarms.js", + "errorHandling": { + "type": "script", + "script": "errorHandling/dceError.js" + } + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "device", + "label": "Device(s) (optional)", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Device" + ] + } + } + } + ], + "metadata": [ + { + "name": "description", + "displayName": "Alarm", + "shape": "string", + "role": "label" + }, + { + "name": "severityText", + "displayName": "Severity", + "shape": [ + "state", + { + "map": { + "error": [ + "Critical", + "Failure", + "Error" + ], + "warning": [ + "Warning" + ], + "unknown": [ + "Information", + "Informational" + ] + } + } + ] + }, + { + "name": "startTime", + "displayName": "Started", + "shape": "date", + "role": "timestamp" + }, + { + "name": "resolveTime", + "displayName": "Resolved", + "shape": "date" + }, + { + "name": "durationSeconds", + "displayName": "Duration", + "shape": [ + "seconds", + { + "formatDuration": true + } + ] + }, + { + "name": "deviceName", + "displayName": "Device", + "sourceId": "deviceGuid", + "sourceType": "Device", + "objectPropertyPath": "name" + }, + { + "name": "sensorName", + "displayName": "Sensor", + "sourceId": "sensorGuid", + "sourceType": "Sensor", + "objectPropertyPath": "name" + }, + { + "name": "recommendedAction", + "displayName": "Recommended Action", + "shape": "string" + }, + { + "name": "hidden", + "displayName": "Hidden", + "shape": "boolean" + }, + { + "name": "alarmAlertSuppressed", + "displayName": "Alerts Suppressed", + "shape": "boolean" + }, + { + "name": "manuallyResolvable", + "displayName": "Manually Resolvable", + "shape": "boolean", + "visible": false + }, + { + "name": "commentCount", + "displayName": "Comments", + "shape": "number", + "visible": false + }, + { + "name": "severity", + "displayName": "Severity Level", + "shape": "number", + "visible": false + }, + { + "name": "isxcGuid", + "displayName": "Alarm ID", + "shape": "string", + "role": "id", + "visible": false + }, + { + "name": "deviceGuid", + "displayName": "Device ID", + "shape": "string", + "visible": false + }, + { + "name": "sensorGuid", + "displayName": "Sensor ID", + "shape": "string", + "visible": false + } + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "startTime", + "desc" + ] + ] + } + }, + "timeframes": true +} diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/alarmSummary.json b/plugins/EcoStruxureIT-DCE/v1/dataStreams/alarmSummary.json new file mode 100644 index 00000000..aacdd422 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/alarmSummary.json @@ -0,0 +1,39 @@ +{ + "name": "alarmSummary", + "displayName": "Alarm Summary", + "description": "Count of active alarms by severity for each device group (including its sub-groups)", + "tags": ["Alerts", "Device Group"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v1/alarms/summary", + "postRequestScript": "alarmSummary.js", + "errorHandling": { "type": "script", "script": "errorHandling/dceError.js" } + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "deviceGroup", + "label": "Device group(s) (optional)", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Device Group"] } + } + } + ], + "metadata": [ + { "name": "deviceGroupName", "displayName": "Device Group", "sourceId": "deviceGroupGuid", "sourceType": "Device Group", "objectPropertyPath": "name", "role": "label" }, + { "name": "criticalAlarmCount", "displayName": "Critical", "shape": "number" }, + { "name": "warningAlarmCount", "displayName": "Warning", "shape": "number" }, + { "name": "infoAlarmCount", "displayName": "Informational", "shape": "number" }, + { "name": "totalAlarmCount", "displayName": "Total", "shape": "number", "role": "value" }, + { + "name": "status", + "displayName": "Status", + "shape": ["state", { "map": { "success": ["success"], "warning": ["warning"], "error": ["error"] } }] + }, + { "name": "deviceGroupGuid", "displayName": "Device Group ID", "shape": "string", "role": "id", "visible": false } + ], + "defaultShaping": { "sort": { "by": [["criticalAlarmCount", "desc"], ["warningAlarmCount", "desc"]] } }, + "timeframes": false +} diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/alarms.json b/plugins/EcoStruxureIT-DCE/v1/dataStreams/alarms.json new file mode 100644 index 00000000..5c97d2d9 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/alarms.json @@ -0,0 +1,159 @@ +{ + "name": "alarms", + "displayName": "Active Alarms", + "description": "Currently active alarms, optionally filtered by device", + "tags": [ + "Alerts", + "Device" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v1/alarms", + "postRequestScript": "alarms.js", + "errorHandling": { + "type": "script", + "script": "errorHandling/dceError.js" + } + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "device", + "label": "Device(s) (optional)", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Device" + ] + } + } + } + ], + "metadata": [ + { + "name": "description", + "displayName": "Alarm", + "shape": "string", + "role": "label" + }, + { + "name": "severityText", + "displayName": "Severity", + "shape": [ + "state", + { + "map": { + "error": [ + "Critical", + "Failure", + "Error" + ], + "warning": [ + "Warning" + ], + "unknown": [ + "Information", + "Informational" + ] + } + } + ] + }, + { + "name": "startTime", + "displayName": "Started", + "shape": "date", + "role": "timestamp" + }, + { + "name": "durationSeconds", + "displayName": "Duration", + "shape": [ + "seconds", + { + "formatDuration": true + } + ] + }, + { + "name": "deviceName", + "displayName": "Device", + "sourceId": "deviceGuid", + "sourceType": "Device", + "objectPropertyPath": "name" + }, + { + "name": "sensorName", + "displayName": "Sensor", + "sourceId": "sensorGuid", + "sourceType": "Sensor", + "objectPropertyPath": "name" + }, + { + "name": "recommendedAction", + "displayName": "Recommended Action", + "shape": "string" + }, + { + "name": "hidden", + "displayName": "Hidden", + "shape": "boolean" + }, + { + "name": "alarmAlertSuppressed", + "displayName": "Alerts Suppressed", + "shape": "boolean" + }, + { + "name": "manuallyResolvable", + "displayName": "Manually Resolvable", + "shape": "boolean", + "visible": false + }, + { + "name": "commentCount", + "displayName": "Comments", + "shape": "number", + "visible": false + }, + { + "name": "severity", + "displayName": "Severity Level", + "shape": "number", + "visible": false + }, + { + "name": "isxcGuid", + "displayName": "Alarm ID", + "shape": "string", + "role": "id", + "visible": false + }, + { + "name": "deviceGuid", + "displayName": "Device ID", + "shape": "string", + "visible": false + }, + { + "name": "sensorGuid", + "displayName": "Sensor ID", + "shape": "string", + "visible": false + } + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "startTime", + "desc" + ] + ] + } + }, + "timeframes": false +} diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/deviceGroupDevices.json b/plugins/EcoStruxureIT-DCE/v1/dataStreams/deviceGroupDevices.json new file mode 100644 index 00000000..2e39014c --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/deviceGroupDevices.json @@ -0,0 +1,42 @@ +{ + "name": "deviceGroupDevices", + "displayName": "Device Group Devices", + "description": "Devices in the selected device group, including devices in its sub-groups", + "tags": ["Device", "Device Group"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "v1/deviceGroups/{{encodeURIComponent(object.rawId)}}/devices", + "errorHandling": { "type": "script", "script": "errorHandling/dceError.js" } + }, + "matches": { + "sourceType": { "type": "oneOf", "values": ["Device Group"] } + }, + "metadata": [ + { "sourceId": "isxcGuid", "sourceType": "Device", "name": "label", "displayName": "Device", "shape": "string", "role": "label" }, + { + "name": "severityText", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": ["Information", "Informational", "Normal", "OK", "None"], + "warning": ["Warning"], + "error": ["Error", "Critical", "Failure"] + } + } + ] + }, + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "model", "displayName": "Model", "shape": "string" }, + { "name": "hostname", "displayName": "Hostname", "shape": "string" }, + { "name": "ipAddress", "displayName": "IP Address", "shape": "string" }, + { "name": "location", "displayName": "Location", "shape": "string" }, + { "name": "serialNumber", "displayName": "Serial Number", "shape": "string" }, + { "name": "inMaintenance", "displayName": "In Maintenance", "shape": "boolean" }, + { "name": "isxcGuid", "displayName": "Device ID", "shape": "string", "role": "id", "visible": false }, + { "name": "severity", "displayName": "Severity Level", "shape": "number", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/deviceGroups.json b/plugins/EcoStruxureIT-DCE/v1/dataStreams/deviceGroups.json new file mode 100644 index 00000000..4a89b533 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/deviceGroups.json @@ -0,0 +1,36 @@ +{ + "name": "deviceGroups", + "displayName": "Device Groups", + "description": "All device groups with their current status and device count", + "tags": ["Device Group", "Inventory"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v1/deviceGroups", + "errorHandling": { "type": "script", "script": "errorHandling/dceError.js" } + }, + "matches": "none", + "metadata": [ + { "sourceId": "isxcGuid", "sourceType": "Device Group", "name": "name", "displayName": "Device Group", "shape": "string", "role": "label" }, + { + "name": "severityText", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": ["Information", "Informational", "Normal", "OK", "None"], + "warning": ["Warning"], + "error": ["Error", "Critical", "Failure"] + } + } + ] + }, + { "name": "deviceCount", "displayName": "Devices", "shape": "number", "role": "value" }, + { "name": "parentName", "displayName": "Parent Group", "sourceId": "parentIsxcGuid", "sourceType": "Device Group", "objectPropertyPath": "name" }, + { "name": "isxcGuid", "displayName": "Device Group ID", "shape": "string", "role": "id", "visible": false }, + { "name": "parentIsxcGuid", "displayName": "Parent Group ID", "shape": "string", "visible": false }, + { "name": "severity", "displayName": "Severity Level", "shape": "number", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/devices.json b/plugins/EcoStruxureIT-DCE/v1/dataStreams/devices.json new file mode 100644 index 00000000..aac8e3a1 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/devices.json @@ -0,0 +1,41 @@ +{ + "name": "devices", + "displayName": "Devices", + "description": "All devices with their current status, model and network details", + "tags": ["Device", "Inventory"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v1/devices", + "errorHandling": { "type": "script", "script": "errorHandling/dceError.js" } + }, + "matches": "none", + "metadata": [ + { "sourceId": "isxcGuid", "sourceType": "Device", "name": "label", "displayName": "Device", "shape": "string", "role": "label" }, + { + "name": "severityText", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": ["Information", "Informational", "Normal", "OK", "None"], + "warning": ["Warning"], + "error": ["Error", "Critical", "Failure"] + } + } + ] + }, + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "model", "displayName": "Model", "shape": "string" }, + { "name": "hostname", "displayName": "Hostname", "shape": "string" }, + { "name": "ipAddress", "displayName": "IP Address", "shape": "string" }, + { "name": "location", "displayName": "Location", "shape": "string" }, + { "name": "serialNumber", "displayName": "Serial Number", "shape": "string" }, + { "name": "inMaintenance", "displayName": "In Maintenance", "shape": "boolean" }, + { "name": "isxcGuid", "displayName": "Device ID", "shape": "string", "role": "id", "visible": false }, + { "name": "parentGuid", "displayName": "Parent Device ID", "shape": "string", "visible": false }, + { "name": "severity", "displayName": "Severity Level", "shape": "number", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/eventLog.json b/plugins/EcoStruxureIT-DCE/v1/dataStreams/eventLog.json new file mode 100644 index 00000000..8767f699 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/eventLog.json @@ -0,0 +1,41 @@ +{ + "name": "eventLog", + "displayName": "Event Log", + "description": "Data Center Expert event log messages within the timeframe (newest 1,000)", + "tags": ["Events", "Logs"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v1/eventlogmessages", + "getArgs": [ + { "key": "startTime", "value": "{{ String(new Date(timeframe.start).getTime()) }}" }, + { "key": "endTime", "value": "{{ String(new Date(timeframe.end).getTime()) }}" }, + { "key": "sortOrder", "value": "desc" }, + { "key": "maxNum", "value": "1000" } + ], + "errorHandling": { "type": "script", "script": "errorHandling/dceError.js" } + }, + "matches": "none", + "metadata": [ + { + "name": "timeStamp", + "displayName": "Time", + "shape": "date", + "role": "timestamp", + "valueExpression": "{{ typeof $['timeStamp'] === 'number' && $['timeStamp'] > 0 ? new Date($['timeStamp']).toISOString() : null }}" + }, + { "name": "message", "displayName": "Message", "shape": "string", "role": "label" }, + { "name": "severity", "displayName": "Severity", "shape": "number" }, + { + "name": "categories", + "displayName": "Categories", + "shape": "string", + "valueExpression": "{{ ($['categories'] || []).map(c => c.label || c.name).filter(Boolean).join(', ') }}" + }, + { "name": "remoteUser", "displayName": "User", "shape": "string" }, + { "name": "remoteAddr", "displayName": "Remote Address", "shape": "string" }, + { "name": "requestUri", "displayName": "Request URI", "shape": "string", "visible": false }, + { "name": "id", "displayName": "Message ID", "shape": "number", "role": "id", "visible": false } + ], + "timeframes": true +} diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/rootDeviceGroup.json b/plugins/EcoStruxureIT-DCE/v1/dataStreams/rootDeviceGroup.json new file mode 100644 index 00000000..fc0d0a12 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/rootDeviceGroup.json @@ -0,0 +1,18 @@ +{ + "name": "rootDeviceGroup", + "displayName": "Root Device Group", + "description": "Identifier of the top-level device group visible to the configured user", + "tags": ["Device Group"], + "baseDataSourceName": "httpRequestUnscoped", + "visibility": { "type": "hidden" }, + "config": { + "httpMethod": "get", + "endpointPath": "v1/deviceGroups/top/guid", + "errorHandling": { "type": "script", "script": "errorHandling/dceError.js" } + }, + "matches": "none", + "metadata": [ + { "name": "result", "displayName": "Device Group ID", "shape": "string", "role": "label" } + ], + "timeframes": false +} diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/alarmSummary.js b/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/alarmSummary.js new file mode 100644 index 00000000..70fec37b --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/alarmSummary.js @@ -0,0 +1,26 @@ +// /v1/alarms/summary returns every device group in one call; the optional device group +// filter is applied here instead of calling /v1/deviceGroups/{guid}/alarmSummary per group, +// which would quickly hit the 5 requests per minute rate limit. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); + +const selectedGroups = (context.config && context.config.deviceGroup) || []; +const groupIds = new Set(selectedGroups.map((o) => unwrap(o.rawId)).filter(Boolean)); + +const count = (n) => (typeof n === "number" ? n : 0); + +result = (Array.isArray(data) ? data : []) + .filter((g) => g && g.deviceGroupGuid) + .filter((g) => groupIds.size === 0 || groupIds.has(g.deviceGroupGuid)) + .map((g) => { + const critical = count(g.criticalAlarmCount); + const warning = count(g.warningAlarmCount); + const info = count(g.infoAlarmCount); + return { + deviceGroupGuid: g.deviceGroupGuid, + criticalAlarmCount: critical, + warningAlarmCount: warning, + infoAlarmCount: info, + totalAlarmCount: critical + warning + info, + status: critical > 0 ? "error" : warning > 0 ? "warning" : "success" + }; + }); diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/alarms.js b/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/alarms.js new file mode 100644 index 00000000..ee73a4c8 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/alarms.js @@ -0,0 +1,33 @@ +// Shared by the alarms and alarmHistory streams, which return the same alarm shape. +// Filtering by device happens here rather than per-device (/v1/devices/{guid}/alarms) +// because the API allows only 5 requests per minute per service. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); + +const selectedDevices = (context.config && context.config.device) || []; +const deviceIds = new Set(selectedDevices.map((o) => unwrap(o.rawId)).filter(Boolean)); + +// Times are Unix milliseconds; a resolveTime of 0 means the alarm is still active. +const toIso = (ms) => (typeof ms === "number" && ms > 0 ? new Date(ms).toISOString() : null); + +result = (Array.isArray(data) ? data : []) + .filter((a) => a && a.isxcGuid) + .filter((a) => deviceIds.size === 0 || deviceIds.has(a.deviceGuid)) + .map((a) => ({ + isxcGuid: a.isxcGuid, + description: a.description, + severityText: a.severityText, + severity: a.severity, + startTime: toIso(a.startTime), + resolveTime: toIso(a.resolveTime), + durationSeconds: + typeof a.startTime === "number" && a.startTime > 0 + ? Math.round(((a.resolveTime > 0 ? a.resolveTime : Date.now()) - a.startTime) / 1000) + : null, + deviceGuid: a.deviceGuid, + sensorGuid: a.sensorGuid, + recommendedAction: a.recommendedAction, + hidden: a.hidden, + alarmAlertSuppressed: a.alarmAlertSuppressed, + manuallyResolvable: a.manuallyResolvable, + commentCount: a.commentCount + })); diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/errorHandling/dceError.js b/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/errorHandling/dceError.js new file mode 100644 index 00000000..c389f592 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/errorHandling/dceError.js @@ -0,0 +1,29 @@ +// Shared by every stream. Data Center Expert enforces a very low rate limit (5 requests per +// minute, per token, per service) and answers with a bare 429, so without this a throttled +// tile just says "request failed" and gives no hint that it will recover on its own. +const header = (name) => { + const headers = response.headers || {}; + return headers[name] || headers[name.toLowerCase()]; +}; + +if (response.status === 429) { + const retryAfter = header("Retry-After"); + result = + "Data Center Expert rate limit reached (5 requests per minute for each API service). " + + (retryAfter ? `Retry after ${retryAfter} seconds.` : "Wait a minute and refresh.") + + " If this happens often, reduce the number of tiles refreshing at the same time."; +} else if (response.status === 401) { + result = + "Data Center Expert rejected the request (HTTP 401). Check the username and password in the data source configuration."; +} else if (response.status === 403) { + result = + "The Data Center Expert user does not have permission to read this data (HTTP 403)."; +} else if (response.status === 404) { + result = + "Not found (HTTP 404). Check the Server URL points at a Data Center Expert server with the REST API available at /isxg."; +} else { + // Spring-style error bodies carry either `message` or OAuth `error_description`. + result = + (data && (data.message || data.error_description || data.error)) || + `Data Center Expert request failed with HTTP ${response.status}.`; +} diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/sensorHistory.js b/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/sensorHistory.js new file mode 100644 index 00000000..d05fc3a0 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/sensorHistory.js @@ -0,0 +1,148 @@ +// /v1/sensors/history returns one entry per sensor with its readings nested in +// timeValuePairs, so flatten to one row per reading. +// +// The API returns every stored reading with no aggregation option. Over long timeframes and +// several sensors that easily exceeds the ~6MB stream response limit, so the output is capped +// at MAX_ROWS in total. Each sensor gets a share of that budget: numeric readings beyond it +// are averaged into equal time buckets, and state readings are reduced to their changes (then +// evenly sampled if there are still too many). +const MAX_ROWS = 20000; + +const toIso = (ms) => (typeof ms === "number" && ms > 0 ? new Date(ms).toISOString() : null); + +const sensors = (Array.isArray(data) ? data : []) + .filter(Boolean) + .map((sensor) => ({ + sensor, + numeric: String(sensor.sensorType || "").startsWith("NUMBER"), + pairs: (sensor.timeValuePairs || []) + .filter((p) => p && typeof p.time === "number") + .sort((a, b) => a.time - b.time) + })); + +const rowFor = (sensor, time, value, valueText, severityText) => ({ + time: toIso(time), + value, + valueText, + severityText, + units: sensor.units, + kind: sensor.kind, + sensorGuid: sensor.sensorGuid, + sensorLabel: sensor.sensorLabel, + deviceGuid: sensor.deviceGuid, + deviceLabel: sensor.deviceLabel, + series: sensor.deviceLabel ? `${sensor.deviceLabel} - ${sensor.sensorLabel}` : sensor.sensorLabel +}); + +// Readings are formatted in the server's locale, so "1.234" may mean 1234 (comma-decimal +// locales) or 1.234. The API doesn't report its locale, so infer the decimal mark from the +// readings that can only be read one way: a separator not followed by exactly three digits, +// one after a leading zero, both separators together, or the same one repeated. Every +// reading in a response shares the server's locale; with no evidence, assume ".". +const numberToken = (text) => { + const match = String(text ?? "").match(/-?\d[\d.,]*/); + return match ? match[0].replace(/[.,]$/, "") : null; +}; + +const unambiguousDecimalMark = (s) => { + const lastDot = s.lastIndexOf("."); + const lastComma = s.lastIndexOf(","); + if (lastDot >= 0 && lastComma >= 0) return lastDot > lastComma ? "." : ","; + const sep = lastDot >= 0 ? "." : lastComma >= 0 ? "," : null; + if (!sep) return null; + if (s.indexOf(sep) !== s.lastIndexOf(sep)) return sep === "." ? "," : "."; + if (/^-?0[.,]/.test(s) || !new RegExp(`\\${sep}\\d{3}$`).test(s)) return sep; + return null; +}; + +const decimalMark = (() => { + let dot = 0; + let comma = 0; + for (const { numeric, pairs } of sensors) { + if (!numeric) continue; + for (const p of pairs) { + const s = numberToken(p.value); + const mark = s && unambiguousDecimalMark(s); + if (mark === ".") dot++; + if (mark === ",") comma++; + } + } + return comma > dot ? "," : "."; +})(); + +const numberOf = (text) => { + let s = numberToken(text); + if (!s) return null; + const mark = unambiguousDecimalMark(s) || decimalMark; + s = mark === "," ? s.replace(/\./g, "").replace(",", ".") : s.replace(/,/g, ""); + const n = parseFloat(s); + return Number.isNaN(n) ? null : n; +}; + +// Picks `limit` evenly spaced items, always keeping the first and last. +const evenlySample = (items, limit) => { + if (items.length <= limit) return items; + if (limit <= 1) return items.slice(-limit); + const step = (items.length - 1) / (limit - 1); + return Array.from({ length: limit }, (_, i) => items[Math.round(i * step)]); +}; + +const averageIntoBuckets = ({ sensor, pairs }, limit) => { + const start = pairs[0].time; + const span = Math.max(1, pairs[pairs.length - 1].time - start); + const bucketMs = span / limit; + const buckets = new Map(); + for (const p of pairs) { + const value = numberOf(p.value); + if (value === null) continue; + // The last reading sits exactly on the end of the span, so clamp it into the final + // bucket rather than opening one more than the limit allows. + const key = Math.min(limit - 1, Math.floor((p.time - start) / bucketMs)); + const bucket = buckets.get(key) || { sum: 0, count: 0, last: p }; + bucket.sum += value; + bucket.count += 1; + bucket.last = p; + buckets.set(key, bucket); + } + return [...buckets.entries()].map(([key, b]) => { + const avg = b.sum / b.count; + return rowFor(sensor, Math.round(start + key * bucketMs), avg, avg.toFixed(2), b.last.severityText); + }); +}; + +const sampleStates = ({ sensor, pairs }, limit) => { + // Keep the last reading too, so the chart runs to the end of the timeframe rather than + // stopping at the most recent change. + const changes = pairs.filter( + (p, i) => + i === 0 || + i === pairs.length - 1 || + p.value !== pairs[i - 1].value || + p.severityText !== pairs[i - 1].severityText + ); + return evenlySample(changes, limit).map((p) => rowFor(sensor, p.time, null, p.value, p.severityText)); +}; + +// Share the budget smallest-first, so sensors needing fewer rows than an equal share hand +// their unused allowance on to the larger ones. The total can never exceed MAX_ROWS. +let remaining = MAX_ROWS; +const rowsBySensor = new Map(); +[...sensors] + .sort((a, b) => a.pairs.length - b.pairs.length) + .forEach((entry, i, ordered) => { + const limit = Math.floor(remaining / (ordered.length - i)); + let rows; + if (limit <= 0 || entry.pairs.length === 0) { + rows = []; + } else if (entry.pairs.length <= limit) { + rows = entry.pairs.map((p) => + rowFor(entry.sensor, p.time, entry.numeric ? numberOf(p.value) : null, p.value, p.severityText) + ); + } else { + rows = entry.numeric ? averageIntoBuckets(entry, limit) : sampleStates(entry, limit); + } + remaining -= rows.length; + rowsBySensor.set(entry, rows); + }); + +result = sensors.flatMap((entry) => rowsBySensor.get(entry)); diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/sensors.js b/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/sensors.js new file mode 100644 index 00000000..8d64adfe --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/scripts/sensors.js @@ -0,0 +1,90 @@ +// /v1/devices/sensors returns every sensor the user can see in one call. Filtering by device +// and kind happens here rather than per-device (/v1/devices/{guid}/sensors) because the API +// allows only 5 requests per minute per service, so a request per device would be throttled. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); + +const selectedDevices = (context.config && context.config.device) || []; +const deviceIds = new Set(selectedDevices.map((o) => unwrap(o.rawId)).filter(Boolean)); + +// autocomplete values arrive as [{ value }], custom values may arrive as plain strings. +const selectedKinds = (context.config && context.config.kind) || []; +const kinds = new Set( + (Array.isArray(selectedKinds) ? selectedKinds : [selectedKinds]) + .map((k) => (k && typeof k === "object" ? k.value : k)) + .filter(Boolean) + .map((k) => String(k).toUpperCase()) +); + +// Readings are formatted in the server's locale, so "1.234" may mean 1234 (comma-decimal +// locales) or 1.234. The API doesn't report its locale, so infer the decimal mark from the +// readings that can only be read one way: a separator not followed by exactly three digits, +// one after a leading zero, both separators together, or the same one repeated. Every +// reading in a response shares the server's locale; with no evidence, assume ".". +const numberToken = (text) => { + const match = String(text ?? "").match(/-?\d[\d.,]*/); + return match ? match[0].replace(/[.,]$/, "") : null; +}; + +const unambiguousDecimalMark = (s) => { + const lastDot = s.lastIndexOf("."); + const lastComma = s.lastIndexOf(","); + if (lastDot >= 0 && lastComma >= 0) return lastDot > lastComma ? "." : ","; + const sep = lastDot >= 0 ? "." : lastComma >= 0 ? "," : null; + if (!sep) return null; + if (s.indexOf(sep) !== s.lastIndexOf(sep)) return sep === "." ? "," : "."; + if (/^-?0[.,]/.test(s) || !new RegExp(`\\${sep}\\d{3}$`).test(s)) return sep; + return null; +}; + +const detectDecimalMark = (texts) => { + let dot = 0; + let comma = 0; + for (const text of texts) { + const s = numberToken(text); + const mark = s && unambiguousDecimalMark(s); + if (mark === ".") dot++; + if (mark === ",") comma++; + } + return comma > dot ? "," : "."; +}; + +const parseReading = (text, decimalMark) => { + let s = numberToken(text); + if (!s) return null; + const mark = unambiguousDecimalMark(s) || decimalMark; + s = mark === "," ? s.replace(/\./g, "").replace(",", ".") : s.replace(/,/g, ""); + const n = parseFloat(s); + return Number.isNaN(n) ? null : n; +}; + +const isNumeric = (sensor) => String(sensor.sensorType || "").startsWith("NUMBER"); + +const sensors = (Array.isArray(data) ? data : []).filter((s) => s && s.guid); +const decimalMark = detectDecimalMark(sensors.filter(isNumeric).map((s) => s.value)); + +// `value` is the formatted reading ("21.5 °C"); `rawValue` may be unscaled, so prefer the +// leading number of `value` and fall back to `rawValue` (unformatted) only when that has none. +const toNumber = (sensor) => { + if (!isNumeric(sensor)) { + return null; + } + const parsed = parseReading(sensor.value, decimalMark); + return parsed !== null ? parsed : parseReading(sensor.rawValue, "."); +}; + +result = sensors + .filter((s) => deviceIds.size === 0 || deviceIds.has(s.podId)) + .filter((s) => kinds.size === 0 || kinds.has(String(s.kind || "").toUpperCase())) + .map((s) => ({ + guid: s.guid, + label: s.label || s.guid, + deviceGuid: s.podId, + kind: s.kind, + sensorType: s.sensorType, + location: s.location, + numericValue: toNumber(s), + units: s.units, + value: s.value, + rawValue: s.rawValue, + severityText: s.severityText + })); diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/sensorHistory.json b/plugins/EcoStruxureIT-DCE/v1/dataStreams/sensorHistory.json new file mode 100644 index 00000000..837b6e21 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/sensorHistory.json @@ -0,0 +1,48 @@ +{ + "name": "sensorHistory", + "displayName": "Sensor History", + "description": "Historical readings for the selected sensors over the timeframe", + "tags": ["Sensor", "Environment", "Power"], + "baseDataSourceName": "httpRequestScoped", + "config": { + "httpMethod": "get", + "endpointPath": "v1/sensors/history", + "getArgs": [ + { "key": "sensorGuid", "value": "{{ objects.map(o => o.rawId) }}" }, + { "key": "startTime", "value": "{{ String(new Date(timeframe.start).getTime()) }}" }, + { "key": "endTime", "value": "{{ String(new Date(timeframe.end).getTime()) }}" } + ], + "postRequestScript": "sensorHistory.js", + "errorHandling": { "type": "script", "script": "errorHandling/dceError.js" } + }, + "matches": { + "sourceType": { "type": "oneOf", "values": ["Sensor"] } + }, + "metadata": [ + { "name": "time", "displayName": "Time", "shape": "date", "role": "timestamp" }, + { "name": "value", "displayName": "Value", "shape": "number", "role": "value" }, + { "name": "series", "displayName": "Sensor", "shape": "string", "role": "label" }, + { "name": "units", "displayName": "Units", "shape": "string", "role": "unitLabel" }, + { "name": "valueText", "displayName": "Reading", "shape": "string" }, + { + "name": "severityText", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": ["Information", "Informational", "Normal", "OK", "None"], + "warning": ["Warning"], + "error": ["Error", "Critical", "Failure"] + } + } + ] + }, + { "sourceId": "sensorGuid", "sourceType": "Sensor", "name": "sensorLabel", "displayName": "Sensor Name", "shape": "string" }, + { "sourceId": "deviceGuid", "sourceType": "Device", "name": "deviceLabel", "displayName": "Device", "shape": "string" }, + { "name": "kind", "displayName": "Kind", "shape": "string" }, + { "name": "sensorGuid", "displayName": "Sensor ID", "shape": "string", "visible": false }, + { "name": "deviceGuid", "displayName": "Device ID", "shape": "string", "visible": false } + ], + "timeframes": ["last1hour", "last12hours", "last24hours", "last7days", "last30days"] +} diff --git a/plugins/EcoStruxureIT-DCE/v1/dataStreams/sensors.json b/plugins/EcoStruxureIT-DCE/v1/dataStreams/sensors.json new file mode 100644 index 00000000..c95f8efc --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/dataStreams/sensors.json @@ -0,0 +1,85 @@ +{ + "name": "sensors", + "displayName": "Sensors", + "description": "Current reading and status of every sensor, optionally filtered by device and sensor kind", + "tags": ["Sensor", "Environment", "Power"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v1/devices/sensors", + "postRequestScript": "sensors.js", + "errorHandling": { "type": "script", "script": "errorHandling/dceError.js" } + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "device", + "label": "Device(s) (optional)", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Device"] } + } + }, + { + "type": "autocomplete", + "name": "kind", + "label": "Sensor kind(s) (optional)", + "isMulti": true, + "isClearable": true, + "allowCustomValues": true, + "data": { + "source": "fixed", + "values": [ + { "value": "TEMPERATURE", "label": "Temperature" }, + { "value": "DIFF_TEMP", "label": "Temperature difference" }, + { "value": "HUMIDITY", "label": "Humidity" }, + { "value": "DEW_POINT", "label": "Dew point" }, + { "value": "AIRFLOW", "label": "Airflow" }, + { "value": "CURRENT", "label": "Current" }, + { "value": "AMP_DETECTOR", "label": "Current (amp detector)" }, + { "value": "VOLTAGE", "label": "Voltage" }, + { "value": "POWER_WATTS", "label": "Power (W)" }, + { "value": "POWER_KWATTS", "label": "Power (kW)" }, + { "value": "POWER_VA", "label": "Apparent power (VA)" }, + { "value": "POWER_KVA", "label": "Apparent power (kVA)" }, + { "value": "ENERGY_KWH", "label": "Energy (kWh)" }, + { "value": "KWATT_HOUR", "label": "Energy (kW hour)" }, + { "value": "DOOR", "label": "Door" }, + { "value": "MOTION_SENSOR", "label": "Motion" }, + { "value": "DRY_CONTACT", "label": "Dry contact" }, + { "value": "COMMUNICATION_SENSOR", "label": "Communication" }, + { "value": "STATE", "label": "State" }, + { "value": "NUMBER", "label": "Number" } + ] + } + } + ], + "metadata": [ + { "sourceId": "guid", "sourceType": "Sensor", "name": "label", "displayName": "Sensor", "shape": "string", "role": "label" }, + { "name": "deviceName", "displayName": "Device", "sourceId": "deviceGuid", "sourceType": "Device", "objectPropertyPath": "name" }, + { "name": "numericValue", "displayName": "Value", "shape": "number", "role": "value" }, + { "name": "units", "displayName": "Units", "shape": "string", "role": "unitLabel" }, + { "name": "value", "displayName": "Reading", "shape": "string" }, + { + "name": "severityText", + "displayName": "Status", + "shape": [ + "state", + { + "map": { + "success": ["Information", "Informational", "Normal", "OK", "None"], + "warning": ["Warning"], + "error": ["Error", "Critical", "Failure"] + } + } + ] + }, + { "name": "kind", "displayName": "Kind", "shape": "string" }, + { "name": "sensorType", "displayName": "Sensor Type", "shape": "string" }, + { "name": "location", "displayName": "Location", "shape": "string" }, + { "name": "guid", "displayName": "Sensor ID", "shape": "string", "role": "id", "visible": false }, + { "name": "deviceGuid", "displayName": "Device ID", "shape": "string", "visible": false }, + { "name": "rawValue", "displayName": "Raw Value", "shape": "string", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/EcoStruxureIT-DCE/v1/defaultContent/dataCenterOverview.dash.json b/plugins/EcoStruxureIT-DCE/v1/defaultContent/dataCenterOverview.dash.json new file mode 100644 index 00000000..fa24932d --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/defaultContent/dataCenterOverview.dash.json @@ -0,0 +1,351 @@ +{ + "name": "Data Center Overview", + "schemaVersion": "1.5", + "timeframe": "last7days", + "variables": [], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "28324941-9c73-42f1-b4cb-d669ed188c03", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Devices by Status", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[devices]}}", + "name": "devices", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "severityText", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "severityText_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + }, + { + "i": "64d81670-e535-48bc-8f50-d20af00ec6aa", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Alarms by Device Group", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[alarmSummary]}}", + "name": "alarmSummary", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "deviceGroupName", + "status", + "criticalAlarmCount", + "warningAlarmCount", + "infoAlarmCount", + "totalAlarmCount" + ] + } + } + } + } + }, + { + "i": "a5683fcf-81f3-4269-95da-9c7b718f52f7", + "x": 0, + "y": 3, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Alarms", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[alarms]}}", + "name": "alarms", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "severityText", + "description", + "deviceName", + "sensorName", + "startTime", + "durationSeconds", + "recommendedAction" + ], + "hiddenColumns": [ + "hidden", + "alarmAlertSuppressed" + ] + } + } + } + } + }, + { + "i": "4bda965d-57dc-4542-be3d-d7b8aeb42d01", + "x": 0, + "y": 7, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Alarm History", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[alarmHistory]}}", + "name": "alarmHistory", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "startTime", + "byDay" + ], + [ + "severityText", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "startTime_byDay", + "asc" + ] + ] + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "startTime_byDay", + "yAxisData": [ + "count" + ], + "xAxisGroup": "severityText_uniqueValues", + "xAxisLabel": "", + "yAxisLabel": "Alarms", + "showXAxisLabel": false, + "showYAxisLabel": true, + "showLegend": true, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": true, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + } + } + }, + { + "i": "a3922981-af15-457f-a3cf-b27bc956c1b1", + "x": 0, + "y": 10, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Devices", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[devices]}}", + "name": "devices", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "label", + "severityText", + "type", + "model", + "hostname", + "ipAddress", + "location", + "serialNumber", + "inMaintenance" + ] + } + } + } + } + }, + { + "i": "de3e4732-ac67-4e23-ba6f-edce015f3598", + "x": 0, + "y": 14, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Device Groups", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deviceGroups]}}", + "name": "deviceGroups", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "name", + "severityText", + "deviceCount", + "parentName" + ] + } + } + } + } + }, + { + "i": "10fc902e-7a2e-41ad-9075-dc080646a36a", + "x": 0, + "y": 17, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Event Log", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[eventLog]}}", + "name": "eventLog", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "timeStamp", + "desc" + ] + ] + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "timeStamp", + "severity", + "message", + "categories", + "remoteUser", + "remoteAddr" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/EcoStruxureIT-DCE/v1/defaultContent/device.dash.json b/plugins/EcoStruxureIT-DCE/v1/defaultContent/device.dash.json new file mode 100644 index 00000000..94b06d4d --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/defaultContent/device.dash.json @@ -0,0 +1,219 @@ +{ + "name": "Device", + "schemaVersion": "1.5", + "timeframe": "last7days", + "variables": [ + "{{variables.[Device]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "23dc19ba-2761-4e89-94a8-f7e7cde343c5", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Properties", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Devices]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device]}}" + }, + "variables": [ + "{{variables.[Device]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "32412c4f-4de0-4d1d-a72f-7685fd0327d9", + "x": 1, + "y": 0, + "w": 3, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Alarms", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[alarms]}}", + "name": "alarms", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "device": { + "variable": "{{variables.[Device]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Devices]}}" + } + } + }, + "variables": [ + "{{variables.[Device]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "severityText", + "description", + "sensorName", + "startTime", + "durationSeconds", + "recommendedAction" + ], + "hiddenColumns": [ + "deviceName", + "hidden", + "alarmAlertSuppressed" + ] + } + } + } + } + }, + { + "i": "53def2f3-4261-472f-92e5-5e2e69a18b70", + "x": 0, + "y": 4, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensors", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[sensors]}}", + "name": "sensors", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "device": { + "variable": "{{variables.[Device]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Devices]}}" + } + } + }, + "variables": [ + "{{variables.[Device]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "label", + "severityText", + "value", + "numericValue", + "units", + "kind", + "sensorType", + "location" + ], + "hiddenColumns": [ + "deviceName" + ] + } + } + } + } + }, + { + "i": "5832506d-2f53-42f9-81a1-0232eadd640e", + "x": 0, + "y": 8, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Alarm History", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[alarmHistory]}}", + "name": "alarmHistory", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "device": { + "variable": "{{variables.[Device]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Devices]}}" + } + } + }, + "variables": [ + "{{variables.[Device]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "severityText", + "description", + "sensorName", + "startTime", + "resolveTime", + "durationSeconds" + ], + "hiddenColumns": [ + "deviceName", + "recommendedAction", + "hidden", + "alarmAlertSuppressed" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/EcoStruxureIT-DCE/v1/defaultContent/deviceGroup.dash.json b/plugins/EcoStruxureIT-DCE/v1/defaultContent/deviceGroup.dash.json new file mode 100644 index 00000000..09ba9421 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/defaultContent/deviceGroup.dash.json @@ -0,0 +1,157 @@ +{ + "name": "Device Group", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": [ + "{{variables.[Device Group]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "dcccef89-6a4c-4a94-b3e8-349194170dbc", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Properties", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Device Groups]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device Group]}}" + }, + "variables": [ + "{{variables.[Device Group]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "dd120dd0-e1f0-42fc-a488-14f350ff949e", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Alarm Summary", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[alarmSummary]}}", + "name": "alarmSummary", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "deviceGroup": { + "variable": "{{variables.[Device Group]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Device Groups]}}" + } + } + }, + "variables": [ + "{{variables.[Device Group]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": [ + "status", + "criticalAlarmCount", + "warningAlarmCount", + "infoAlarmCount", + "totalAlarmCount" + ], + "hiddenColumns": [ + "deviceGroupName" + ] + } + } + } + } + }, + { + "i": "1b35a20c-ed9c-4f99-9b6e-c7e7d786e347", + "x": 0, + "y": 3, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Devices", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deviceGroupDevices]}}", + "name": "deviceGroupDevices", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Device Groups]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Device Group]}}" + }, + "variables": [ + "{{variables.[Device Group]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "label", + "severityText", + "type", + "model", + "hostname", + "ipAddress", + "location", + "serialNumber", + "inMaintenance" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/EcoStruxureIT-DCE/v1/defaultContent/manifest.json b/plugins/EcoStruxureIT-DCE/v1/defaultContent/manifest.json new file mode 100644 index 00000000..af1b5d3d --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/defaultContent/manifest.json @@ -0,0 +1,20 @@ +{ + "items": [ + { + "name": "dataCenterOverview", + "type": "dashboard" + }, + { + "name": "deviceGroup", + "type": "dashboard" + }, + { + "name": "device", + "type": "dashboard" + }, + { + "name": "sensor", + "type": "dashboard" + } + ] +} diff --git a/plugins/EcoStruxureIT-DCE/v1/defaultContent/scopes.json b/plugins/EcoStruxureIT-DCE/v1/defaultContent/scopes.json new file mode 100644 index 00000000..410fa9cc --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/defaultContent/scopes.json @@ -0,0 +1,53 @@ +[ + { + "name": "Devices", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Device" + ] + } + }, + "variable": { + "name": "Device", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Device Groups", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Device Group" + ] + } + }, + "variable": { + "name": "Device Group", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Sensors", + "matches": { + "sourceType": { + "type": "oneOf", + "values": [ + "Sensor" + ] + } + }, + "variable": { + "name": "Sensor", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/EcoStruxureIT-DCE/v1/defaultContent/sensor.dash.json b/plugins/EcoStruxureIT-DCE/v1/defaultContent/sensor.dash.json new file mode 100644 index 00000000..5e38918b --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/defaultContent/sensor.dash.json @@ -0,0 +1,162 @@ +{ + "name": "Sensor", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": [ + "{{variables.[Sensor]}}" + ], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "1a85d2c1-46b6-4a08-905f-147a99e07339", + "x": 0, + "y": 0, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Properties", + "description": "", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Sensors]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Sensor]}}" + }, + "variables": [ + "{{variables.[Sensor]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "5006ffec-2a62-4e19-8f5d-03216ffa96ce", + "x": 1, + "y": 0, + "w": 3, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Sensor History", + "description": "", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[sensorHistory]}}", + "name": "sensorHistory", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Sensors]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Sensor]}}" + }, + "variables": [ + "{{variables.[Sensor]}}" + ], + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "time", + "yAxisColumn": [ + "value" + ], + "seriesColumn": "series", + "showLegend": false, + "legendPosition": "bottom", + "yAxisLabel": "", + "showYAxisLabel": false, + "showTrendLine": false + } + } + } + } + }, + { + "i": "a1ea7336-590f-47a0-82a3-2698c4de9e97", + "x": 0, + "y": 4, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Readings", + "description": "Readings in the timeframe, including state sensors such as doors and contacts that the graph can't plot. Over long timeframes some readings and state changes may be averaged or left out to stay within the row limit.", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[sensorHistory]}}", + "name": "sensorHistory", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "time", + "desc" + ] + ] + } + }, + "scope": { + "scope": "{{scopes.[Sensors]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Sensor]}}" + }, + "variables": [ + "{{variables.[Sensor]}}" + ], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "time", + "valueText", + "severityText", + "units" + ], + "hiddenColumns": [ + "value", + "series", + "sensorLabel", + "deviceLabel", + "kind" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/EcoStruxureIT-DCE/v1/docs/README.md b/plugins/EcoStruxureIT-DCE/v1/docs/README.md new file mode 100644 index 00000000..f42a08d9 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/docs/README.md @@ -0,0 +1,73 @@ +Monitor the power, cooling and environmental devices managed by [EcoStruxure IT Data Center Expert](https://www.se.com/ww/en/product-range/61851-ecostruxure-it-data-center-expert/) (formerly StruxureWare Data Center Expert) in SquaredUp. The plugin covers device health, live sensor readings and history, and active and historical alarms, using the Data Center Expert REST API. + +> ⚠️ Data Center Expert is usually installed on-premises. If SquaredUp cannot reach the server directly, add this plugin through a [SquaredUp relay agent](https://docs.squaredup.com/features/connect-and-explore/relay-agents) that can. + +## Setup + +You will need a Data Center Expert **user account** and network access to the server's web interface (HTTPS). + +1. Sign in to the Data Center Expert client as an administrator and create (or choose) a user for SquaredUp. Give it access to the **device groups** you want to monitor. The plugin only reads data, so a view-only user is enough. +2. Check the REST API is available by browsing to `https:///isxg/dce-rest-api.html`. You should see the **DCE REST API UI** page. Optionally, click **Authorize** and sign in with the new user to confirm the credentials work. +3. In SquaredUp, enter the server address in **Server URL**, and the user's credentials in **Username** and **Password**. +4. If the server uses a self-signed certificate (the default for Data Center Expert), tick **Ignore certificate errors**. + +## Configuration fields + +| Field | What it is | Where to find it | Required | +| ----- | ---------- | ---------------- | -------- | +| **Server URL** | Address of the Data Center Expert server, for example `https://dce.example.com`. The `/isxg` REST API path is added automatically. | The address you use to open the Data Center Expert web interface. | Yes | +| **Username** | Data Center Expert user the plugin signs in as. | Data Center Expert client → user management. | Yes | +| **Password** | Password for that user. | Set when the user is created. | Yes | +| **Ignore certificate errors** | Skips HTTPS certificate checks. | Tick this when the server uses a self-signed certificate. | No | +| **OAuth client ID** | OAuth client sent when requesting an access token. Leave blank unless sign-in fails (see below). | See [If sign-in fails](#if-sign-in-fails). | No | +| **OAuth client secret** | Secret for the OAuth client above. | See [If sign-in fails](#if-sign-in-fails). | No | + +On save, the plugin signs in to Data Center Expert and reads the top-level device group, then checks that at least one device is visible. If the first check fails, the server address, credentials or certificate setting is wrong. If the second check fails, the user has no access to any device groups. + +### If sign-in fails + +The plugin gets a short-lived access token from `https:///isxg/oauth/token` using the username and password. Some Data Center Expert versions may also need an OAuth client ID and secret. If setup fails with a valid username and password: + +1. Open `https:///isxg/js/springfox-dce.js` in a browser. +2. Search the file for `client` or `Basic` to find the client ID and secret the API page itself uses. +3. Enter them in **OAuth client ID** and **OAuth client secret** under **Advanced**. + +## What this plugin monitors + +- **Device health**: the status of every UPS, PDU, cooling unit, NetBotz appliance and other device in Data Center Expert, plus model, serial number, IP address and maintenance state. +- **Sensors**: current readings such as temperature, humidity, power, current and door state, with status and units. History is available for any sensor. +- **Alarms**: active alarms with severity and recommended action, resolved alarm history, and alarm counts by device group. +- **Event log**: Data Center Expert's own event log messages. + +The out-of-the-box dashboards include a **Data Center Overview**, plus perspectives for each **Device**, **Sensor** and **Device Group**. + +## Data streams + +- **Devices**: every device with its current status, model and network details (account-wide). +- **Device Groups**: every device group with status and device count (account-wide). +- **Device Group Devices**: devices in a selected device group, including its sub-groups (per device group). +- **Sensors**: current reading and status of every sensor, optionally filtered by device and sensor kind. +- **Sensor History**: readings over time for the selected sensors (per sensor). +- **Active Alarms**: currently active alarms, optionally filtered by device. +- **Alarm History**: resolved alarms that started within the timeframe, optionally filtered by device. +- **Alarm Summary**: active alarm counts by severity per device group, optionally filtered by device group. +- **Event Log**: the newest 1,000 Data Center Expert event log messages in the timeframe (account-wide). + +## What gets indexed + +| Object type | API source | Represents | +| ----------- | ---------- | ---------- | +| **Device** | `GET /v1/devices` | A managed device such as a UPS, PDU, cooling unit or NetBotz appliance. | +| **Device Group** | `GET /v1/deviceGroups` | A device group from the Data Center Expert group tree. | +| **Sensor** | `GET /v1/devices/sensors` | A single sensor on a device, for example a temperature probe or an output current reading. | + +**Relationships:** each Sensor stores its parent device's ID (`deviceGuid`), each Device stores its parent device's ID (`parentGuid`), and each Device Group stores its parent group's ID (`parentIsxcGuid`). + +## Known limitations + +- **Strict rate limit**: Data Center Expert allows only 5 requests per minute for each API area (devices, device groups, alarms, sensors, event log). Each tile makes one request, so a dashboard with many Data Center Expert tiles, or several dashboards refreshing at once, can be throttled. Throttled tiles show a rate-limit message and load again on the next refresh. +- **Large estates**: sensors are read in a single request for the whole server. Very large installations (many thousands of sensors) may exceed SquaredUp's response size limit. +- **Sensor history is downsampled** over long timeframes: a tile returns at most 20,000 readings in total, shared across its sensors. Beyond that, numeric readings are averaged into time buckets, and state readings (such as door open or closed) are reduced to the points where the state changed. If a sensor has more state changes than its share of the limit, the changes are sampled evenly, so some changes may not be shown. +- **Sensor names are not unique**: a sensor is named as it appears in Data Center Expert (for example "Temperature"), so search results show many sensors with the same name. Drill down from a device to find its sensors. +- **Units follow the server locale**: readings use the Data Center Expert server's default locale (metric or US units). +- **Read-only**: the plugin never acknowledges alarms, changes device controls, or modifies anything in Data Center Expert. diff --git a/plugins/EcoStruxureIT-DCE/v1/icon.svg b/plugins/EcoStruxureIT-DCE/v1/icon.svg new file mode 100644 index 00000000..04e35194 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/icon.svg @@ -0,0 +1 @@ + diff --git a/plugins/EcoStruxureIT-DCE/v1/indexDefinitions/default.json b/plugins/EcoStruxureIT-DCE/v1/indexDefinitions/default.json new file mode 100644 index 00000000..bdc86428 --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/indexDefinitions/default.json @@ -0,0 +1,45 @@ +{ + "steps": [ + { + "name": "devices", + "dataStream": { "name": "devices" }, + "timeframe": "none", + "objectMapping": { + "id": "isxcGuid", + "name": "label", + "type": { "value": "Device" }, + "properties": [ + { "deviceType": "type" }, + "model", + "hostname", + "ipAddress", + "location", + "serialNumber", + "parentGuid" + ] + } + }, + { + "name": "deviceGroups", + "dataStream": { "name": "deviceGroups" }, + "timeframe": "none", + "objectMapping": { + "id": "isxcGuid", + "name": "name", + "type": { "value": "Device Group" }, + "properties": ["parentIsxcGuid"] + } + }, + { + "name": "sensors", + "dataStream": { "name": "sensors" }, + "timeframe": "none", + "objectMapping": { + "id": "guid", + "name": "label", + "type": { "value": "Sensor" }, + "properties": ["deviceGuid", "kind", "sensorType", "units", "location"] + } + } + ] +} diff --git a/plugins/EcoStruxureIT-DCE/v1/metadata.json b/plugins/EcoStruxureIT-DCE/v1/metadata.json new file mode 100644 index 00000000..74d6af6a --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/metadata.json @@ -0,0 +1,68 @@ +{ + "name": "ecostruxure-it-dce", + "displayName": "EcoStruxure IT Data Center Expert", + "version": "1.0.0", + "author": { + "name": "@shawn149", + "type": "community" + }, + "description": "Monitor data center power, cooling and environmental devices, sensors and alarms from EcoStruxure IT Data Center Expert (formerly StruxureWare).", + "category": "Infrastructure", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": [ + "struxureware", + "ecostruxure", + "dce", + "data center expert", + "schneider electric", + "apc", + "ups", + "pdu", + "netbotz", + "dcim", + "sensor", + "alarm" + ], + "objectTypes": ["Device", "Device Group", "Sensor"], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/EcoStruxureIT-DCE/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/EcoStruxureIT-DCE/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "{{serverUrl.trim().replace(/\\/+$/, '').replace(/\\/isxg$/i, '')}}/isxg", + "authMode": "oauth2", + "oauth2GrantType": "password", + "oauth2TokenUrl": "{{serverUrl.trim().replace(/\\/+$/, '').replace(/\\/isxg$/i, '')}}/isxg/oauth/token", + "oauth2PasswordGrantUserName": "{{username}}", + "oauth2PasswordGrantPassword": "{{password}}", + "oauth2ClientId": "{{typeof clientId === 'string' && clientId.trim() ? clientId.trim() : 'notused'}}", + "oauth2ClientSecret": "{{typeof clientSecret === 'string' && clientSecret ? clientSecret : 'notused'}}", + "oauth2ClientSecretLocationDuringAuth": "{{typeof clientId === 'string' && clientId.trim() ? 'header' : 'body'}}", + "oauth2Scope": "", + "oauth2TokenExtraArgs": [], + "oauth2TokenExtraHeaders": [], + "ignoreCertificateErrors": "{{ignoreCertificateErrors}}", + "headers": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "queryArgs": [] + } + } +} diff --git a/plugins/EcoStruxureIT-DCE/v1/ui.json b/plugins/EcoStruxureIT-DCE/v1/ui.json new file mode 100644 index 00000000..e09dbf4d --- /dev/null +++ b/plugins/EcoStruxureIT-DCE/v1/ui.json @@ -0,0 +1,62 @@ +[ + { + "type": "text", + "name": "serverUrl", + "label": "Server URL", + "help": "The address of the Data Center Expert server, for example `https://dce.example.com`. The `/isxg` REST API path is added automatically.", + "placeholder": "https://dce.example.com", + "validation": { + "required": true, + "pattern": { + "value": "^https?://[^\\s]+$", + "message": "Enter a URL starting with https://" + } + } + }, + { + "type": "text", + "name": "username", + "label": "Username", + "help": "A Data Center Expert user with access to the device groups to monitor.", + "placeholder": "Enter a username", + "validation": { + "required": true + } + }, + { + "type": "password", + "name": "password", + "label": "Password", + "placeholder": "Enter a password", + "validation": { + "required": true + } + }, + { + "type": "checkbox", + "name": "ignoreCertificateErrors", + "label": "Ignore certificate errors", + "help": "Enable when the Data Center Expert server uses a self-signed certificate." + }, + { + "type": "fieldGroup", + "name": "advanced", + "label": "Advanced", + "displayAs": "fieldGroupToggle", + "fields": [ + { + "type": "text", + "name": "clientId", + "label": "OAuth client ID", + "help": "Only needed if the Data Center Expert token endpoint rejects sign-in without a client. Leave blank by default.", + "placeholder": "Leave blank unless required" + }, + { + "type": "password", + "name": "clientSecret", + "label": "OAuth client secret", + "placeholder": "Leave blank unless required" + } + ] + } +]