示例#1
0
        /// <summary>Posts changes saved in the local DB (excluding histories) to the server. Only Items with
        /// UpdatedAt or CreatedAt changed since the last post are posted.</summary>
        private static async Task <List <string> > PostRequestUpdateAsync(MainDbContext db)
        {
            var creds = Creds.FromUserIdAndToken(Settings.Instance.CredUserId, Settings.Instance.CredToken);

            var lastDatabasePost = Settings.Instance.LastSuccessfulGeneralDbPost;
            var postTime         = DateTimeOffset.Now;

            var responses = new List <string>();

            // Simple tables that change:
            // CropCycle, Devices.

            responses.Add(
                await
                PostRequestTableWhereUpdatedAsync(db.Locations, nameof(db.Locations), lastDatabasePost, creds)
                .ConfigureAwait(false));

            // CropTypes is unique:
            var changedCropTypes = db.CropTypes.Where(c => c.CreatedAt > lastDatabasePost);

            if (changedCropTypes.Any())
            {
                var cropTypeData = JsonConvert.SerializeObject(changedCropTypes);
                responses.Add(
                    await Request.PostTable(ApiUrl, nameof(db.CropTypes), cropTypeData, creds).ConfigureAwait(false));
            }

            responses.Add(
                await
                PostRequestTableWhereUpdatedAsync(db.CropCycles, nameof(db.CropCycles), lastDatabasePost, creds)
                .ConfigureAwait(false));
            responses.Add(
                await
                PostRequestTableWhereUpdatedAsync(db.Devices, nameof(db.Devices), lastDatabasePost, creds)
                .ConfigureAwait(false));
            responses.Add(
                await
                PostRequestTableWhereUpdatedAsync(db.Sensors, nameof(db.Sensors), lastDatabasePost, creds)
                .ConfigureAwait(false));
            responses.Add(
                await
                PostRequestTableWhereUpdatedAsync(db.Relays, nameof(db.Relays), lastDatabasePost, creds)
                .ConfigureAwait(false));


            var errors = responses.Where(r => r != null).ToList();

            if (!errors.Any())
            {
                Settings.Instance.LastSuccessfulGeneralDbPost = postTime;
            }
            return(errors);
        }
示例#2
0
        private async Task <List <string> > GetRequestUpdateAsync(MainDbContext db)
        {
            var settings = Settings.Instance;
            var creds    = Creds.FromUserIdAndToken(settings.CredUserId, settings.CredToken);
            var now      = DateTimeOffset.Now;

            var res = new List <string>();


            // Setting configure await to false allows all of this method to be run on the threadpool.
            // Without setting it false the continuation would be posted onto the SynchronisationContext, which is the UI.
            res.Add(await GetReqDeserMergeTable(nameof(db.Parameters), db.Parameters).ConfigureAwait(false));
            res.Add(await GetReqDeserMergeTable(nameof(db.Placements), db.Placements).ConfigureAwait(false));
            res.Add(await GetReqDeserMergeTable(nameof(db.Subsystems), db.Subsystems).ConfigureAwait(false));
            res.Add(await GetReqDeserMergeTable(nameof(db.RelayTypes), db.RelayTypes).ConfigureAwait(false));
            res.Add(await GetReqDeserMergeTable(nameof(db.SensorTypes), db.SensorTypes).ConfigureAwait(false));

            if (res.Any(r => r != null))
            {
                return(res.Where(r => r != null).ToList());
            }
            db.SaveChanges();

            // Editable types that must be merged:

            res.Add(await GetReqDeserMergeTable(nameof(db.People), db.People, creds).ConfigureAwait(false));
            // Crop type is the only mergable that is no-auth.
            res.Add(await GetReqDeserMergeTable(nameof(db.CropTypes), db.CropTypes).ConfigureAwait(false));
            res.Add(await GetReqDeserMergeTable(nameof(db.Locations), db.Locations, creds).ConfigureAwait(false));
            res.Add(await GetReqDeserMergeTable(nameof(db.CropCycles), db.CropCycles, creds).ConfigureAwait(false));
            res.Add(await GetReqDeserMergeTable(nameof(db.Devices), db.Devices, creds).ConfigureAwait(false));
            res.Add(await GetReqDeserMergeTable(nameof(db.Relays), db.Relays, creds).ConfigureAwait(false));
            res.Add(await GetReqDeserMergeTable(nameof(db.Sensors), db.Sensors, creds).ConfigureAwait(false));

            if (res.Any(r => r != null))
            {
                return(res.Where(r => r != null).ToList());
            }
            db.SaveChanges();


            res = res.Where(r => r != null).ToList();

            if (!res.Any())
            {
                settings.LastSuccessfulGeneralDbGet = now;
            }

            return(res);
        }
