/// <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>
        /// Updates the <see cref="Device"/> cache.  All new or changed <see cref="Device"/>s are obtained using a GetFeed() call.  Then, <see cref="Device"/>s that are already in the cache are replaced and any <see cref="Device"/>s not in the cache are added to the cache.
        /// </summary>
        /// <returns></returns>
        async Task UpdateDeviceCacheAsync()
        {
            ConsoleUtility.LogInfoStart("Updating Device cache...");

            // Populate the deviceCache, adding new items and updating existing items with their changed counterparts from the database.  Repeat execution of the GetFeedDeviceAsync method until no more results are returned to ensure that the cache is complete and up-to-date.
            FeedResult <Device> feedResult = null;
            bool keepGoing = true;

            while (keepGoing == true)
            {
                feedResult = await api.GetFeedDeviceAsync(lastDeviceFeedVersion);

                lastDeviceFeedVersion = feedResult.ToVersion;
                foreach (Device feedResultDevice in feedResult.Data)
                {
                    if (deviceCache.ContainsKey(feedResultDevice.Id))
                    {
                        deviceCache[feedResultDevice.Id] = feedResultDevice;
                    }
                    else
                    {
                        deviceCache.Add(feedResultDevice.Id, feedResultDevice);
                    }
                }
                if (feedResult.Data.Count < DefaultFeedResultsLimitDevice)
                {
                    keepGoing = false;
                }
            }

            ConsoleUtility.LogComplete(Common.ConsoleColorForUnchangedData);
            ConsoleUtility.LogListItem($"Device cache records added/updated:", feedResult.Data.Count.ToString(), Common.ConsoleColorForListItems, (feedResult.Data.Count > 0) ? Common.ConsoleColorForChangedData : Common.ConsoleColorForUnchangedData);
        }
        public static void Main()
        {
            try
            {
                Console.Title = "Geotab Customer Onboarding Starter Kit";
                ConsoleUtility.LogUtilityStartup("Customer Onboarding Starter Kit");

                ConsoleUtility.LogInfo("Available Utilities:");
                ConsoleUtility.LogListItem("1", ": Create Database & Load Devices", ConsoleColor.Green);
                ConsoleUtility.LogListItem("2", ": Update Devices", ConsoleColor.Green);

                bool utilitySelected = false;
                while (!utilitySelected)
                {
                    utilitySelected = true;
                    string input = ConsoleUtility.GetUserInput("number of the utility to launch (from the above list)");
                    if (int.TryParse(input, out int selection))
                    {
                        switch (selection)
                        {
                        case 1:
                            var processor_CreateDatabaseAndLoadDevices = Processor_CreateDatabaseAndLoadDevices.Create();
                            break;

                        case 2:
                            var processor_UpdateDevices = Processor_UpdateDevices.Create();
                            break;

                        default:
                            utilitySelected = false;
                            ConsoleUtility.LogError($"The value '{input}' is not valid.");
                            break;
                        }
                    }
                    else
                    {
                        utilitySelected = false;
                        ConsoleUtility.LogError($"The value '{input}' is not valid.");
                    }
                }
            }
            catch (Exception e)
            {
                ConsoleUtility.LogError(e);
            }
            finally
            {
                Console.WriteLine("======================================================================");
                Console.ForegroundColor = ConsoleColor.Yellow;
                ConsoleUtility.LogInfo("Customer Onboarding Starter Kit finshed.  Press any key to exit.");
                Console.ReadKey();
            }
        }
        /// <summary>
        /// Updates the <see cref="Diagnostic"/> cache.  All new or changed <see cref="Diagnostic"/>s are obtained using a GetFeed() call.  Then, the <see cref="Controller"/> and <see cref="UnitOfMeasure"/> child objects are "hydrated" with fully-populated cached counterparts. Finally, <see cref="Diagnostic"/>s that are already in the cache are replaced and any <see cref="Diagnostic"/>s not in the cache are added to the cache.
        /// </summary>
        async Task UpdateDiagnosticCacheAsync()
        {
            ConsoleUtility.LogInfoStart("Updating Diagnostic cache...");

            // Populate the diagnosticCache, adding new items and updating existing items with their changed counterparts from the database.  Repeat execution of the GetFeedDiagnosticAsync method until no more results are returned to ensure that the cache is complete and up-to-date.
            FeedResult <Diagnostic> feedResult = null;
            bool keepGoing = true;

            while (keepGoing == true)
            {
                feedResult = await api.GetFeedDiagnosticAsync(lastDiagnosticFeedVersion);

                lastDiagnosticFeedVersion = feedResult.ToVersion;
                foreach (Diagnostic feedResultDiagnostic in feedResult.Data)
                {
                    // Hydrate Controller and UnitOfMeasure objects.
                    Controller controller = feedResultDiagnostic.Controller;
                    if (controller == null)
                    {
                        feedResultDiagnostic.Controller = NoController.Value;
                    }
                    else if (!controller.Equals(NoController.Value))
                    {
                        feedResultDiagnostic.Controller = controllerCache[controller.Id];
                    }
                    UnitOfMeasure unitOfMeasure = feedResultDiagnostic.UnitOfMeasure;
                    if (unitOfMeasure != null)
                    {
                        feedResultDiagnostic.UnitOfMeasure = unitOfMeasureCache[unitOfMeasure.Id];
                    }

                    // Add or update.
                    if (diagnosticCache.ContainsKey(feedResultDiagnostic.Id))
                    {
                        diagnosticCache[feedResultDiagnostic.Id] = feedResultDiagnostic;
                    }
                    else
                    {
                        diagnosticCache.Add(feedResultDiagnostic.Id, feedResultDiagnostic);
                    }
                }
                if (feedResult.Data.Count < DefaultFeedResultsLimitDiagnostic)
                {
                    keepGoing = false;
                }
            }
            ConsoleUtility.LogComplete(Common.ConsoleColorForUnchangedData);
            ConsoleUtility.LogListItem($"Diagnostics added/updated:", feedResult.Data.Count.ToString(), Common.ConsoleColorForListItems, (feedResult.Data.Count > 0) ? Common.ConsoleColorForChangedData : Common.ConsoleColorForUnchangedData);
        }
        /// <summary>
        /// Writes updates to the console window for the current feed iteration including lists of <see cref="Device"/>s for which new <see cref="LogRecord"/>s, <see cref="StatusData"/> records and <see cref="FaultData"/> records have been received.
        /// </summary>
        public void WriteFeedResultStatsToConsole()
        {
            StringBuilder         vehicleListStringBuilder;
            string                vehicleList;
            List <TrackedVehicle> trackedVehiclesWithNewData;

            // Build and display list of devices for which there are GPS log updates.
            trackedVehiclesWithNewData = TrackedVehicles.Where(trackedVehicle => trackedVehicle.HasNewGpsLogRecords == true).ToList();
            if (trackedVehiclesWithNewData != null && trackedVehiclesWithNewData.Any())
            {
                vehicleListStringBuilder = new StringBuilder();
                foreach (TrackedVehicle trackedVehicle in trackedVehiclesWithNewData)
                {
                    _ = vehicleListStringBuilder.Append($"{trackedVehicle.DeviceId}, ");
                }
                vehicleList = vehicleListStringBuilder.ToString();
                vehicleList = vehicleList.Substring(0, vehicleList.Length - 2);
                ConsoleUtility.LogListItem("GPS log updates received for devices:", vehicleList, Common.ConsoleColorForListItems, Common.ConsoleColorForChangedData);
            }
            // Build and display list of devices for which there are StatusData updates.
            trackedVehiclesWithNewData = TrackedVehicles.Where(trackedVehicle => trackedVehicle.HasNewStatusDataRecords == true).ToList();
            if (trackedVehiclesWithNewData != null && trackedVehiclesWithNewData.Any())
            {
                vehicleListStringBuilder = new StringBuilder();
                foreach (TrackedVehicle trackedVehicle in trackedVehiclesWithNewData)
                {
                    _ = vehicleListStringBuilder.Append($"{trackedVehicle.DeviceId}, ");
                }
                vehicleList = vehicleListStringBuilder.ToString();
                vehicleList = vehicleList.Substring(0, vehicleList.Length - 2);
                ConsoleUtility.LogListItem("StatusData updates received for devices:", vehicleList, Common.ConsoleColorForListItems, Common.ConsoleColorForChangedData);
            }
            // Build and display list of devices for which there are FaultData updates.
            trackedVehiclesWithNewData = TrackedVehicles.Where(trackedVehicle => trackedVehicle.HasNewFaultDataRecords == true).ToList();
            if (trackedVehiclesWithNewData != null && trackedVehiclesWithNewData.Any())
            {
                vehicleListStringBuilder = new StringBuilder();
                foreach (TrackedVehicle trackedVehicle in trackedVehiclesWithNewData)
                {
                    _ = vehicleListStringBuilder.Append($"{trackedVehicle.DeviceId}, ");
                }
                vehicleList = vehicleListStringBuilder.ToString();
                vehicleList = vehicleList.Substring(0, vehicleList.Length - 2);
                ConsoleUtility.LogListItem("FaultData updates received for devices:", vehicleList, Common.ConsoleColorForListItems, Common.ConsoleColorForChangedData);
            }
        }
        /// <summary>
        /// Updates the <see cref="UnitOfMeasure"/> cache.  All <see cref="UnitOfMeasure"/> objects are obtained using a Get() call.  Then, <see cref="UnitOfMeasure"/> objects that are already in the cache are replaced and any <see cref="UnitOfMeasure"/> objects not in the cache are added to the cache.
        /// </summary>
        /// <returns></returns>
        async Task UpdateUnitOfMeasureCacheAsync()
        {
            ConsoleUtility.LogInfoStart("Updating UnitsOfMeasure cache...");

            List <UnitOfMeasure> returnedUnitsOfMeasure = await api.GetUnitsOfMeasureAsync() as List <UnitOfMeasure>;

            foreach (UnitOfMeasure returnedUnitOfMeasure in returnedUnitsOfMeasure)
            {
                if (unitOfMeasureCache.ContainsKey(returnedUnitOfMeasure.Id))
                {
                    unitOfMeasureCache[returnedUnitOfMeasure.Id] = returnedUnitOfMeasure;
                }
                else
                {
                    unitOfMeasureCache.Add(returnedUnitOfMeasure.Id, returnedUnitOfMeasure);
                }
            }

            ConsoleUtility.LogComplete(Common.ConsoleColorForUnchangedData);
            ConsoleUtility.LogListItem($"UnitsOfMeasure cache records added/updated:", returnedUnitsOfMeasure.Count.ToString(), Common.ConsoleColorForListItems, (returnedUnitsOfMeasure.Count > 0) ? Common.ConsoleColorForChangedData : Common.ConsoleColorForUnchangedData);
        }
        /// <summary>
        /// Updates the <see cref="FailureMode"/> cache.  All <see cref="FailureMode"/>s are obtained using a Get() call.  Then, <see cref="FailureMode"/>s that are already in the cache are replaced and any <see cref="FailureMode"/>s not in the cache are added to the cache.
        /// </summary>
        /// <returns></returns>
        async Task UpdateFailureModeCacheAsync()
        {
            ConsoleUtility.LogInfoStart("Updating FailureMode cache...");

            List <FailureMode> returnedFailureModes = await api.GetFailureModesAsync() as List <FailureMode>;

            foreach (FailureMode returnedFailureMode in returnedFailureModes)
            {
                if (failureModeCache.ContainsKey(returnedFailureMode.Id))
                {
                    failureModeCache[returnedFailureMode.Id] = returnedFailureMode;
                }
                else
                {
                    failureModeCache.Add(returnedFailureMode.Id, returnedFailureMode);
                }
            }

            ConsoleUtility.LogComplete(Common.ConsoleColorForUnchangedData);
            ConsoleUtility.LogListItem($"FailureMode cache records added/updated:", returnedFailureModes.Count.ToString(), Common.ConsoleColorForListItems, (returnedFailureModes.Count > 0) ? Common.ConsoleColorForChangedData : Common.ConsoleColorForUnchangedData);
        }
        /// <summary>
        /// Updates the <see cref="Controller"/> cache.  All <see cref="Controller"/>s are obtained using a Get() call.  Then, <see cref="Controller"/>s that are already in the cache are replaced and any <see cref="Controller"/>s not in the cache are added to the cache.
        /// </summary>
        /// <returns></returns>
        async Task UpdateControllerCacheAsync()
        {
            ConsoleUtility.LogInfoStart("Updating Controller cache...");

            List <Controller> returnedControllers = await api.GetControllersAsync() as List <Controller>;

            foreach (Controller returnedController in returnedControllers)
            {
                if (controllerCache.ContainsKey(returnedController.Id))
                {
                    controllerCache[returnedController.Id] = returnedController;
                }
                else
                {
                    controllerCache.Add(returnedController.Id, returnedController);
                }
            }

            ConsoleUtility.LogComplete(Common.ConsoleColorForUnchangedData);
            ConsoleUtility.LogListItem($"Controller cache records added/updated:", returnedControllers.Count.ToString(), Common.ConsoleColorForListItems, (returnedControllers.Count > 0) ? Common.ConsoleColorForChangedData : Common.ConsoleColorForUnchangedData);
        }
        static async Task Main()
        {
            IList <ConfigItem> configItems;
            bool           trackSpecificVehicles = false;
            IList <Device> devicesToTrack        = null;

            Common.FeedStartOption feedStartOption         = Common.FeedStartOption.CurrentTime;
            DateTime              feedStartSpecificTimeUTC = DateTime.MinValue;
            int                   feedIntervalSeconds      = 60;
            IList <Diagnostic>    diagnosticsToTrack       = null;
            string                parentOutputFolder;
            string                outputFolder;
            int                   maximumFileSizeBytes = 1024000;
            GeotabDataOnlyPlanAPI api;

            string username = "";
            string password = "";
            string server   = "my.geotab.com";
            string database = "";
            string title    = "";

            try
            {
                // Set title.
                title         = AppDomain.CurrentDomain.FriendlyName.Replace(".", " ");
                Console.Title = title;
                ConsoleUtility.LogUtilityStartup(title);

                // Request MyGeotab credentials and database name.
                server   = ConsoleUtility.GetUserInput($"MyGeotab server");
                database = ConsoleUtility.GetUserInput($"Database to run examples against.").ToLower();
                username = ConsoleUtility.GetUserInput($"MyGeotab username");
                password = ConsoleUtility.GetUserInputMasked($"MyGeotab password");

                // Create Geotab Data-Only Plan API instance and authenticate.
                api = new GeotabDataOnlyPlanAPI(server, database, username, password);
                ConsoleUtility.LogInfoStart("Authenticating...");
                await api.AuthenticateAsync();

                ConsoleUtility.LogOk();

                // Load configuration information from the config file.
                configItems = GetConfigItems("configuration");

                // Validate output folder and create subfolder for output files.
                parentOutputFolder = configItems.Where(configItem => configItem.Key == ArgNameOutputFolder).FirstOrDefault().Value;
                if (!Directory.Exists(parentOutputFolder))
                {
                    throw new ArgumentException($"The specified output folder, '{parentOutputFolder}', does not exist.");
                }
                DirectoryInfo directoryInfo = new DirectoryInfo(parentOutputFolder);
                string        subfolderName = $"Output_{DateTime.Now.ToString("yyyyMMdd_HHmmss")}";
                directoryInfo.CreateSubdirectory(subfolderName);
                outputFolder = Path.Combine(directoryInfo.FullName, subfolderName);

                // Validate and set maximum file size.
                string maxFileSizeMBString = configItems.Where(configItem => configItem.Key == ArgNameMaximumFileSizeMB).FirstOrDefault().Value;
                if (int.TryParse(maxFileSizeMBString, out int maxFileSizeMB))
                {
                    if (maxFileSizeMB > 0)
                    {
                        maximumFileSizeBytes = maxFileSizeMB * Common.MegabyteToByteMultiplier;
                    }
                }

                // Get the vehicle tracking option.
                string vehicleTrackingOption = configItems.Where(configItem => configItem.Key == ArgNameVehicleTrackingOption).FirstOrDefault().Value;
                switch (vehicleTrackingOption)
                {
                case nameof(Common.VehicleTrackingOption.Reporting):
                    trackSpecificVehicles = false;
                    break;

                case nameof(Common.VehicleTrackingOption.Specific):
                    trackSpecificVehicles = true;
                    break;

                default:
                    break;
                }

                // If specific vehicles are to be tracked, validate the supplied list of device IDs against the current database.  Discard any supplied items that are not valid device IDs.  If no valid device IDs are supplied, switch back to tracking vehicles that are currently reporting data.
                if (trackSpecificVehicles == true)
                {
                    ConsoleUtility.LogInfo("Validating SpecificVehiclesToTrack...");
                    string   vehicleListSupplied = configItems.Where(configItem => configItem.Key == ArgNameSpecificVehiclesToTrack).FirstOrDefault().Value;
                    string[] vehicleList         = vehicleListSupplied.Split(",");
                    devicesToTrack = new List <Device>();
                    IList <Device> devices = await GetAllDevicesAsync(api);

                    for (int vehicleListIndex = 0; vehicleListIndex < vehicleList.Length; vehicleListIndex++)
                    {
                        string vehicleDeviceId = vehicleList[vehicleListIndex];
                        Device checkedDevice   = devices.Where(device => device.Id.ToString() == vehicleDeviceId).FirstOrDefault();
                        if (checkedDevice == null)
                        {
                            ConsoleUtility.LogListItem($"Note - The following is not a valid device Id", $"{vehicleDeviceId}", Common.ConsoleColorForUnchangedData, Common.ConsoleColorForErrors);
                            continue;
                        }
                        devicesToTrack.Add(checkedDevice);
                    }
                    if (devicesToTrack.Count == 0)
                    {
                        ConsoleUtility.LogWarning($"No valid device IDs have been entered. Switching to tracking of vehicles that are currently reporting data.");
                        trackSpecificVehicles = false;
                    }
                }

                // Get diagnostics to be tracked.
                ConsoleUtility.LogInfo("Validating DiagnosticsToTrack...");
                string   diagnosticListSupplied = configItems.Where(configItem => configItem.Key == ArgNameDiagnosticsToTrack).FirstOrDefault().Value;
                string[] diagnosticList         = diagnosticListSupplied.Split(",");
                diagnosticsToTrack = new List <Diagnostic>();
                IList <Diagnostic> diagnostics = await GetAllDiagnosticsAsync(api);

                for (int diagnosticListIndex = 0; diagnosticListIndex < diagnosticList.Length; diagnosticListIndex++)
                {
                    string     diagnosticId      = diagnosticList[diagnosticListIndex];
                    Diagnostic checkedDiagnostic = diagnostics.Where(diagnostic => diagnostic.Id.ToString() == diagnosticId).FirstOrDefault();
                    if (checkedDiagnostic == null)
                    {
                        ConsoleUtility.LogListItem($"Note - The following is not a valid diagnostic Id", $"{diagnosticId}", Common.ConsoleColorForUnchangedData, Common.ConsoleColorForErrors);
                        continue;
                    }
                    diagnosticsToTrack.Add(checkedDiagnostic);
                }
                if (diagnosticsToTrack.Count == 0)
                {
                    ConsoleUtility.LogWarning($"No valid diagnostic IDs have been entered. As such, no diagnostics will be tracked.");
                }

                // Get the feed start option.
                string feedStartOptionString = configItems.Where(configItem => configItem.Key == ArgNameFeedStartOption).FirstOrDefault().Value;
                switch (feedStartOptionString)
                {
                case nameof(Common.FeedStartOption.CurrentTime):
                    feedStartOption = Common.FeedStartOption.CurrentTime;
                    break;

                case nameof(Common.FeedStartOption.FeedResultToken):
                    feedStartOption = Common.FeedStartOption.FeedResultToken;
                    break;

                case nameof(Common.FeedStartOption.SpecificTime):
                    string feedStartSpecificTimeUTCString = configItems.Where(configItem => configItem.Key == ArgNameFeedStartSpecificTimeUTC).FirstOrDefault().Value;
                    if (DateTime.TryParse(feedStartSpecificTimeUTCString, null, System.Globalization.DateTimeStyles.RoundtripKind, out feedStartSpecificTimeUTC) == false)
                    {
                        ConsoleUtility.LogWarning($"The value of '{feedStartSpecificTimeUTCString}' specified for FeedStartSpecificTimeUTC is invalid. As such, the current date and time will be used instead.");
                        feedStartOption = Common.FeedStartOption.CurrentTime;
                    }
                    else
                    {
                        feedStartOption = Common.FeedStartOption.SpecificTime;
                    }
                    break;

                default:
                    break;
                }

                // Get the feed interval.
                string feedIntervalSecondsString = configItems.Where(configItem => configItem.Key == ArgNameFeedIntervalSeconds).FirstOrDefault().Value;
                if (int.TryParse(feedIntervalSecondsString, out int feedIntervalSecondsInt))
                {
                    if (feedIntervalSecondsInt < ShortestAllowedFeedIntervalSeconds)
                    {
                        ConsoleUtility.LogListItem($"Note - The specified FeedIntervalSeconds value of '{feedIntervalSecondsString}' is less then the shortest allowed value of '{ShortestAllowedFeedIntervalSeconds.ToString()}'.  FeedIntervalSeconds will be set to:", ShortestAllowedFeedIntervalSeconds.ToString(), Common.ConsoleColorForUnchangedData, Common.ConsoleColorForErrors);
                        feedIntervalSeconds = ShortestAllowedFeedIntervalSeconds;
                    }
                    else
                    {
                        feedIntervalSeconds = feedIntervalSecondsInt;
                    }
                }
                else
                {
                    ConsoleUtility.LogListItem($"Note - The specified FeedIntervalSeconds value of '{feedIntervalSecondsString}' is invalid.  FeedIntervalSeconds will be set to:", ShortestAllowedFeedIntervalSeconds.ToString(), Common.ConsoleColorForUnchangedData, Common.ConsoleColorForErrors);
                    feedIntervalSeconds = ShortestAllowedFeedIntervalSeconds;
                }

                // Instantiate a DatabaseWorker to start processing the data feeds.
                bool   continuous        = true;
                Worker worker            = new DatabaseWorker(username, password, database, server, parentOutputFolder, outputFolder, maximumFileSizeBytes, feedIntervalSeconds, feedStartOption, feedStartSpecificTimeUTC, trackSpecificVehicles, devicesToTrack, diagnosticsToTrack);
                var    cancellationToken = new CancellationTokenSource();
                Task   task = Task.Run(async() => await worker.DoWorkAsync(continuous), cancellationToken.Token);
                if (continuous && Console.ReadLine() != null)
                {
                    worker.RequestStop();
                    cancellationToken.Cancel();
                }
            }
            catch (Exception ex)
            {
                ConsoleUtility.LogError(ex);
            }
            finally
            {
                ConsoleUtility.LogUtilityShutdown(title);
                Console.ReadKey();
            }
        }
        /// <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(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);
        }
