/// <summary>
        /// Returns station reading DTO to be used by clients.
        /// Note: Cloudbase calculations taken from https://en.wikipedia.org/wiki/Cloud_base.
        /// </summary>
        /// <param name="stationReading">The station reading.</param>
        /// <param name="stationHeightInM">The station height in M.</param>
        /// <returns>Station reading Data Transfer Object.</returns>
        ///
        public static StationReadingDto ToStationReadingDto(this StationReading stationReading, float stationHeightInM)
        {
            StationReadingDto dto = new StationReadingDto();

            dto.DewpointC        = stationReading.DewpointC;
            dto.HeatIndexC       = stationReading.HeatIndexC;
            dto.Id               = stationReading.Id;
            dto.PressureMb       = stationReading.PressureMb;
            dto.RainCmPerHour    = stationReading.RainCmPerHour;
            dto.RelativeHumidity = stationReading.RelativeHumidity;
            dto.TempC            = stationReading.TempC;
            dto.When             = DateTime.SpecifyKind(stationReading.When, DateTimeKind.Utc);
            dto.WindChillC       = stationReading.WindChillC;
            dto.WindDegrees      = stationReading.WindDegrees;
            dto.WindAvgGustMph   = stationReading.WindAvgGustMph;
            dto.WindAvgMph       = stationReading.WindAvgMph;

            float spread       = stationReading.TempC - stationReading.DewpointC;
            float cloudBaseAgl = spread / 2.5f * 1000;

            dto.EstimatedCloudBaseFt = cloudBaseAgl + stationHeightInM.ToFeetFromM();

            return(dto);
        }
        public async Task Run(
            [QueueTrigger("davis-station-collections")] string queueItem)
        {
            // Queue item format is seperated by pipes: "[weather station ID]|[report epoch]|[collection attempt number]"
            string[] queueItemParts = queueItem.Split('|');

            if (!Guid.TryParse(queueItemParts[0], out Guid weatherStationId))
            {
                _logger.LogError($"Could not parse weather station id from queue item: '{queueItem}'.");
                return;
            }

            if (!DateTime.TryParse(queueItemParts[1], out DateTime reportEpoch))
            {
                _logger.LogError($"Could not parse report epoch from queue item: '{queueItem}'.");
                return;
            }

            if (!int.TryParse(queueItemParts[2], out int collectionAttemptNumber))
            {
                _logger.LogError($"Could not parse collection attempt number from queue item: '{queueItem}'.");
                return;
            }

            _logger.LogInformation($"Processing data collection for weather station: {weatherStationId}, report epoch: {reportEpoch:u}, collection attempt: {collectionAttemptNumber}");

            HttpResponseMessage httpResponse;

            WeatherStation weatherStation = await _weatherStationRepository.GetByIdAsync(weatherStationId);

            if (weatherStation == null)
            {
                _logger.LogError($"Could not find the weather station with id: {weatherStationId}.");
                return;
            }

            var userSetting         = weatherStation.FetcherSettings.SingleOrDefault(x => x.Key == "User");
            var passwordSetting     = weatherStation.FetcherSettings.SingleOrDefault(x => x.Key == "Password");
            var apiTokenSetting     = weatherStation.FetcherSettings.SingleOrDefault(x => x.Key == "ApiToken");
            var pickupPeriodSetting = weatherStation.FetcherSettings.SingleOrDefault(x => x.Key == "PickupPeriod");
            var timeZoneIdSetting   = weatherStation.FetcherSettings.SingleOrDefault(x => x.Key == "TimeZoneId");

            if (userSetting == null)
            {
                _logger.LogError($"Weather station with the Id: {weatherStation.Id} does not provide a value for 'User'.");
                return;
            }

            if (passwordSetting == null)
            {
                _logger.LogError($"Weather station with the Id: {weatherStation.Id} does not provide a value for 'Password'.");
                return;
            }

            if (apiTokenSetting == null)
            {
                _logger.LogError($"Weather station with the Id: {weatherStation.Id} does not provide a value for 'ApiKey'.");
                return;
            }

            if (pickupPeriodSetting == null)
            {
                _logger.LogError($"Weather station with the Id: {weatherStation.Id} does not provide a value for 'PickupPeriod'.");
                return;
            }

            if (timeZoneIdSetting == null)
            {
                _logger.LogError($"Weather station with the Id: {weatherStation.Id} does not provide a value for 'TimeZoneId'.");
                return;
            }

            var timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneIdSetting.Value);

            if (timeZone == null)
            {
                _logger.LogError($"'TimeZoneId' for weather station with Id: {weatherStationId} could not be parsed. For the UK use 'GMT Standard Time'");
            }

            try
            {
                HttpClient client     = _httpClientFactory.CreateClient();
                var        requestUrl = $"https://api.weatherlink.com/v1/NoaaExt.json?user={userSetting.Value}&pass={passwordSetting.Value}&apiToken={apiTokenSetting.Value}";
                httpResponse = await client.GetAsync(requestUrl);

                if (!httpResponse.IsSuccessStatusCode)
                {
                    _logger.LogError($"Could not query weather API for station with id: {weatherStationId}. Response code was {httpResponse.StatusCode}");
                    return;
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"Exception trying to receive weather information from API for weather station: {weatherStationId}");
                return;
            }

            string responseBody = await httpResponse.Content.ReadAsStringAsync();

            var weatherStationInfo = JsonSerializer.Deserialize <NoaaExtResult>(responseBody);

            // Station Reading
            StationReading receivedReading = weatherStationInfo.ToStationReading(weatherStation);

            _logger.LogInformation($"Parsed reading for station: {weatherStationId} reading date: {receivedReading.When:u}.");

            // This is not the report we were looking for! Maybe queue another attempt?
            if (receivedReading.When != reportEpoch)
            {
                if (collectionAttemptNumber < 3)
                {
                    QueueClient queueClient = new (
                        _storageQueueSettings.StorageConnectionString,
                        "davis-station-collections",
                        new QueueClientOptions {
                        MessageEncoding = QueueMessageEncoding.Base64
                    });

                    int      nextCollectionNumber = collectionAttemptNumber + 1;
                    DateTime nextAttemptEpoch     = reportEpoch.AddMinutes(nextCollectionNumber);
                    TimeSpan nextAttemptDelay     = DateTime.UtcNow - nextAttemptEpoch;

                    // 1 here denotes the collection attempt number for this report epoch
                    await queueClient.SendMessageAsync($"{weatherStation.Id:D}|{reportEpoch:u}|{nextCollectionNumber}", nextAttemptDelay);

                    _logger.LogWarning($"Attempt to receive report: {reportEpoch:u} but recieved: {receivedReading.When:u} for attempt {collectionAttemptNumber} @ {DateTime.UtcNow:u}. Enqueed another attempt at: {nextAttemptEpoch:u}.");
                }
                else
                {
                    _logger.LogError($"Attempt to receive report: {reportEpoch:u} but recieved: {receivedReading.When:u} for attempt {collectionAttemptNumber} @ {DateTime.UtcNow:u}. Not enqueuing another attempt.");
                }

                return;
            }

            var stationUpdateHub = new HubConnectionBuilder()
                                   .WithUrl(_signalRSettings.HubUri, connectionOptions =>
            {
                connectionOptions.Headers.Add("Authorization", $"SharedKey {_signalRSettings.SharedKey}");
            })
                                   .Build();

            try
            {
                await stationUpdateHub.StartAsync(CancellationToken.None);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"Error connecting to Signal R server. This will effect the real time updating of information in the widget.");
            }

            StationReading latestReading = await _stationReadingRepository.FetchLatestReadingAsync(weatherStationId);

            if (latestReading == null || latestReading.When != receivedReading.When)
            {
                _stationReadingRepository.Create(receivedReading);

                if (stationUpdateHub.State == HubConnectionState.Connected)
                {
                    await stationUpdateHub.SendAsync("NewStationReading", weatherStationId, receivedReading.ToStationReadingDto(weatherStation.AltitudeM));
                }
            }

            // Station Day Statistics
            DateTime             statisticsDate      = TimeZoneInfo.ConvertTimeFromUtc(receivedReading.When, timeZone).Date;
            StationDayStatistics latestDayStatistics = await _stationDayStatisticsRepository.FetchForDateAsync(weatherStationId, statisticsDate);

            StationDayStatistics receivedStats = weatherStationInfo.CurrentObservation.ToStationDayStatistics(weatherStation, statisticsDate);

            _logger.LogInformation($"Parsed daily stats for station: {weatherStationId} reading date: {statisticsDate}.");

            if (latestDayStatistics == null)
            {
                _stationDayStatisticsRepository.Create(receivedStats);
                _logger.LogInformation($"Creating new stats for: {weatherStationId} reading date: {statisticsDate}.");
            }
            else
            {
                latestDayStatistics.DewpointHighC          = receivedStats.DewpointHighC;
                latestDayStatistics.DewpointHighTime       = receivedStats.DewpointHighTime;
                latestDayStatistics.DewpointLowC           = receivedStats.DewpointLowC;
                latestDayStatistics.DewpointLowTime        = receivedStats.DewpointLowTime;
                latestDayStatistics.HeatIndexHighC         = receivedStats.HeatIndexHighC;
                latestDayStatistics.HeatIndexHighTime      = receivedStats.HeatIndexHighTime;
                latestDayStatistics.PressureHighMbar       = receivedStats.PressureHighMbar;
                latestDayStatistics.PressureHighTime       = receivedStats.PressureHighTime;
                latestDayStatistics.PressureLowMbar        = receivedStats.PressureLowMbar;
                latestDayStatistics.PressureLowTime        = receivedStats.PressureLowTime;
                latestDayStatistics.RainRateHighCmPerHour  = receivedStats.RainRateHighCmPerHour;
                latestDayStatistics.RelativeHumidityHigh   = receivedStats.RelativeHumidityHigh;
                latestDayStatistics.RelativeHumidityLow    = receivedStats.RelativeHumidityLow;
                latestDayStatistics.RelativeHumidyHighTime = receivedStats.RelativeHumidyHighTime;
                latestDayStatistics.RelativeHumidyLowTime  = receivedStats.RelativeHumidyLowTime;
                latestDayStatistics.TempHighC        = receivedStats.TempHighC;
                latestDayStatistics.TempHighTime     = receivedStats.TempHighTime;
                latestDayStatistics.TempLowC         = receivedStats.TempLowC;
                latestDayStatistics.TempLowTime      = receivedStats.TempLowTime;
                latestDayStatistics.TotalRainCm      = receivedStats.TotalRainCm;
                latestDayStatistics.WindChillLowC    = receivedStats.WindChillLowC;
                latestDayStatistics.WindChillLowTime = receivedStats.WindChillLowTime;
                latestDayStatistics.WindHighMph      = receivedStats.WindHighMph;
                latestDayStatistics.WindHighTime     = receivedStats.WindHighTime;
                _stationDayStatisticsRepository.Update(latestDayStatistics);
                _logger.LogInformation($"Updating stats for: {weatherStationId} reading date: {statisticsDate}.");
            }

            await _dbContext.SaveChangesAsync(CancellationToken.None);

            DateTime?lastRain = await _stationReadingRepository.FetchLastRainDateAsync(weatherStationId);

            StationStatisticsDto statisticsDto = new StationStatisticsDto
            {
                DayStatistics = latestDayStatistics.ToStationDayStatisticsDto(),
                LastRain      = lastRain,
            };

            if (stationUpdateHub.State == HubConnectionState.Connected)
            {
                await stationUpdateHub.SendAsync("UpdatedStationStatistics", weatherStationId, statisticsDto);
            }

            await stationUpdateHub.StopAsync();

            await stationUpdateHub.DisposeAsync();

            _logger.LogInformation($"Processed data collection for weather station: {weatherStationId}");
        }
        public static StationReading ToStationReading(this NoaaExtResult noaaExtResult, WeatherStation station)
        {
            StationReading stationReading = new StationReading();

            stationReading.Station = station;
            stationReading.When    = DateTime.Parse(noaaExtResult.ObservationTimeRfc822).ToUniversalTime();

            if (float.TryParse(noaaExtResult.DewPointC, out float dewpointC))
            {
                stationReading.DewpointC = dewpointC;
            }

            if (float.TryParse(noaaExtResult.HeatIndexC, out float heatIndexC))
            {
                stationReading.HeatIndexC = heatIndexC;
            }

            if (float.TryParse(noaaExtResult.PressureMb, out float pressureMb))
            {
                stationReading.PressureMb = pressureMb;
            }

            if (float.TryParse(noaaExtResult.RelativeHumidity, out float relativeHuimidity))
            {
                stationReading.RelativeHumidity = relativeHuimidity;
            }

            if (float.TryParse(noaaExtResult.TempC, out float tempC))
            {
                stationReading.TempC = tempC;
            }

            if (float.TryParse(noaaExtResult.WindChillC, out float windChillC))
            {
                stationReading.WindChillC = windChillC;
            }

            if (float.TryParse(noaaExtResult.WindDegrees, out float windDegrees))
            {
                stationReading.WindDegrees = windDegrees;
            }

            if (float.TryParse(noaaExtResult.WindMph, out float windMph))
            {
                stationReading.WindMph = windMph;
            }

            if (noaaExtResult.CurrentObservation != null)
            {
                if (float.TryParse(noaaExtResult.CurrentObservation.RainRateInPerHour, out float rainRateInPerHour))
                {
                    stationReading.RainCmPerHour = rainRateInPerHour.ToCmFromInches();
                }

                if (float.TryParse(noaaExtResult.CurrentObservation.WindTenMinAvgMph, out float windAvgMph))
                {
                    stationReading.WindAvgMph = windAvgMph;
                }

                if (float.TryParse(noaaExtResult.CurrentObservation.WindTenMinGustMph, out float windGustMph))
                {
                    stationReading.WindAvgGustMph = windGustMph;
                }
            }

            return(stationReading);
        }