/// <summary> /// Calc the aggregation range for given aggregation histogram /// </summary> /// <param name="aggregatedTimeSpan">Aggregated Histogram</param> /// <param name="toTime">End time of aggregation</param> /// <returns>The aggregation range</returns> public static DateTimeRange CalcAggregationRange(ContosoAggregatedOeeKpiHistogram aggregatedTimeSpan, DateTime toTime) { TimeSpan intervalTimeSpan = aggregatedTimeSpan[0].IntervalTimeSpan; DateTimeRange aggregateSpan = new DateTimeRange( (aggregatedTimeSpan.Intervals.Count == 1) ? toTime.Subtract(intervalTimeSpan) : RDXUtils.RoundDateTimeToTimeSpan( toTime.Subtract(TimeSpan.FromSeconds(1)), intervalTimeSpan), toTime ); return(aggregateSpan); }
/// <summary> /// Calc the search range for given aggregation histogram /// </summary> /// <param name="aggregatedTimeSpan">Aggregated Histogram</param> /// <returns>The search range</returns> public static DateTimeRange TotalSearchRangeFromNow(ContosoAggregatedOeeKpiHistogram aggregatedTimeSpan) { DateTime endTime = RDXUtils.RoundDateTimeToTimeSpan( DateTime.UtcNow, aggregatedTimeSpan.UpdateTimeSpan); DateTime startTime = endTime.Subtract(aggregatedTimeSpan.TotalTimeSpan); if (aggregatedTimeSpan.UpdateTimeSpan != aggregatedTimeSpan.IntervalTimeSpan) { if (aggregatedTimeSpan.TotalTimeSpan > aggregatedTimeSpan.IntervalTimeSpan) { // The full search range is totaltime + interval startTime = startTime.Subtract(aggregatedTimeSpan.IntervalTimeSpan); } startTime = startTime.Subtract(aggregatedTimeSpan.UpdateTimeSpan); } return(new DateTimeRange(startTime, endTime)); }
/// <summary> /// Return the time series for a given OPC UA server and a given node /// </summary> /// <param name="station">The OPC UA server</param> /// <param name="node">The OPC UA node Id</param> /// <param name="aggregationView">The hourly, daily or weekly view</param> /// <param name="getCount">Get event Count aggregate</param> public static async Task <AggregatedTimeSeriesResult> AggregatedNodeId( Station station, ContosoOpcUaNode node, ContosoTopologyNode.AggregationView aggregationView, bool getCount = false) { int aggregateIndex = (int)aggregationView; RDXOpcUaQueries opcUaQuery = new RDXOpcUaQueries(CancellationToken.None); ContosoAggregatedOeeKpiHistogram aggregatedTimeSpan = station[aggregateIndex]; DateTimeRange searchSpan = RDXUtils.TotalSearchRangeFromNow(aggregatedTimeSpan); DateTimeRange aggregateSpan = new DateTimeRange( searchSpan.To.Subtract(aggregatedTimeSpan.TotalTimeSpan), searchSpan.To ); double roundFactor = 100.0; int index = (int)RDXOpcUaQueries.AggregateIndex.Count; if (!getCount) { if (!IsAggregatedOperator(node.OpCode)) { throw new Exception("Unsupported Operator for aggregation"); } // handle special case for SubMaxMin, result is derived by substraction if (node.OpCode == ContosoOpcNodeOpCode.SubMaxMin) { index = (int)RDXOpcUaQueries.AggregateIndex.Max; } else { index = AggregatedOperatorIndex(node.OpCode); } } int resultDimension = aggregatedTimeSpan.Intervals.Count; DateTime [] dateTimeResult = CreateAggregateDateTimeArray(aggregateSpan.From, aggregatedTimeSpan.IntervalTimeSpan, resultDimension); AggregateResult queryResult = await opcUaQuery.GetAllAggregatedNodesWithInterval( aggregateSpan, station.Key, node.NodeId, aggregatedTimeSpan.IntervalTimeSpan ); int count = queryResult.Aggregate.Dimension.Count; AggregatedTimeSeriesResult result = new AggregatedTimeSeriesResult(resultDimension, aggregateSpan.To, aggregatedTimeSpan.IntervalTimeSpan, node.Units); if (queryResult != null) { for (int i = 0; i < count; i++) { double?value = queryResult.Aggregate.Aggregate.Measures.TryGetPropertyMeasure <double?>(new int[] { 0, i, index }); if (value != null) { // find matching date/time slot for value var dateTime = queryResult.Aggregate.Dimension[i]; int resultIndex = Array.IndexOf(dateTimeResult, dateTime); if (resultIndex >= 0) { result.YValues[resultIndex] = roundFactor * (double)value; if (node.OpCode == ContosoOpcNodeOpCode.SubMaxMin) { value = queryResult.Aggregate.Aggregate.Measures.TryGetPropertyMeasure <double?>(new int[] { 0, i, (int)RDXOpcUaQueries.AggregateIndex.Min }); if (value != null) { result.YValues[resultIndex] -= roundFactor * (double)value; } } result.YValues[resultIndex] = Math.Round(result.YValues[resultIndex]) / roundFactor; } else { throw new Exception("DateTime not found in aggregated query array"); } } } } return(result); }
/// <summary> /// Queries all intervals, servers and nodes in a topology for new values. /// Updates all relevances and checks for alerts. /// Returns list of task. /// Caller must wait for tasks to run to completion before topology is fully up to date. /// </summary> /// <param name="totalSearchSpan">Search span of query</param> /// <param name="topology">Topology to update</param> /// <param name="fullQuery">The cached query for the search span</param> /// <param name="opcUaServers">List of OPC UA servers to query for values</param> /// <param name="tasks">List of async tasks processing queries</param> /// <param name="aggregateIndex">The index of the aggregate in the topology nodes</param> public async Task ScheduleAllOeeKpiQueries( DateTimeRange totalSearchSpan, ContosoTopology topology, RDXCachedAggregatedQuery fullQuery, List <string> opcUaServers, List <Task> tasks, int aggregateIndex) { RDXAggregatedQueryCache queryCache = new RDXAggregatedQueryCache(); queryCache.List.Add(fullQuery); foreach (string appUri in opcUaServers) { Station station = topology[appUri] as Station; if (station != null) { ContosoAggregatedOeeKpiHistogram oeeKpiHistogram = station[aggregateIndex]; DateTime toTime = totalSearchSpan.To; int intervals = oeeKpiHistogram.Intervals.Count; oeeKpiHistogram.EndTime = toTime; station[aggregateIndex].EndTime = toTime; foreach (ContosoAggregatedOeeKpiTimeSpan oeeKpiTimeSpan in oeeKpiHistogram.Intervals) { DateTime fromTime; // first interval is current time if (totalSearchSpan.To != toTime || intervals == 1) { fromTime = toTime.Subtract(oeeKpiTimeSpan.IntervalTimeSpan); } else { fromTime = RDXUtils.RoundDateTimeToTimeSpan(toTime.Subtract(TimeSpan.FromSeconds(1)), oeeKpiTimeSpan.IntervalTimeSpan); } DateTimeRange intervalSearchSpan = new DateTimeRange(fromTime, toTime); if (toTime <= oeeKpiTimeSpan.EndTime) { // The interval is still up to date from a previous query, skip toTime = fromTime; continue; } oeeKpiTimeSpan.EndTime = toTime; toTime = fromTime; // find a cached query, if not try to cache the aggregation for this search span RDXCachedAggregatedQuery aggregatedQuery = queryCache.Find(intervalSearchSpan); if (aggregatedQuery == null) { RDXCachedAggregatedQuery newQuery = new RDXCachedAggregatedQuery(_opcUaQueries); queryCache.List.Add(newQuery); tasks.Add(newQuery.Execute(intervalSearchSpan)); aggregatedQuery = queryCache.Find(intervalSearchSpan); } RDXOeeKpiQuery oeeKpiQuery = new RDXOeeKpiQuery(_opcUaQueries, intervalSearchSpan, appUri, oeeKpiTimeSpan); await oeeKpiQuery.QueryAllNodes(tasks, station.NodeList, oeeKpiHistogram.AwaitTasks, aggregatedQuery); if (oeeKpiHistogram.CheckAlerts) { station.Status = ContosoPerformanceStatus.Good; foreach (ContosoOpcUaNode node in station.NodeList) { if (node.Minimum != null || node.Maximum != null) { station.Status |= await CheckAlert(intervalSearchSpan, appUri, station, node); } } } } } } }
/// <summary> /// Worker to query one aggregate of a topology /// </summary> /// <param name="aggregateIndex">The aggregation view to update by the worker</param> /// <param name="token">CancellationToken</param> public async Task Worker(int aggregateIndex, CancellationToken token) { RDXOpcUaQueries opcUaQueries = new RDXOpcUaQueries(token); RDXOeeKpiQueries oeeKpiQueries = new RDXOeeKpiQueries(opcUaQueries, token); RDXTrace.TraceInformation("RDX Worker {0} started", aggregateIndex); // give app some time to start before updating queries await Task.Delay(10000 *(aggregateIndex + 1)); while (!token.IsCancellationRequested) { Stopwatch stopWatch = new Stopwatch(); DateTime nextUpdate = DateTime.MaxValue; bool resetStartDelayed = false; stopWatch.Start(); Interlocked.Increment(ref _busyWorkers); // the station and node list is updated, other workers are delayed while (_workerStartDelayed != 0) { RDXTrace.TraceInformation("RDX Worker {0} delayed", aggregateIndex); await Task.Delay(1000); } try { List <Task> tasks = new List <Task>(); ContosoTopologyNode rootNode = _topology.GetRootNode(); ContosoAggregatedOeeKpiHistogram aggregatedTimeSpan = rootNode[aggregateIndex]; DateTimeRange searchSpan = RDXUtils.TotalSearchRangeFromNow(aggregatedTimeSpan); RDXTrace.TraceInformation("RDX Worker {0} updating Range {1} to {2}", aggregateIndex, searchSpan.From, searchSpan.To); // calc next update. To time is already rounded for update time span. nextUpdate = searchSpan.To + rootNode[aggregateIndex].UpdateTimeSpan; // query all stations in topology and find all active servers in timespan Task <StringDimensionResult> aggServerTask = opcUaQueries.AggregateServers(searchSpan); // always get an aggregate of all activity for the latest interval, use it as a cache TimeSpan intervalTimeSpan = aggregatedTimeSpan[0].IntervalTimeSpan; DateTimeRange aggregateSpan = RDXUtils.CalcAggregationRange(aggregatedTimeSpan, searchSpan.To); RDXCachedAggregatedQuery fullQuery = new RDXCachedAggregatedQuery(opcUaQueries); Task aggServerAndNodesTask = fullQuery.Execute(aggregateSpan); // wait for all outstanding aggregates tasks.Add(aggServerTask); tasks.Add(aggServerAndNodesTask); await RDXUtils.WhenAllTasks("Aggregates", tasks, stopWatch); List <string> topologyStations = _topology.GetAllChildren(_tree.TopologyRoot.Key, typeof(Station)); List <string> opcUaServers = await aggServerTask; // intersect list of active servers and schedule all queries tasks.Clear(); List <string> opcUaServersToQuery = opcUaServers.Intersect(topologyStations, StringComparer.InvariantCultureIgnoreCase).ToList(); await oeeKpiQueries.ScheduleAllOeeKpiQueries(searchSpan, _topology, fullQuery, opcUaServersToQuery, tasks, aggregateIndex); // wait for all outstanding queries await RDXUtils.WhenAllTasks("Queries", tasks, stopWatch); // Update the topology Oee and KPI values _topology.UpdateAllKPIAndOEEValues(aggregateIndex); // one worker issues the Browser update if (aggregatedTimeSpan.UpdateBrowser) { // Push updates to dashboard BrowserUpdate(); RDXTrace.TraceInformation("BrowserUpdate finished after {0}ms", stopWatch.ElapsedMilliseconds); } // add new stations and nodes to topology if (aggregatedTimeSpan.UpdateTopology) { // delay other workers Interlocked.Exchange(ref _workerStartDelayed, 1); resetStartDelayed = true; // only add stations and nodes if no other worker is busy yet if (Interlocked.Increment(ref _busyWorkers) == 2) { AddNewServers(fullQuery, topologyStations); await AddNewNodes(fullQuery, topologyStations); RDXTrace.TraceInformation("Add New Server and Nodes finished after {0}ms", stopWatch.ElapsedMilliseconds); } } } catch (Exception e) { RDXTrace.TraceError("Exception {0} in Worker after {1}ms", e.Message, stopWatch.ElapsedMilliseconds); } finally { Interlocked.Decrement(ref _busyWorkers); if (resetStartDelayed) { Interlocked.Decrement(ref _busyWorkers); Interlocked.Exchange(ref _workerStartDelayed, 0); } } RDXTrace.TraceInformation("RDX Worker {0} schedule next update for {1}", aggregateIndex, nextUpdate); TimeSpan delay = nextUpdate.Subtract(DateTime.UtcNow); if (delay.TotalMilliseconds > 0) { await Task.Delay(delay, token); } } }