Пример #11
0
        public static async Task Run(GeotabDataOnlyPlanAPI api)
        {
            ConsoleUtility.LogExampleStarted(typeof(GetFeedDiagnosticAsyncExample).Name);

            try
            {
                // Feed parameters.
                // See MyGeotab SDK <a href="https://geotab.github.io/sdk/software/guides/concepts/#result-limits">Result Limits</a> and <a href="https://geotab.github.io/sdk/software/api/reference/#M:Geotab.Checkmate.Database.DataStore.GetFeed1">GetFeed()</a> documentation for information about the feed result limit defined below.
                const int DefaultFeedResultsLimitDiagnostic = 50000;
                int       getFeedNumberOfCallsToMake        = 5;
                int       getFeedSecondsToWaitBetweenCalls  = 5;
                long?     feedVersion = 0;

                List <Diagnostic>       diagnosticCache = new();
                FeedResult <Diagnostic> feedResult;

                // Start by populating the diagnosticCache with a list of all diagnostics.
                ConsoleUtility.LogListItem($"Population of diagnosticCache started.");
                bool isFirstCall = true;
                bool keepGoing   = true;
                while (keepGoing == true)
                {
                    feedResult = await api.GetFeedDiagnosticAsync(feedVersion);

                    feedVersion = feedResult.ToVersion;
                    ConsoleUtility.LogListItem("GetFeedDiagnosticAsync executed:");
                    ConsoleUtility.LogListItem("FeedResult ToVersion:", feedVersion.ToString(), Common.ConsoleColorForUnchangedData, Common.ConsoleColorForSuccess);
                    ConsoleUtility.LogListItem("FeedResult Records:", feedResult.Data.Count.ToString(), Common.ConsoleColorForUnchangedData, Common.ConsoleColorForSuccess);
                    if (isFirstCall == true)
                    {
                        diagnosticCache.AddRange(feedResult.Data);
                        isFirstCall = false;
                    }
                    else
                    {
                        // Add new items to the cache, or update existing items with their changed counterparts.
                        foreach (Diagnostic feedResultDiagnostic in feedResult.Data)
                        {
                            Diagnostic cachedDiagnosticToUpdate = diagnosticCache.Where(diagnostic => diagnostic.Id == feedResultDiagnostic.Id).FirstOrDefault();
                            if (cachedDiagnosticToUpdate == null)
                            {
                                diagnosticCache.Add(feedResultDiagnostic);
                            }
                            else
                            {
                                var index = diagnosticCache.IndexOf(cachedDiagnosticToUpdate);
                                diagnosticCache[index] = feedResultDiagnostic;
                            }
                        }
                    }
                    if (feedResult.Data.Count < DefaultFeedResultsLimitDiagnostic)
                    {
                        keepGoing = false;
                    }
                }
                ConsoleUtility.LogListItem($"Population of diagnosticCache completed.");

                // Execute a GetFeed loop for the prescribed number of iterations, adding new items to the cache, or updating existing items with their changed counterparts.
                for (int getFeedCallNumber = 1; getFeedCallNumber < getFeedNumberOfCallsToMake + 1; getFeedCallNumber++)
                {
                    feedResult = await api.GetFeedDiagnosticAsync(feedVersion);

                    feedVersion = feedResult.ToVersion;
                    ConsoleUtility.LogListItem("GetFeedDiagnosticAsync executed.  Iteration:", getFeedCallNumber.ToString(), Common.ConsoleColorForUnchangedData, Common.ConsoleColorForSuccess);
                    ConsoleUtility.LogListItem("FeedResult ToVersion:", feedVersion.ToString(), Common.ConsoleColorForUnchangedData, Common.ConsoleColorForSuccess);
                    ConsoleUtility.LogListItem("FeedResult Records:", feedResult.Data.Count.ToString(), Common.ConsoleColorForUnchangedData, Common.ConsoleColorForSuccess);
                    // Add new items to the cache, or update existing items with their changed counterparts.
                    foreach (Diagnostic feedResultDiagnostic in feedResult.Data)
                    {
                        Diagnostic cachedDiagnosticToUpdate = diagnosticCache.Where(diagnostic => diagnostic.Id == feedResultDiagnostic.Id).FirstOrDefault();
                        if (cachedDiagnosticToUpdate == null)
                        {
                            diagnosticCache.Add(feedResultDiagnostic);
                        }
                        else
                        {
                            var index = diagnosticCache.IndexOf(cachedDiagnosticToUpdate);
                            diagnosticCache[index] = feedResultDiagnostic;
                        }
                    }
                    // Wait for the prescribed amount of time before making the next GetFeed call.
                    Thread.Sleep(getFeedSecondsToWaitBetweenCalls * 1000);
                }
            }
            catch (Exception ex)
            {
                ConsoleUtility.LogError(ex);
            }

            ConsoleUtility.LogExampleFinished(typeof(GetFeedDiagnosticAsyncExample).Name);
        }