示例#3
0
        /// <summary>Posts all new history items since the last time data was posted.</summary>
        /// <returns></returns>
        private static async Task <string> PostRequestHistoryAsync(MainDbContext db)
        {
            var creds     = Creds.FromUserIdAndToken(Settings.Instance.CredUserId, Settings.Instance.CredToken);
            var tableName = nameof(db.SensorsHistory);

            if (!db.SensorsHistory.Any())
            {
                return(null);
            }

            var uploadedAtWillBe = DateTimeOffset.Now;

            // This is a list of never uploaded histories (half uploaded locally updated histories will be exluded).
            // It should be noted that it is impossible to have a never-uploaded history older than a half uploaded one.
            //TRACKING
            var uploadSet   = db.SensorsHistory.AsTracking().Where(s => s.UploadedAt == default(DateTimeOffset)).ToList();
            var uploadQueue = new Queue <SensorHistory>(uploadSet);

            while (uploadQueue.Count > 0)
            {
                // Collect a batch of a sensible size.
                var       batch          = new List <SensorHistory>();
                const int maxUploadBatch = 30;
                while (batch.Count < maxUploadBatch && uploadQueue.Any())
                {
                    var item = uploadQueue.Dequeue();
                    // Histories come out of the database as raw blobs, serialise to populate the Data property.
                    // The json converter needs the Data property.
                    item.DeserialiseData();
                    batch.Add(item);
                    // This will only be saved if the whole upload is successful.
                    item.UploadedAt = uploadedAtWillBe;

                    // This next line combined with AsNoTracking would be efficient, but i dont trust EFCore.
                    db.Entry(item).Property(nameof(item.UploadedAt)).IsModified = true;
                }

                // Prepare and upload collection of histories.
                var json   = JsonConvert.SerializeObject(batch);
                var result = await Request.PostTable(ApiUrl, tableName, json, creds).ConfigureAwait(false);

                if (result != null)
                {
                    //abort, non-null responses are descriptions of errors
                    return(result);
                }
            }

            // Upload success, we can save the changes to UploadedAt
            await Task.Run(() => db.SaveChanges()).ConfigureAwait(true);

            // Untrack the items so they cant clash with the next part.
            foreach (var tracked in uploadSet)
            {
                db.Entry(tracked).State = EntityState.Detached;
            }

            // Now we need to upload anything that could have been uploaded after having its UploadedAt set.
            // We can recognise these because the UploadedAt will be newer than the timestamp.

            //TRACKED.
            var uploadedButMayBeChangedSet =
                db.SensorsHistory.AsTracking().Where(sh => sh.TimeStamp > sh.UploadedAt).ToList();

            var haveChanged = new List <SensorHistory>();

            foreach (var uploadedButMayBeChanged in uploadedButMayBeChangedSet)
            {
                // Detect local modifications by looking for datapoint timestamps newer than UploadedAt.
                uploadedButMayBeChanged.DeserialiseData();
                if (uploadedButMayBeChanged.Data.Any(d => d.TimeStamp > uploadedButMayBeChanged.UploadedAt))
                {
                    var updatedTime = DateTimeOffset.Now;
                    // Get the new part by clicing after the UploadAt date.
                    var hasChanged = uploadedButMayBeChanged.Slice(uploadedButMayBeChanged.UploadedAt);

                    haveChanged.Add(hasChanged);

                    uploadedButMayBeChanged.UploadedAt = updatedTime;
                }
            }


            var jsonOfLocallyChanged = JsonConvert.SerializeObject(haveChanged);

            Debug.WriteLine($"Uploading {haveChanged.Count} histories.");
            var uploadLocallyChangedResult = await Request.PostTable(ApiUrl, tableName, jsonOfLocallyChanged, creds);

            if (uploadLocallyChangedResult != null)
            {
                //abort
                return(uploadLocallyChangedResult);
            }

            // Upload success, we can save the changes to UploadedAt
            await Task.Run(() => db.SaveChanges()).ConfigureAwait(true);

            // Untracking the items isn't necessary because this is the last part of Sync, context to be disposed.
            // However its much safer to do so incase someone updates Sync with new stuff.
            foreach (var tracked in uploadedButMayBeChangedSet)
            {
                db.Entry(tracked).State = EntityState.Detached;
            }
            // Return success.

            return(null);
        }
