/// <summary>
        /// Processes the feed results.  First, <see cref="LogRecord"/>s are added to <see cref="TrackedGpsData"/> of <see cref="TrackedVehicle"/>s and <see cref="StatusData"/>/<see cref="FaultData"/> records are added to <see cref="TrackedDiagnostic"/>s of <see cref="TrackedVehicle"/>s.  Then, the newly-received data for the affected <see cref="TrackedVehicle"/>s is written to file(s) (and summary info is written to the console window).
        /// </summary>
        /// <param name="results">The <see cref="FeedResultData"/> containing new data received from the data feeds.</param>
        /// <returns></returns>
        async Task ProcessFeedResultsAsync(FeedResultData results)
        {
            try
            {
                // For each received LogRecord, if the LogRecord is for a vehicle that is being tracked, add the LogRecord to the TrackedVehicle's TrackedGpsData.
                foreach (LogRecord logRecord in results.GpsRecords)
                {
                    TrackedVehicle trackedVehicleToUpdate = GetTrackedVehicle(logRecord.Device);
                    if (trackedVehicleToUpdate != null)
                    {
                        TrackedGpsData trackedGpsData = trackedVehicleToUpdate.TrackedGpsData;
                        trackedGpsData.AddData(logRecord);
                    }
                }

                if (useStatusDataFeed == true)
                {
                    // For each received StatusData, if the StatusData represents a Diagnostic that is being tracked and the StatusData is for a vehicle that is being tracked, add the StatusData to the TrackedVehicle's TrackedDiagnostics.
                    foreach (StatusData statusData in results.StatusData)
                    {
                        if (!DiagnosticsToTrack.Where(diagnostic => diagnostic.Id == statusData.Diagnostic.Id).Any())
                        {
                            continue;
                        }
                        TrackedVehicle trackedVehicleToUpdate = GetTrackedVehicle(statusData.Device);
                        if (trackedVehicleToUpdate != null)
                        {
                            TrackedDiagnostic trackedDiagnosticToUpdate = trackedVehicleToUpdate.TrackedDiagnostics.Where(trackedDiagnostic => trackedDiagnostic.DiagnosticId == statusData.Diagnostic.Id).First();
                            trackedDiagnosticToUpdate.AddData(statusData);
                        }
                    }
                }
                if (useFaultDataFeed == true)
                {
                    // For each received FaultData, if the FaultData represents a Diagnostic that is being tracked and the FaultData is for a vehicle that is being tracked, add the FaultData to the TrackedVehicle's TrackedDiagnostics.
                    foreach (FaultData faultData in results.FaultData)
                    {
                        if (!DiagnosticsToTrack.Where(diagnostic => diagnostic.Id == faultData.Diagnostic.Id).Any())
                        {
                            continue;
                        }
                        TrackedVehicle trackedVehicleToUpdate = GetTrackedVehicle(faultData.Device);
                        if (trackedVehicleToUpdate != null)
                        {
                            TrackedDiagnostic trackedDiagnosticToUpdate = trackedVehicleToUpdate.TrackedDiagnostics.Where(trackedDiagnostic => trackedDiagnostic.DiagnosticId == faultData.Diagnostic.Id).First();
                            trackedDiagnosticToUpdate.AddData(faultData);
                        }
                    }
                }
                WriteFeedResultStatsToConsole();
                foreach (TrackedVehicle trackedVehicle in TrackedVehicles)
                {
                    await trackedVehicle.WriteDataToFileAsync();
                }
            }
            catch (Exception ex)
            {
                ConsoleUtility.LogError(ex);
            }
        }
        /// <summary>
        /// Executes a "feed iteration" whereby data is retrieved via feed(s), feed result tokens are stored and persisted to file, feed results are processed (including writing data to output files and displaying summary data in the console window), and a delay of the configured duration is applied.  This method is called iteratively via the inherited <see cref="Worker.DoWorkAsync(bool)"/> method of the base <see cref="Worker"/> class.
        /// </summary>
        /// <returns></returns>
        public async override Task WorkActionAsync()
        {
            iterationNumber += 1;
            ConsoleUtility.LogSeparator2();
            ConsoleUtility.LogInfoMultiPart("Iteration:", iterationNumber.ToString(), Common.ConsoleColorForUnchangedData);

            // Execute the feed calls and get the results for processing.
            FeedResultData feedResultData = await feedProcessor.GetFeedDataAsync(feedParameters);

            // Write the feed result token (toVersion) values to file.
            using (StreamWriter faultDataTokenFileWriter = new(faultDataTokenFilePath))
            {
                faultDataTokenFileWriter.Write(feedParameters.LastFaultDataToken);
            }
            using (StreamWriter gpsTokenFileWriter = new(gpsTokenFilePath))
            {
                gpsTokenFileWriter.Write(feedParameters.LastGpsDataToken);
            }
            using (StreamWriter statusDataTokenFileWriter = new(statusDataTokenFilePath))
            {
                statusDataTokenFileWriter.Write(feedParameters.LastStatusDataToken);
            }

            // Process the feed results.
            await ProcessFeedResultsAsync(feedResultData);

            // Wait for the configured duration before executing the process again.
            ConsoleUtility.LogListItem($"Waiting for {FeedIntervalSeconds} second(s) before starting next iteration...");
            await Task.Delay(TimeSpan.FromSeconds(FeedIntervalSeconds));
        }
        /// <summary>
        /// Requests <see cref="LogRecord"/>, <see cref="FaultData"/> and <see cref="StatusData"/> records via data feeds.  Then, updates local caches of "lookup" data.  Finally, iterates through the returned objects, "hydrating" important child objects using their fully-populated counterparts in the caches.
        /// </summary>
        /// <param name="feedParameters">Contains the latest data tokens and collections to be used in the next set of data feed calls.</param>
        /// <returns><see cref="FeedResultData"/></returns>
        public async Task <FeedResultData> GetFeedDataAsync(FeedParameters feedParameters)
        {
            FeedResultData          feedResults = new FeedResultData(new List <LogRecord>(), new List <StatusData>(), new List <FaultData>());
            FeedResult <LogRecord>  feedLogRecordData;
            FeedResult <StatusData> feedStatusData = null;
            FeedResult <FaultData>  feedFaultData  = null;

            try
            {
                if (feedParameters.FeedStartOption == Common.FeedStartOption.CurrentTime || feedParameters.FeedStartOption == Common.FeedStartOption.SpecificTime)
                {
                    // If the feeds are to be started at the current date/time or at a specific date/time, get the appropriate DateTime value and then use it to populate the fromDate parameter when making the GetFeed() calls.
                    DateTime feedStartTime = DateTime.UtcNow;
                    if (feedParameters.FeedStartOption == Common.FeedStartOption.SpecificTime)
                    {
                        feedStartTime = feedParameters.FeedStartSpecificTimeUTC;
                    }
                    feedLogRecordData = await api.GetFeedLogRecordAsync(feedStartTime);

                    ConsoleUtility.LogListItem("GPS log records received:", feedLogRecordData.Data.Count().ToString(), Common.ConsoleColorForListItems, (feedLogRecordData.Data.Count() > 0) ? Common.ConsoleColorForChangedData : Common.ConsoleColorForUnchangedData);
                    if (UseStatusDataFeed == true)
                    {
                        feedStatusData = await api.GetFeedStatusDataAsync(feedStartTime);

                        ConsoleUtility.LogListItem("StatusData records received:", feedStatusData.Data.Count().ToString(), Common.ConsoleColorForListItems, (feedStatusData.Data.Count() > 0) ? Common.ConsoleColorForChangedData : Common.ConsoleColorForUnchangedData);
                    }
                    if (UseFaultDataFeed == true)
                    {
                        feedFaultData = await api.GetFeedFaultDataAsync(feedStartTime);

                        ConsoleUtility.LogListItem("FaultData records received:", feedFaultData.Data.Count().ToString(), Common.ConsoleColorForListItems, (feedFaultData.Data.Count() > 0) ? Common.ConsoleColorForChangedData : Common.ConsoleColorForUnchangedData);
                    }
                    // Switch to FeedResultToken for subsequent calls.
                    feedParameters.FeedStartOption = Common.FeedStartOption.FeedResultToken;
                }
                else
                {
                    // If the feeds are to be called based on feed result token, use the tokens returned in the toVersion of previous GetFeed() calls (or loaded from file, if continuing where processing last left-off) to populate the fromVersion parameter when making the next GetFeed() calls.
                    feedLogRecordData = await api.GetFeedLogRecordAsync(feedParameters.LastGpsDataToken);

                    ConsoleUtility.LogListItem("GPS log records received:", feedLogRecordData.Data.Count().ToString(), Common.ConsoleColorForListItems, (feedLogRecordData.Data.Count() > 0) ? Common.ConsoleColorForChangedData : Common.ConsoleColorForUnchangedData);
                    if (UseStatusDataFeed == true)
                    {
                        feedStatusData = await api.GetFeedStatusDataAsync(feedParameters.LastStatusDataToken);

                        ConsoleUtility.LogListItem("StatusData records received:", feedStatusData.Data.Count().ToString(), Common.ConsoleColorForListItems, (feedStatusData.Data.Count() > 0) ? Common.ConsoleColorForChangedData : Common.ConsoleColorForUnchangedData);
                    }
                    if (UseFaultDataFeed == true)
                    {
                        feedFaultData = await api.GetFeedFaultDataAsync(feedParameters.LastFaultDataToken);

                        ConsoleUtility.LogListItem("FaultData records received:", feedFaultData.Data.Count().ToString(), Common.ConsoleColorForListItems, (feedFaultData.Data.Count() > 0) ? Common.ConsoleColorForChangedData : Common.ConsoleColorForUnchangedData);
                    }
                }

                // Update local caches of "lookup" data.
                if (DateTime.UtcNow > nextCacheRepopulationTime)
                {
                    // "Feedless" caches are for object types not available via data feed in the MyGeotab API.  In this case, it is necessary to updates all of the objects of each type with every update.  Since these lookup data objects are not frequently-changing (if they were, they would be accessible via data feed), these caches are only updated on a specified interval instead of on every call to this GetFeedDataAsync() method in order to avoid unnecessary processing.
                    await UpdateAllFeedlessCachesAsync();

                    nextCacheRepopulationTime = DateTime.UtcNow.AddMinutes(CacheRepopulationIntervalMinutes);
                }
                // For object types supported by the MyGeotab API data feed, the local caches can be updated every time this GetFeedDataAsync() method is executed bacause only new or changed objects are returned.
                await UpdateDeviceCacheAsync();
                await UpdateDiagnosticCacheAsync();

                // Use the local caches to "hydrate" child objects of objects returned via data feed.
                feedParameters.LastGpsDataToken = feedLogRecordData.ToVersion;
                foreach (LogRecord logRecord in feedLogRecordData.Data)
                {
                    // Populate relevant LogRecord fields.
                    logRecord.Device = GetDevice(logRecord.Device);
                    feedResults.GpsRecords.Add(logRecord);
                }
                if (UseStatusDataFeed == true)
                {
                    feedParameters.LastStatusDataToken = feedStatusData.ToVersion;
                    foreach (StatusData statusData in feedStatusData.Data)
                    {
                        // Populate relevant StatusData fields.
                        statusData.Device     = GetDevice(statusData.Device);
                        statusData.Diagnostic = GetDiagnostic(statusData.Diagnostic);
                        feedResults.StatusData.Add(statusData);
                    }
                }
                if (UseFaultDataFeed == true)
                {
                    feedParameters.LastFaultDataToken = feedFaultData.ToVersion;
                    foreach (FaultData faultData in feedFaultData.Data)
                    {
                        // Populate relevant FaultData fields.
                        faultData.Device     = GetDevice(faultData.Device);
                        faultData.Diagnostic = GetDiagnostic(faultData.Diagnostic);
                        faultData.Controller = await GetControllerAsync(faultData.Controller);

                        faultData.FailureMode = await GetFailureModeAsync(faultData.FailureMode);

                        feedResults.FaultData.Add(faultData);
                    }
                }
            }
            catch (Exception e)
            {
                Console.ForegroundColor = Common.ConsoleColorForErrors;
                Console.WriteLine(e.Message);
                Console.ForegroundColor = Common.ConsoleColorDefault;
                if (e is HttpRequestException)
                {
                    await Task.Delay(5000);
                }
                if (e is DbUnavailableException)
                {
                    await Task.Delay(TimeSpan.FromMinutes(5));
                }
            }
            return(feedResults);
        }