示例#4
0
        /// <summary>Updates sensor history from server.</summary>
        /// <returns>Errors, null on succes. Some data may have been successfully saved alongside errors.</returns>
        private static async Task <string> GetRequestSensorHistoryAsync(MainDbContext db)
        {
            var devices = db.Devices.AsNoTracking().Include(d => d.Sensors).ToList();
            var table   = nameof(db.SensorsHistory);
            var errors  = "";

            // Download all the data for each device in turn.
            foreach (var device in devices)
            {
                // Find the newest received item so dowloading can resume after it.
                // There will be items for multiple sensors with the same time, it diesn't matter which one is used.
                // It will be updated after each item is added, much cheaper than re-querying.
                var mostRecentDayDownloaded =
                    db.SensorsHistory.AsNoTracking()
                    .Include(sh => sh.Sensor)
                    .Where(sh => sh.Sensor.DeviceID == device.ID)
                    // Never uploaded days may be much newer than items that have not yet been downloaded.
                    .Where(sh => sh.UploadedAt != default(DateTimeOffset))     //Ignore never uploaded days.
                    .OrderByDescending(sh => sh.TimeStamp).FirstOrDefault();   //Don't use MaxBy, it wont EF query.

                DateTimeOffset mostRecentDownloadedTimestamp;
                if (null == mostRecentDayDownloaded)
                {
                    // Data has never been downloaded for this device, so start downloading for time 0.
                    mostRecentDownloadedTimestamp = default(DateTimeOffset);
                }
                else
                {
                    //The timestamp is always the end of the day so look inside to see what time the data actually ends.

                    // Its pulled out of the database as raw so deserialise to access the data.
                    mostRecentDayDownloaded.DeserialiseData();
                    if (mostRecentDayDownloaded.Data.Any())
                    {
                        mostRecentDownloadedTimestamp = mostRecentDayDownloaded.Data.Max(entry => entry.TimeStamp);
                    }
                    else
                    {
                        Log.ShouldNeverHappen(
                            $"Found a history with no entries: {device.Name}, {mostRecentDayDownloaded.TimeStamp}");

                        // This is a broken situation, but it should be  fine if we continue from the start of the day.
                        mostRecentDownloadedTimestamp = mostRecentDayDownloaded.TimeStamp - TimeSpan.FromDays(1);
                    }
                }

                // This loop will keep requesting data untill the server gives no more.
                bool itemsReceived;
                var  added = new List <SensorHistory>();
                do
                {
                    var cred = Creds.FromUserIdAndToken(Settings.Instance.CredUserId, Settings.Instance.CredToken);
                    // Although any time far in the past should work, using 0 makes intent clear in debugging.
                    var unixTimeSeconds = mostRecentDownloadedTimestamp == default(DateTimeOffset)
                        ? 0
                        : mostRecentDownloadedTimestamp.ToUnixTimeSeconds();

                    List <SensorHistory> histories;
                    try
                    {
                        // Download with get request.
                        var raw =
                            await
                            GetRequestTableThrowOnErrorAsync($"{table}/{device.ID}/{unixTimeSeconds}/{MaxDaysDl}",
                                                             cred).ConfigureAwait(false);

                        // Deserialise download to POCO object.
                        histories =
                            await DeserializeTableThrowOnErrrorAsync <SensorHistory>(table, raw).ConfigureAwait(false);
                    }
                    catch (Exception ex)
                    {
                        // Something went wrong, try moving onto next device.
                        var message = $"GetRequest or deserialise failed for {device.Name}: {ex}";
                        errors += message + Environment.NewLine;
                        Debug.WriteLine(message);
                        break;
                    }

                    Debug.WriteLine($"{histories.Count} dl for {device.Name}");

                    if (histories.Any())
                    {
                        var lastDayOfThisDownloadSet = DateTimeOffset.MinValue;
                        foreach (var hist in histories)
                        {
                            Debug.WriteLine(
                                $"[{mostRecentDownloadedTimestamp}]{hist.TimeStamp}#{hist.UploadedAt}#{device.Name}#{hist.SensorID}");

                            // See if this is a new object.
                            var existing =
                                db.SensorsHistory.AsNoTracking()
                                .FirstOrDefault(
                                    sh => sh.SensorID == hist.SensorID && sh.TimeStamp.Date == hist.TimeStamp.Date);
                            // Make sure that its not an object tracked from a previous loop.
                            // This can happen because the end of the previous day will always get sliced with no data.
                            // That end slice isn't perfect but is makes sure all the data was taken.
                            if (existing == null)
                            {
                                existing =
                                    added.FirstOrDefault(
                                        sh => sh.SensorID == hist.SensorID && sh.TimeStamp.Date == hist.TimeStamp.Date);
                                if (existing != null)
                                {
                                    // Detach the existing object because it will be merged and replaced.
                                    var entityEntry = db.Entry(existing);
                                    entityEntry.State = EntityState.Detached;
                                    added.Remove(existing);
                                }
                            }
                            if (existing == null)
                            {
                                // The json deserialiser deserialises json to the .Data property.
                                // The data has to be serialised into raw-data-blobs before saving to the databse.
                                hist.SerialiseData();
                                added.Add(db.Add(hist).Entity);
                            }
                            else
                            {
                                // The data is pulled from the database as serialised raw data blobs.
                                existing.DeserialiseData();
                                // The merged object merges using the deserialised Data property.
                                var merged = SensorHistory.Merge(existing, hist);
                                // Data has to be serialised again into raw data blobs for the database.
                                merged.SerialiseData();

                                added.Add(db.Update(merged).Entity);
                            }

                            // This day was just downloaded so it must be completed
                            if (hist.TimeStamp > lastDayOfThisDownloadSet)
                            {
                                lastDayOfThisDownloadSet = hist.TimeStamp;
                            }
                        }
                        // The GET request allways gets days completed to the end (start may be missing but not the end)
                        mostRecentDownloadedTimestamp = lastDayOfThisDownloadSet;
                        itemsReceived = true;
                    }
                    else
                    {
                        itemsReceived = false;
                    }
                } while (itemsReceived);

                // Save the data for this device.
                await Task.Run(() => db.SaveChanges()).ConfigureAwait(false);

                foreach (var entity in added)
                {
                    var entry = db.Entry(entity);
                    if (entry.State != EntityState.Detached)
                    {
                        entry.State = EntityState.Detached;
                    }
                }

                // Move onto next device.
            }

            return(string.IsNullOrWhiteSpace(errors) ? null : errors);
        }