├── Images └── sync.PNG ├── tests └── Tests │ ├── Data │ ├── phys.json │ └── food.json │ ├── Properties │ ├── serviceDependencies.json │ ├── serviceDependencies.local.json │ └── launchSettings.json │ ├── appsettings.json │ ├── Tests.csproj │ └── BasicTest.cs ├── Using Tidepool Data APIs (Read Only).pdf ├── src ├── Core │ ├── Model │ │ ├── Nightscout │ │ │ ├── Settings.cs │ │ │ ├── Status.cs │ │ │ ├── Sen.cs │ │ │ ├── Basal.cs │ │ │ ├── Target.cs │ │ │ ├── Carbratio.cs │ │ │ ├── Profile.cs │ │ │ ├── ProfileInfo.cs │ │ │ └── Treatment.cs │ │ └── Tidepool │ │ │ ├── Nutrition.cs │ │ │ ├── DataType.cs │ │ │ ├── Unit.cs │ │ │ ├── Energy.cs │ │ │ ├── CarbRatio.cs │ │ │ ├── Distance.cs │ │ │ ├── Duration.cs │ │ │ ├── BasalSchedule.cs │ │ │ ├── Carbohydrate.cs │ │ │ ├── GlucoseTarget.cs │ │ │ ├── InsulinSensitivity.cs │ │ │ ├── Food.cs │ │ │ ├── AuthResponse.cs │ │ │ ├── PhysicalActivity.cs │ │ │ ├── BgValue.cs │ │ │ ├── Bolus.cs │ │ │ └── PumpSettings.cs │ ├── Services │ │ ├── Tidepool │ │ │ ├── ITidepoolClientFactory.cs │ │ │ ├── TidepoolClientOptions.cs │ │ │ ├── ITidepoolClient.cs │ │ │ ├── TidepoolClientFactory.cs │ │ │ └── TidepoolClient.cs │ │ ├── Nightscout │ │ │ ├── NightscoutClientOptions.cs │ │ │ └── NightscoutClient.cs │ │ ├── TidepoolToNightScoutSyncerOptions.cs │ │ └── TidepoolToNightScoutSyncer.cs │ ├── Extensions │ │ ├── StringExtension.cs │ │ └── DependencyInjectionExtensions.cs │ └── Core.csproj └── CLI │ ├── appsettings.json │ ├── Sanitizer.cs │ ├── Program.cs │ └── CLI.csproj ├── .github ├── workflows │ └── worker.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .dockerignore ├── docker-compose.yml ├── .gitattributes ├── TidepoolToNightScoutSync.sln ├── README.md └── .gitignore /Images/sync.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skalahonza/TidepoolToNightScoutSync/HEAD/Images/sync.PNG -------------------------------------------------------------------------------- /tests/Tests/Data/phys.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skalahonza/TidepoolToNightScoutSync/HEAD/tests/Tests/Data/phys.json -------------------------------------------------------------------------------- /tests/Tests/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "secrets1": { 4 | "type": "secrets" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /Using Tidepool Data APIs (Read Only).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skalahonza/TidepoolToNightScoutSync/HEAD/Using Tidepool Data APIs (Read Only).pdf -------------------------------------------------------------------------------- /tests/Tests/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "secrets1": { 4 | "type": "secrets.user" 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/Core/Model/Nightscout/Settings.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Nightscout; 4 | 5 | public class Settings 6 | { 7 | [JsonProperty("units")] public string? Units { get; set; } 8 | } -------------------------------------------------------------------------------- /src/Core/Model/Nightscout/Status.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Nightscout; 4 | 5 | public class Status 6 | { 7 | [JsonProperty("settings")] public Settings? Settings { get; set; } 8 | } -------------------------------------------------------------------------------- /tests/Tests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "TidepoolToNightScoutSync.Tests": { 4 | "commandName": "Project" 5 | }, 6 | "Docker": { 7 | "commandName": "Docker" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/Nutrition.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 4 | { 5 | public class Nutrition 6 | { 7 | [JsonProperty("carbohydrate")] public Carbohydrate Carbohydrate { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Core/Services/Tidepool/ITidepoolClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Services.Tidepool 4 | { 5 | public interface ITidepoolClientFactory 6 | { 7 | Task CreateAsync(); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Core/Services/Nightscout/NightscoutClientOptions.cs: -------------------------------------------------------------------------------- 1 | namespace TidepoolToNightScoutSync.Core.Services.Nightscout 2 | { 3 | public class NightscoutClientOptions 4 | { 5 | public string BaseUrl { get; set; } 6 | public string ApiKey { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/DataType.cs: -------------------------------------------------------------------------------- 1 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 2 | { 3 | public enum DataType 4 | { 5 | Bolus, 6 | Food, 7 | PhysicalActivity, 8 | PumpSettings, 9 | Cbg, 10 | Smbg 11 | } 12 | } -------------------------------------------------------------------------------- /.github/workflows/worker.yml: -------------------------------------------------------------------------------- 1 | name: Synchronization worker 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | 14 | - name: Say Hello 15 | run: echo "Hello, World!" 16 | -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/Unit.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 4 | { 5 | public class Unit 6 | { 7 | [JsonProperty("bg")] public string? Bg { get; set; } 8 | 9 | [JsonProperty("carb")] public string? Carb { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/Energy.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 4 | { 5 | public class Energy 6 | { 7 | [JsonProperty("units")] public string Units { get; set; } 8 | 9 | [JsonProperty("value")] public double Value { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/CarbRatio.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 4 | { 5 | public class CarbRatio 6 | { 7 | [JsonProperty("amount")] public double Amount { get; set; } 8 | 9 | [JsonProperty("start")] public int Start { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/Distance.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 4 | { 5 | public class Distance 6 | { 7 | [JsonProperty("units")] public string Units { get; set; } 8 | 9 | [JsonProperty("value")] public double Value { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/Duration.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 4 | { 5 | public class Duration 6 | { 7 | [JsonProperty("units")] public string Units { get; set; } 8 | 9 | [JsonProperty("value")] public double Value { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/BasalSchedule.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 4 | { 5 | public class BasalSchedule 6 | { 7 | [JsonProperty("rate")] public double Rate { get; set; } 8 | 9 | [JsonProperty("start")] public int Start { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/Carbohydrate.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 4 | { 5 | public class Carbohydrate 6 | { 7 | [JsonProperty("net")] public double? Net { get; set; } 8 | 9 | [JsonProperty("units")] public string? Units { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/GlucoseTarget.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 4 | { 5 | public class GlucoseTarget 6 | { 7 | [JsonProperty("start")] public int Start { get; set; } 8 | 9 | [JsonProperty("target")] public double Target { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/InsulinSensitivity.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 4 | { 5 | public class InsulinSensitivity 6 | { 7 | [JsonProperty("amount")] public double Amount { get; set; } 8 | 9 | [JsonProperty("start")] public int Start { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Services/TidepoolToNightScoutSyncerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Services 4 | { 5 | public class TidepoolToNightScoutSyncerOptions 6 | { 7 | public DateTime? Since { get; set; } 8 | public DateTime? Till { get; set; } 9 | public double TargetLow { get; set; } = 3.7; 10 | } 11 | } -------------------------------------------------------------------------------- /src/Core/Extensions/StringExtension.cs: -------------------------------------------------------------------------------- 1 | namespace TidepoolToNightScoutSync.Core.Extensions 2 | { 3 | public static class StringExtension 4 | { 5 | public static string ToCamelCase(this string str) => 6 | string.IsNullOrEmpty(str) || str.Length < 2 7 | ? str 8 | : char.ToLowerInvariant(str[0]) + str.Substring(1); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Core/Services/Tidepool/TidepoolClientOptions.cs: -------------------------------------------------------------------------------- 1 | namespace TidepoolToNightScoutSync.Core.Services.Tidepool 2 | { 3 | public class TidepoolClientOptions 4 | { 5 | public string BaseUrl { get; set; } = "https://api.tidepool.org"; 6 | public string UserId { get; set; } 7 | public string Username { get; set; } 8 | public string Password { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Core/Model/Nightscout/Sen.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Nightscout 4 | { 5 | public class Sen 6 | { 7 | [JsonProperty("time")] public string? Time { get; set; } 8 | 9 | [JsonProperty("value")] public string? Value { get; set; } 10 | 11 | [JsonProperty("timeAsSeconds")] public string? TimeAsSeconds { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Core/Model/Nightscout/Basal.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Nightscout 4 | { 5 | public class Basal 6 | { 7 | [JsonProperty("time")] public string? Time { get; set; } 8 | 9 | [JsonProperty("value")] public string? Value { get; set; } 10 | 11 | [JsonProperty("timeAsSeconds")] public string? TimeAsSeconds { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Core/Model/Nightscout/Target.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Nightscout 4 | { 5 | public class Target 6 | { 7 | [JsonProperty("time")] public string? Time { get; set; } 8 | 9 | [JsonProperty("value")] public string? Value { get; set; } 10 | 11 | [JsonProperty("timeAsSeconds")] public string? TimeAsSeconds { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Core/Model/Nightscout/Carbratio.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace TidepoolToNightScoutSync.Core.Model.Nightscout 4 | { 5 | public class Carbratio 6 | { 7 | [JsonProperty("time")] public string? Time { get; set; } 8 | 9 | [JsonProperty("value")] public string? Value { get; set; } 10 | 11 | [JsonProperty("timeAsSeconds")] public string? TimeAsSeconds { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/Food.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 5 | { 6 | public class Food 7 | { 8 | [JsonProperty("id")] public string? Id { get; set; } 9 | 10 | [JsonProperty("nutrition")] public Nutrition? Nutrition { get; set; } 11 | 12 | [JsonProperty("time")] public DateTime? Time { get; set; } 13 | 14 | [JsonProperty("uploadId")] public string? UploadId { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/AuthResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 6 | { 7 | public class AuthResponse 8 | { 9 | [JsonProperty("emailVerified")] public bool EmailVerified { get; set; } 10 | 11 | [JsonProperty("emails")] public List Emails { get; set; } 12 | 13 | [JsonProperty("termsAccepted")] public DateTime TermsAccepted { get; set; } 14 | 15 | [JsonProperty("userid")] public string Userid { get; set; } 16 | 17 | [JsonProperty("username")] public string Username { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: skalahonza 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /tests/Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information" 5 | } 6 | }, 7 | "Serilog": { 8 | "Using": [ 9 | "Serilog.Sinks.Console", 10 | "Serilog.Sinks.RollingFile" 11 | ], 12 | "WriteTo": [ 13 | { 14 | "Name": "Console" 15 | }, 16 | { 17 | "Name": "RollingFile", 18 | "Args": { 19 | "pathFormat": "Logs/{Date}.log" 20 | } 21 | } 22 | ] 23 | }, 24 | "tidepool:Username": "tidepool@username.com", 25 | "tidepool:Password": "password", 26 | "nightscout:BaseUrl": "http://localhost:5001", 27 | "nightscout:ApiKey": "123456789abc", 28 | "sync:since": null, 29 | "sync:till": null 30 | } 31 | -------------------------------------------------------------------------------- /src/Core/Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | TidepoolToNightScoutSync.Core 7 | TidepoolToNightScoutSync.Core 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/PhysicalActivity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 5 | { 6 | public class PhysicalActivity 7 | { 8 | [JsonProperty("distance")] public Distance? Distance { get; set; } 9 | 10 | [JsonProperty("duration")] public Duration? Duration { get; set; } 11 | 12 | [JsonProperty("energy")] public Energy? Energy { get; set; } 13 | 14 | [JsonProperty("id")] public string? Id { get; set; } 15 | 16 | [JsonProperty("name")] public string? Name { get; set; } 17 | 18 | [JsonProperty("time")] public DateTime? Time { get; set; } 19 | 20 | [JsonProperty("uploadId")] public string? UploadId { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/CLI/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information" 5 | } 6 | }, 7 | "Serilog": { 8 | "Using": [ 9 | "Serilog.Sinks.Console", 10 | "Serilog.Sinks.RollingFile" 11 | ], 12 | "WriteTo": [ 13 | { 14 | "Name": "Console" 15 | }, 16 | { 17 | "Name": "RollingFile", 18 | "Args": { 19 | "pathFormat": "Logs/{Date}.log" 20 | } 21 | } 22 | ] 23 | }, 24 | "tidepool:Username": "tidepool@username.com", 25 | "tidepool:Password": "password", 26 | "nightscout:BaseUrl": "https://[name of app].herokuapp.com", 27 | "nightscout:ApiKey": "nightscout secret with at least careportal role", 28 | "sync:since": null, 29 | "sync:till": null 30 | } 31 | -------------------------------------------------------------------------------- /src/Core/Services/Tidepool/ITidepoolClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using TidepoolToNightScoutSync.Core.Model.Tidepool; 5 | 6 | namespace TidepoolToNightScoutSync.Core.Services.Tidepool 7 | { 8 | public interface ITidepoolClient 9 | { 10 | Task> GetBolusAsync(DateTime? start = null, DateTime? end = null); 11 | Task> GetFoodAsync(DateTime? start = null, DateTime? end = null); 12 | Task> GetPhysicalActivityAsync(DateTime? start = null, DateTime? end = null); 13 | Task> GetPumpSettingsAsync(DateTime? start = null, DateTime? end = null); 14 | Task> GetBgValues(DateTime? start = null, DateTime? end = null); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Core/Model/Nightscout/Profile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace TidepoolToNightScoutSync.Core.Model.Nightscout 6 | { 7 | public class Profile 8 | { 9 | [JsonProperty("_id")] public string? Id { get; set; } 10 | 11 | [JsonProperty("defaultProfile")] public string? DefaultProfile { get; set; } 12 | 13 | [JsonProperty("store")] 14 | public Dictionary Store { get; set; } = new Dictionary(); 15 | 16 | [JsonProperty("startDate")] public DateTime? StartDate { get; set; } 17 | 18 | [JsonProperty("mills")] public string? Mills { get; set; } 19 | 20 | [JsonProperty("units")] public string? Units { get; set; } 21 | 22 | [JsonProperty("created_at")] public DateTime? CreatedAt { get; set; } 23 | } 24 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Actual behavior** 24 | A clear and concise description of what happened.. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: [e.g. Windows 10] 31 | - Version of the app [e.g. 0.9] 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/BgValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 5 | { 6 | public class BgValue 7 | { 8 | [JsonProperty("clockDriftOffset")] public int? ClockDriftOffset { get; set; } 9 | 10 | [JsonProperty("conversionOffset")] public int? ConversionOffset { get; set; } 11 | 12 | [JsonProperty("deviceId")] public string DeviceId { get; set; } 13 | 14 | [JsonProperty("deviceTime")] public DateTime? DeviceTime { get; set; } 15 | 16 | [JsonProperty("guid")] public string Guid { get; set; } 17 | 18 | [JsonProperty("id")] public string Id { get; set; } 19 | 20 | [JsonProperty("time")] public DateTime? Time { get; set; } 21 | 22 | [JsonProperty("units")] public string Units { get; set; } 23 | 24 | [JsonProperty("value")] public double Value { get; set; } 25 | 26 | [JsonProperty("uploadId")] public string? UploadId { get; set; } 27 | } 28 | } -------------------------------------------------------------------------------- /src/CLI/Sanitizer.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | using Serilog.Core; 3 | using Serilog.Events; 4 | 5 | namespace TidepoolToNightScoutSync.CLI 6 | { 7 | public class Sanitizer : ILogEventEnricher 8 | { 9 | public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) 10 | { 11 | if (logEvent.Properties.TryGetValue("Uri", out var value)) 12 | { 13 | var builder = new UriBuilder(value.ToString()); 14 | var parameters = HttpUtility.ParseQueryString(builder.Query); 15 | 16 | // sanitize token query parameter 17 | if (!string.IsNullOrEmpty(parameters.Get("token"))) 18 | { 19 | parameters.Set("token", "*****"); 20 | builder.Query = parameters.ToString(); 21 | logEvent.AddOrUpdateProperty(new LogEventProperty("Uri", new ScalarValue(builder.ToString()))); 22 | } 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/Bolus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 5 | { 6 | public class Bolus 7 | { 8 | [JsonProperty("id")] public string? Id { get; set; } 9 | 10 | [JsonProperty("normal")] public double? Normal { get; set; } 11 | 12 | [JsonProperty("extended")] public double? Extended { get; set; } 13 | 14 | /// 15 | /// Duration in milliseconds. 16 | /// 17 | [JsonProperty("duration")] 18 | public long? DurationMs { get; set; } 19 | 20 | [JsonProperty("subType")] public string? SubType { get; set; } 21 | 22 | [JsonProperty("time")] public DateTime? Time { get; set; } 23 | 24 | [JsonProperty("uploadId")] public string? UploadId { get; set; } 25 | 26 | public TimeSpan? Duration => DurationMs.HasValue 27 | ? TimeSpan.FromMilliseconds(DurationMs.Value) 28 | : default(TimeSpan?); 29 | } 30 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | nightscout: 3 | image: "nightscout/cgm-remote-monitor:15.0.2" 4 | environment: 5 | API_SECRET: "${API_SECRET:-123456789abc}" 6 | MONGO_CONNECTION: 'mongodb://${MONGODB_USERNAME:-nightscout}:${MONGODB_PASSWORD:-secret}@database/nightscout?retryWrites=true&w=majority&authSource=admin' 7 | MONGO_COLLECTION: 'entries' 8 | INSECURE_USE_HTTP: "true" 9 | SECURE_HSTS_HEADER: "false" 10 | ENABLE: "basal bwp careportal iob cob rawbg treatmentnotify boluscalc profile food ar2" 11 | SHOW_PLUGINS: "careportal" 12 | TREATMENTS_AUTH: "off" 13 | PORT: 1337 14 | TIME_FORMAT: "24" 15 | DISPLAY_UNITS: "mmol" 16 | WAIT_HOSTS: "database:27017" 17 | depends_on: 18 | - database 19 | expose: 20 | - "1337" 21 | ports: 22 | - "5001:1337" 23 | 24 | database: 25 | image: mongo:latest 26 | environment: 27 | MONGO_INITDB_DATABASE: nightscout 28 | MONGO_INITDB_ROOT_USERNAME: ${MONGODB_USERNAME:-nightscout} 29 | MONGO_INITDB_ROOT_PASSWORD: ${MONGODB_PASSWORD:-secret} 30 | AUTH: 'true' 31 | expose: 32 | - 27017 33 | - 27018 34 | - 27019 35 | -------------------------------------------------------------------------------- /src/Core/Model/Nightscout/ProfileInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace TidepoolToNightScoutSync.Core.Model.Nightscout 6 | { 7 | public class ProfileInfo 8 | { 9 | [JsonProperty("dia")] public string? Dia { get; set; } 10 | 11 | [JsonProperty("carbratio")] public List Carbratio { get; } = new List(); 12 | 13 | [JsonProperty("carbs_hr")] public string? CarbsHr { get; set; } 14 | 15 | [JsonProperty("delay")] public string? Delay { get; set; } 16 | 17 | [JsonProperty("sens")] public List Sens { get; } = new List(); 18 | 19 | [JsonProperty("timezone")] public string? Timezone { get; set; } 20 | 21 | [JsonProperty("basal")] public List Basal { get; } = new List(); 22 | 23 | [JsonProperty("target_low")] public List TargetLow { get; } = new List(); 24 | 25 | [JsonProperty("target_high")] public List TargetHigh { get; } = new List(); 26 | 27 | [JsonProperty("startDate")] public DateTime? StartDate { get; set; } 28 | 29 | [JsonProperty("units")] public string? Units { get; set; } 30 | } 31 | } -------------------------------------------------------------------------------- /src/Core/Model/Nightscout/Treatment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace TidepoolToNightScoutSync.Core.Model.Nightscout 5 | { 6 | public class Treatment 7 | { 8 | [JsonProperty("_id")] public string? Id { get; set; } 9 | 10 | [JsonProperty("eventType")] public string? EventType { get; set; } 11 | 12 | [JsonProperty("created_at")] public DateTime? CreatedAt { get; set; } 13 | 14 | [JsonProperty("glucose")] public string? Glucose { get; set; } 15 | 16 | [JsonProperty("glucoseType")] public string? GlucoseType { get; set; } 17 | 18 | [JsonProperty("carbs")] public double? Carbs { get; set; } 19 | 20 | [JsonProperty("protein")] public double? Protein { get; set; } 21 | 22 | [JsonProperty("fat")] public double? Fat { get; set; } 23 | 24 | [JsonProperty("insulin")] public double? Insulin { get; set; } 25 | 26 | [JsonProperty("relative")] public double? Relative { get; set; } 27 | 28 | [JsonProperty("units")] public string Units { get; set; } = "mmol"; 29 | 30 | [JsonProperty("notes")] public string? Notes { get; set; } 31 | 32 | [JsonProperty("enteredBy")] public string? EnteredBy { get; set; } 33 | 34 | [JsonProperty("duration")] public double? Duration { get; set; } 35 | } 36 | } -------------------------------------------------------------------------------- /src/CLI/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Serilog; 4 | using TidepoolToNightScoutSync.CLI; 5 | using TidepoolToNightScoutSync.Core.Extensions; 6 | using TidepoolToNightScoutSync.Core.Services; 7 | 8 | var configuration = new ConfigurationBuilder() 9 | .AddJsonFile("appsettings.json") 10 | .AddEnvironmentVariables() 11 | .AddUserSecrets() 12 | .AddCommandLine(args) 13 | .Build(); 14 | 15 | //logging 16 | Log.Logger = new LoggerConfiguration() 17 | .ReadFrom.Configuration(configuration) 18 | .Enrich.With() 19 | .CreateLogger(); 20 | 21 | // dependency injection 22 | var services = new ServiceCollection() 23 | .AddSingleton(configuration) 24 | .AddTidepoolClient((settings, configuration) => 25 | ConfigurationBinder.Bind(configuration.GetSection("tidepool"), settings)) 26 | .AddNightscoutClient((settings, configuration) => configuration.GetSection("nightscout").Bind(settings)) 27 | .AddTidepoolToNightScoutSyncer((settings, configuration) => 28 | configuration.GetSection("sync").Bind(settings)) 29 | .AddLogging(x => x.AddSerilog()) 30 | .BuildServiceProvider(); 31 | 32 | var syncer = services.GetRequiredService(); 33 | await syncer.SyncProfiles(); 34 | await syncer.SyncAsync(); -------------------------------------------------------------------------------- /src/CLI/CLI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | Nullable 9 | 98dacf2e-f0b2-4606-82ae-2b92356fd6fc 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | PreserveNewest 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Core/Model/Tidepool/PumpSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace TidepoolToNightScoutSync.Core.Model.Tidepool 6 | { 7 | public class PumpSettings 8 | { 9 | [JsonProperty("activeSchedule")] public string? ActiveSchedule { get; set; } 10 | 11 | [JsonProperty("automatedDelivery")] public bool AutomatedDelivery { get; set; } 12 | 13 | [JsonProperty("deviceTime")] public DateTime? DeviceTime { get; set; } 14 | 15 | [JsonProperty("basalSchedules")] 16 | public IReadOnlyDictionary> BasalSchedules { get; set; } = 17 | new Dictionary>(); 18 | 19 | [JsonProperty("bgTargets")] 20 | public IReadOnlyDictionary> BgTargets { get; set; } = 21 | new Dictionary>(); 22 | 23 | [JsonProperty("carbRatios")] 24 | public IReadOnlyDictionary> CarbRatios { get; set; } = 25 | new Dictionary>(); 26 | 27 | [JsonProperty("insulinSensitivities")] 28 | public IReadOnlyDictionary> InsulinSensitivities { get; set; } = 29 | new Dictionary>(); 30 | 31 | [JsonProperty("units")] public Unit Units { get; set; } = new Unit(); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Core/Services/Tidepool/TidepoolClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Options; 6 | using Pathoschild.Http.Client; 7 | using TidepoolToNightScoutSync.Core.Model.Tidepool; 8 | 9 | namespace TidepoolToNightScoutSync.Core.Services.Tidepool 10 | { 11 | public class TidepoolClientFactory : ITidepoolClientFactory 12 | { 13 | private readonly IClient _client; 14 | private readonly TidepoolClientOptions _options; 15 | 16 | public TidepoolClientFactory(IOptions options, HttpClient client) 17 | { 18 | _options = options.Value; 19 | _client = new FluentClient(new Uri(_options.BaseUrl), client); 20 | } 21 | 22 | private async Task AuthorizeAsync() 23 | { 24 | var response = await _client 25 | .PostAsync("auth/login") 26 | .WithBasicAuthentication(_options.Username, _options.Password) 27 | .AsResponse(); 28 | 29 | var token = response.Message.Headers.GetValues("x-tidepool-session-token").Single(); 30 | _client.AddDefault(x => x.WithHeader("x-tidepool-session-token", token)); 31 | 32 | var authResponse = await response.As(); 33 | 34 | if (string.IsNullOrEmpty(_options.UserId)) 35 | _options.UserId = authResponse.Userid; 36 | 37 | return new TidepoolClient(_client, _options); 38 | } 39 | 40 | public Task CreateAsync() => 41 | AuthorizeAsync(); 42 | } 43 | } -------------------------------------------------------------------------------- /src/Core/Extensions/DependencyInjectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using TidepoolToNightScoutSync.Core.Services; 5 | using TidepoolToNightScoutSync.Core.Services.Nightscout; 6 | using TidepoolToNightScoutSync.Core.Services.Tidepool; 7 | 8 | namespace TidepoolToNightScoutSync.Core.Extensions 9 | { 10 | public static class DependencyInjectionExtensions 11 | { 12 | public static IServiceCollection AddTidepoolToNightScoutSyncer(this IServiceCollection services, 13 | Action configureOptions) 14 | { 15 | services.AddOptions().Configure(configureOptions); 16 | return services.AddSingleton(); 17 | } 18 | 19 | public static IServiceCollection AddNightscoutClient(this IServiceCollection services, 20 | Action configureOptions) 21 | { 22 | services.AddHttpClient(); 23 | services.AddOptions().Configure(configureOptions); 24 | return services; 25 | } 26 | 27 | public static IServiceCollection AddTidepoolClient(this IServiceCollection services, 28 | Action configureOptions) 29 | { 30 | services.AddHttpClient(); 31 | services.AddOptions().Configure(configureOptions); 32 | return services; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /tests/Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | false 6 | Linux 7 | 96b39b78-2766-4663-8618-9c0b77a8eafc 8 | TidepoolToNightScoutSync.Tests 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | PreserveNewest 36 | 37 | 38 | PreserveNewest 39 | 40 | 41 | PreserveNewest 42 | 43 | 44 | PreserveNewest 45 | 46 | 47 | PreserveNewest 48 | 49 | 50 | PreserveNewest 51 | 52 | 53 | PreserveNewest 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /TidepoolToNightScoutSync.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30309.148 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5F9D776E-345A-463E-8541-6097709D39CF}" 7 | ProjectSection(SolutionItems) = preProject 8 | docker-compose.yml = docker-compose.yml 9 | README.md = README.md 10 | EndProjectSection 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A6978862-3D57-4B78-A0AE-A58245C4968D}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B6393862-7D87-4D86-B5C3-360B2115EFD6}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "tests\Tests\Tests.csproj", "{5C36E4D5-61B8-4CC8-A596-C85F6538E0B1}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "src\Core\Core.csproj", "{2E027FFE-1349-48F9-8C83-74447C3CF5D3}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CLI", "src\CLI\CLI.csproj", "{22C0860A-35C5-457E-AA7A-16A1CDD1E4CA}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {5C36E4D5-61B8-4CC8-A596-C85F6538E0B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {5C36E4D5-61B8-4CC8-A596-C85F6538E0B1}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {5C36E4D5-61B8-4CC8-A596-C85F6538E0B1}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {5C36E4D5-61B8-4CC8-A596-C85F6538E0B1}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {2E027FFE-1349-48F9-8C83-74447C3CF5D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {2E027FFE-1349-48F9-8C83-74447C3CF5D3}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {2E027FFE-1349-48F9-8C83-74447C3CF5D3}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {2E027FFE-1349-48F9-8C83-74447C3CF5D3}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {22C0860A-35C5-457E-AA7A-16A1CDD1E4CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {22C0860A-35C5-457E-AA7A-16A1CDD1E4CA}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {22C0860A-35C5-457E-AA7A-16A1CDD1E4CA}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {22C0860A-35C5-457E-AA7A-16A1CDD1E4CA}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | GlobalSection(ExtensibilityGlobals) = postSolution 45 | SolutionGuid = {391D187F-D8D7-4352-9AEF-A4E9B3A028D4} 46 | EndGlobalSection 47 | GlobalSection(NestedProjects) = preSolution 48 | {5C36E4D5-61B8-4CC8-A596-C85F6538E0B1} = {B6393862-7D87-4D86-B5C3-360B2115EFD6} 49 | {2E027FFE-1349-48F9-8C83-74447C3CF5D3} = {A6978862-3D57-4B78-A0AE-A58245C4968D} 50 | {22C0860A-35C5-457E-AA7A-16A1CDD1E4CA} = {A6978862-3D57-4B78-A0AE-A58245C4968D} 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /src/Core/Services/Nightscout/NightscoutClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.Options; 9 | using Pathoschild.Http.Client; 10 | using TidepoolToNightScoutSync.Core.Model.Nightscout; 11 | 12 | namespace TidepoolToNightScoutSync.Core.Services.Nightscout 13 | { 14 | public class NightscoutClient 15 | { 16 | private readonly IClient _client; 17 | private readonly NightscoutClientOptions _options; 18 | 19 | public NightscoutClient(IOptions options, HttpClient client) 20 | { 21 | _options = options.Value; 22 | _client = new FluentClient(new Uri(_options.BaseUrl), client); 23 | _client.AddDefault(x => x.WithArgument("token", _options.ApiKey)); 24 | _client.Formatters.JsonFormatter.SerializerSettings.NullValueHandling = 25 | Newtonsoft.Json.NullValueHandling.Ignore; 26 | } 27 | 28 | private static string SHA1(in string input) 29 | { 30 | using var sha1 = new SHA1Managed(); 31 | var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(input)); 32 | return string.Concat(hash.Select(b => b.ToString("x2"))); 33 | } 34 | 35 | public async Task> GetProfiles() => 36 | await _client 37 | .GetAsync("api/v1/profile") 38 | .AsArray(); 39 | 40 | public async Task SetProfile(Profile profile) => 41 | await _client 42 | .PutAsync("api/v1/profile", profile) 43 | .WithHeader("api-secret", SHA1(_options.ApiKey)) 44 | .As(); 45 | 46 | public async Task> AddTreatmentsAsync(IEnumerable treatments) => 47 | await _client 48 | .PostAsync("api/v1/treatments", treatments) 49 | .WithHeader("api-secret", SHA1(_options.ApiKey)) 50 | .AsArray(); 51 | 52 | /// 53 | /// Get information about the Nightscout treatments. 54 | /// 55 | /// The query used to find entries, supports nested query syntax. Examples find[insulin][$gte]=3 find[carb][$gte]=100 find[eventType]=Correction+Bolus All find parameters are interpreted as strings. 56 | /// Number of entries to return. 57 | /// 58 | public async Task> GetTreatmentsAsync(string? find, int? count) => 59 | await _client 60 | .GetAsync("api/v1/treatments") 61 | .WithArgument("find", find) 62 | .WithArgument("count", count) 63 | .AsArray(); 64 | 65 | public async Task GetStatus() => 66 | await _client 67 | .GetAsync("api/v1/status.json") 68 | .As(); 69 | } 70 | } -------------------------------------------------------------------------------- /src/Core/Services/Tidepool/TidepoolClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Pathoschild.Http.Client; 5 | using TidepoolToNightScoutSync.Core.Extensions; 6 | using TidepoolToNightScoutSync.Core.Model.Tidepool; 7 | 8 | namespace TidepoolToNightScoutSync.Core.Services.Tidepool 9 | { 10 | public class TidepoolClient : ITidepoolClient 11 | { 12 | private readonly IClient _client; 13 | private readonly TidepoolClientOptions _options; 14 | 15 | internal TidepoolClient(IClient client, TidepoolClientOptions options) 16 | { 17 | _client = client; 18 | _options = options; 19 | } 20 | 21 | public async Task> GetBolusAsync(DateTime? start = null, DateTime? end = null) => 22 | await _client 23 | .GetAsync($"data/{_options.UserId}") 24 | .WithArgument("startDate", start?.ToUniversalTime().ToString("o")) 25 | .WithArgument("endDate", end?.ToUniversalTime().ToString("o")) 26 | .WithArgument("type", nameof(DataType.Bolus).ToCamelCase()) 27 | .AsArray(); 28 | 29 | public async Task> GetFoodAsync(DateTime? start = null, DateTime? end = null) => 30 | await _client 31 | .GetAsync($"data/{_options.UserId}") 32 | .WithArgument("startDate", start?.ToUniversalTime().ToString("o")) 33 | .WithArgument("endDate", end?.ToUniversalTime().ToString("o")) 34 | .WithArgument("type", nameof(DataType.Food).ToCamelCase()) 35 | .AsArray(); 36 | 37 | public async Task> GetPhysicalActivityAsync(DateTime? start = null, 38 | DateTime? end = null) => 39 | await _client 40 | .GetAsync($"data/{_options.UserId}") 41 | .WithArgument("startDate", start?.ToUniversalTime().ToString("o")) 42 | .WithArgument("endDate", end?.ToUniversalTime().ToString("o")) 43 | .WithArgument("type", nameof(DataType.PhysicalActivity).ToCamelCase()) 44 | .AsArray(); 45 | 46 | public async Task> GetPumpSettingsAsync(DateTime? start = null, 47 | DateTime? end = null) => 48 | await _client 49 | .GetAsync($"data/{_options.UserId}") 50 | .WithArgument("startDate", start?.ToUniversalTime().ToString("o")) 51 | .WithArgument("endDate", end?.ToUniversalTime().ToString("o")) 52 | .WithArgument("type", nameof(DataType.PumpSettings).ToCamelCase()) 53 | .AsArray(); 54 | 55 | public async Task> GetBgValues(DateTime? start = null, 56 | DateTime? end = null) 57 | { 58 | // https://tidepool.stoplight.io/docs/tidepool-api/47411eab004f3-common-fields 59 | // type 60 | // Allowed values: cbg, smbg 61 | var types = string.Join(',', nameof(DataType.Cbg), nameof(DataType.Smbg)); 62 | return await _client 63 | .GetAsync($"data/{_options.UserId}") 64 | .WithArgument("startDate", start?.ToUniversalTime().ToString("o")) 65 | .WithArgument("endDate", end?.ToUniversalTime().ToString("o")) 66 | .WithArgument("type", types.ToLower()) 67 | .AsArray(); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tidepool to NightScout sync tool 2 | ## Questions and answers 3 | 4 | If you have any question, or you need to help with getting started write a post 5 | here: https://github.com/skalahonza/TidepoolToNightScoutSync/discussions/categories/q-a. 6 | I know that writing an email might be faster, but GitHub Discussions are preferred for following reasons: 7 | 8 | * maybe someone already asked the same question 9 | * answers to questions can be found easier 10 | 11 | ## Description 12 | 13 | This tool helps to sync data from **Tidepool** to **NightScout**. 14 | 15 | ### Synced data 16 | 17 | - **Bolus** - Normal and Combo bolus 18 | - **Carbs** - Carbs intake 19 | - **Physical activity** - Physical activity 20 | - **Basal schedules** - Basal schedules 21 | - **BG targets** - BG targets 22 | - **BG values** - BG values 23 | - **Carb ratios** - Carb ratios 24 | - **Insulin sensitivities** - Insulin sensitivities 25 | ![Sync](Images/sync.PNG) 26 | 27 | ## What is Tidepool 28 | Tidepool is a nonprofit organization dedicated to making diabetes data more accessible, actionable, and meaningful for people with diabetes, their care teams, and researchers. 29 | [More](https://www.tidepool.org/) 30 | 31 | ## What is NightScout 32 | Nightscout (CGM in the Cloud) is an open source, DIY project that allows real time access to a CGM data via personal website, smartwatch viewers, or apps and widgets available for smartphones. 33 | 34 | Nightscout was developed by parents of children with Type 1 Diabetes and has continued to be developed, maintained, and supported by volunteers. 35 | [More](http://www.nightscout.info/) 36 | 37 | ## Build and run on your device 38 | 39 | 1. Install [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 40 | 2. Navigate to `src/CLI` folder 41 | 3. Open file `appsettings.json` 42 | 4. Change items below and fill in your user credentials 43 | * tidepool:Username = your tidepool username 44 | * tidepool:Password = your tidepool password 45 | * nightscout:BaseUrl = your NightScout url 46 | * nightscout:ApiKey = your NightScout API KEY 47 | * sync:since = since when the data should be imported (optional, can be null) 48 | * sync:till = till when the data should be imported (optional, can be null) 49 | ### Example configuration 50 | ```json 51 | { 52 | "tidepool:Username": "tidepool@username.com", 53 | "tidepool:Password": "password", 54 | "nightscout:BaseUrl": "https://skalich.herokuapp.com", 55 | "nightscout:ApiKey": "123456789101112", 56 | "sync:since": "2020-07-01", 57 | "sync:till": null 58 | } 59 | ``` 60 | 1. Open command prompt in the folder and run the app using `dotnet run` 61 | 2. You should now see your data in NightScout 62 | 63 | ## Run using only CMD 64 | Good for AdHoc syncing. 65 | 66 | 1. Navigate to `src/CLI` folder 67 | 2. Run command `dotnet run -- tidepool:Username="tidepool@username.com" tidepool:Password="password" nightscout:BaseUrl="https://skalich.herokuapp.com" nightscout:ApiKey="123456789101112" sync:since="2020-07-31"` 68 | * This will sync all date since 31st of July 2020 69 | * It will also use Tidepool username, password and NightScout base url, API Secret from the command 70 | 71 | ## Sync values from Today 72 | Good for AdHoc syncing. 73 | 74 | 1. Navigate to `src/CLI` folder 75 | 2. Run command `dotnet run -- tidepool:Username="tidepool@username.com" tidepool:Password="password" nightscout:BaseUrl="https://skalich.herokuapp.com" nightscout:ApiKey="123456789101112"` 76 | * This will sync all data from Today 77 | * Notice that **sync:since** parameter is missing 78 | 79 | ## Sync values from any date 80 | If you have filled your user credentials in `appsettings.json` you don't have to mention them when running the app using CMD. 81 | 82 | 1. Navigate to `src/CLI` folder 83 | 2. Run command `dotnet run -- sync:since="2020-07-31"` 84 | * This will sync all date since 31st of July 2020 85 | * Rest of the configuration is loaded from `appsettings.json` 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # Logs 7 | */Logs/ 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUNIT 46 | *.VisualState.xml 47 | TestResult.xml 48 | 49 | # Build Results of an ATL Project 50 | [Dd]ebugPS/ 51 | [Rr]eleasePS/ 52 | dlldata.c 53 | 54 | # Benchmark Results 55 | BenchmarkDotNet.Artifacts/ 56 | 57 | # .NET Core 58 | project.lock.json 59 | project.fragment.lock.json 60 | artifacts/ 61 | 62 | # StyleCop 63 | StyleCopReport.xml 64 | 65 | # Files built by Visual Studio 66 | *_i.c 67 | *_p.c 68 | *_h.h 69 | *.ilk 70 | *.meta 71 | *.obj 72 | *.iobj 73 | *.pch 74 | *.pdb 75 | *.ipdb 76 | *.pgc 77 | *.pgd 78 | *.rsp 79 | *.sbr 80 | *.tlb 81 | *.tli 82 | *.tlh 83 | *.tmp 84 | *.tmp_proj 85 | *_wpftmp.csproj 86 | *.log 87 | *.vspscc 88 | *.vssscc 89 | .builds 90 | *.pidb 91 | *.svclog 92 | *.scc 93 | 94 | # Chutzpah Test files 95 | _Chutzpah* 96 | 97 | # Visual C++ cache files 98 | ipch/ 99 | *.aps 100 | *.ncb 101 | *.opendb 102 | *.opensdf 103 | *.sdf 104 | *.cachefile 105 | *.VC.db 106 | *.VC.VC.opendb 107 | 108 | # Visual Studio profiler 109 | *.psess 110 | *.vsp 111 | *.vspx 112 | *.sap 113 | 114 | # Visual Studio Trace Files 115 | *.e2e 116 | 117 | # TFS 2012 Local Workspace 118 | $tf/ 119 | 120 | # Guidance Automation Toolkit 121 | *.gpState 122 | 123 | # ReSharper is a .NET coding add-in 124 | _ReSharper*/ 125 | *.[Rr]e[Ss]harper 126 | *.DotSettings.user 127 | 128 | # JustCode is a .NET coding add-in 129 | .JustCode 130 | 131 | # TeamCity is a build add-in 132 | _TeamCity* 133 | 134 | # DotCover is a Code Coverage Tool 135 | *.dotCover 136 | 137 | # AxoCover is a Code Coverage Tool 138 | .axoCover/* 139 | !.axoCover/settings.json 140 | 141 | # Visual Studio code coverage results 142 | *.coverage 143 | *.coveragexml 144 | 145 | # NCrunch 146 | _NCrunch_* 147 | .*crunch*.local.xml 148 | nCrunchTemp_* 149 | 150 | # MightyMoose 151 | *.mm.* 152 | AutoTest.Net/ 153 | 154 | # Web workbench (sass) 155 | .sass-cache/ 156 | 157 | # Installshield output folder 158 | [Ee]xpress/ 159 | 160 | # DocProject is a documentation generator add-in 161 | DocProject/buildhelp/ 162 | DocProject/Help/*.HxT 163 | DocProject/Help/*.HxC 164 | DocProject/Help/*.hhc 165 | DocProject/Help/*.hhk 166 | DocProject/Help/*.hhp 167 | DocProject/Help/Html2 168 | DocProject/Help/html 169 | 170 | # Click-Once directory 171 | publish/ 172 | 173 | # Publish Web Output 174 | *.[Pp]ublish.xml 175 | *.azurePubxml 176 | # Note: Comment the next line if you want to checkin your web deploy settings, 177 | # but database connection strings (with potential passwords) will be unencrypted 178 | *.pubxml 179 | *.publishproj 180 | 181 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 182 | # checkin your Azure Web App publish settings, but sensitive information contained 183 | # in these scripts will be unencrypted 184 | PublishScripts/ 185 | 186 | # NuGet Packages 187 | *.nupkg 188 | # The packages folder can be ignored because of Package Restore 189 | **/[Pp]ackages/* 190 | # except build/, which is used as an MSBuild target. 191 | !**/[Pp]ackages/build/ 192 | # Uncomment if necessary however generally it will be regenerated when needed 193 | #!**/[Pp]ackages/repositories.config 194 | # NuGet v3's project.json files produces more ignorable files 195 | *.nuget.props 196 | *.nuget.targets 197 | 198 | # Microsoft Azure Build Output 199 | csx/ 200 | *.build.csdef 201 | 202 | # Microsoft Azure Emulator 203 | ecf/ 204 | rcf/ 205 | 206 | # Windows Store app package directories and files 207 | AppPackages/ 208 | BundleArtifacts/ 209 | Package.StoreAssociation.xml 210 | _pkginfo.txt 211 | *.appx 212 | 213 | # Visual Studio cache files 214 | # files ending in .cache can be ignored 215 | *.[Cc]ache 216 | # but keep track of directories ending in .cache 217 | !?*.[Cc]ache/ 218 | 219 | # Others 220 | ClientBin/ 221 | ~$* 222 | *~ 223 | *.dbmdl 224 | *.dbproj.schemaview 225 | *.jfm 226 | *.pfx 227 | *.publishsettings 228 | orleans.codegen.cs 229 | 230 | # Including strong name files can present a security risk 231 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 232 | #*.snk 233 | 234 | # Since there are multiple workflows, uncomment next line to ignore bower_components 235 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 236 | #bower_components/ 237 | 238 | # RIA/Silverlight projects 239 | Generated_Code/ 240 | 241 | # Backup & report files from converting an old project file 242 | # to a newer Visual Studio version. Backup files are not needed, 243 | # because we have git ;-) 244 | _UpgradeReport_Files/ 245 | Backup*/ 246 | UpgradeLog*.XML 247 | UpgradeLog*.htm 248 | ServiceFabricBackup/ 249 | *.rptproj.bak 250 | 251 | # SQL Server files 252 | *.mdf 253 | *.ldf 254 | *.ndf 255 | 256 | # Business Intelligence projects 257 | *.rdl.data 258 | *.bim.layout 259 | *.bim_*.settings 260 | *.rptproj.rsuser 261 | *- Backup*.rdl 262 | 263 | # Microsoft Fakes 264 | FakesAssemblies/ 265 | 266 | # GhostDoc plugin setting file 267 | *.GhostDoc.xml 268 | 269 | # Node.js Tools for Visual Studio 270 | .ntvs_analysis.dat 271 | node_modules/ 272 | 273 | # Visual Studio 6 build log 274 | *.plg 275 | 276 | # Visual Studio 6 workspace options file 277 | *.opt 278 | 279 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 280 | *.vbw 281 | 282 | # Visual Studio LightSwitch build output 283 | **/*.HTMLClient/GeneratedArtifacts 284 | **/*.DesktopClient/GeneratedArtifacts 285 | **/*.DesktopClient/ModelManifest.xml 286 | **/*.Server/GeneratedArtifacts 287 | **/*.Server/ModelManifest.xml 288 | _Pvt_Extensions 289 | 290 | # Paket dependency manager 291 | .paket/paket.exe 292 | paket-files/ 293 | 294 | # FAKE - F# Make 295 | .fake/ 296 | 297 | # JetBrains Rider 298 | .idea/ 299 | *.sln.iml 300 | 301 | # CodeRush personal settings 302 | .cr/personal 303 | 304 | # Python Tools for Visual Studio (PTVS) 305 | __pycache__/ 306 | *.pyc 307 | 308 | # Cake - Uncomment if you are using it 309 | # tools/** 310 | # !tools/packages.config 311 | 312 | # Tabs Studio 313 | *.tss 314 | 315 | # Telerik's JustMock configuration file 316 | *.jmconfig 317 | 318 | # BizTalk build output 319 | *.btp.cs 320 | *.btm.cs 321 | *.odx.cs 322 | *.xsd.cs 323 | 324 | # OpenCover UI analysis results 325 | OpenCover/ 326 | 327 | # Azure Stream Analytics local run output 328 | ASALocalRun/ 329 | 330 | # MSBuild Binary and Structured Log 331 | *.binlog 332 | 333 | # NVidia Nsight GPU debugger configuration file 334 | *.nvuser 335 | 336 | # MFractors (Xamarin productivity tool) working folder 337 | .mfractor/ 338 | 339 | # Local History for Visual Studio 340 | .localhistory/ 341 | 342 | # BeatPulse healthcheck temp database 343 | healthchecksdb 344 | /TidepoolToNightScoutSync.API/Properties/serviceDependencies.skalich-tidepool-sync - Web Deploy.json 345 | -------------------------------------------------------------------------------- /tests/Tests/BasicTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using FluentAssertions; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Moq; 11 | using Newtonsoft.Json; 12 | using TidepoolToNightScoutSync.Core.Extensions; 13 | using TidepoolToNightScoutSync.Core.Model.Tidepool; 14 | using TidepoolToNightScoutSync.Core.Services; 15 | using TidepoolToNightScoutSync.Core.Services.Tidepool; 16 | using Xunit; 17 | 18 | namespace TidepoolToNightScoutSync.Tests 19 | { 20 | public class BasicTest 21 | { 22 | private readonly TidepoolToNightScoutSyncer _syncer; 23 | private readonly ITidepoolClient _tidepool; 24 | 25 | private static async Task FromFile(string path) => 26 | JsonConvert.DeserializeObject(await File.ReadAllTextAsync(path)); 27 | 28 | private ITidepoolClientFactory MockFactory() 29 | { 30 | var client = new Mock(); 31 | client.Setup(x => x.GetBolusAsync(It.IsAny(), It.IsAny())) 32 | .Returns(FromFile>("Data/bolus.json")); 33 | client.Setup(x => x.GetFoodAsync(It.IsAny(), It.IsAny())) 34 | .Returns(FromFile>("Data/food.json")); 35 | client.Setup(x => x.GetPhysicalActivityAsync(It.IsAny(), It.IsAny())) 36 | .Returns(FromFile>("Data/phys.json")); 37 | client.Setup(x => x.GetPumpSettingsAsync(It.IsAny(), It.IsAny())) 38 | .Returns(FromFile>("Data/pumpSettings.json")); 39 | client.Setup(x => x.GetBgValues(It.IsAny(), It.IsAny())) 40 | .Returns(FromFile>("Data/bgValues.json")); 41 | 42 | var factory = new Mock(); 43 | factory.Setup(x => x.CreateAsync()).Returns(Task.FromResult(client.Object)); 44 | return factory.Object; 45 | } 46 | 47 | public BasicTest() 48 | { 49 | var configuration = new ConfigurationBuilder() 50 | .AddJsonFile("appsettings.json") 51 | .AddEnvironmentVariables() 52 | .AddUserSecrets() 53 | .Build(); 54 | 55 | var services = new ServiceCollection() 56 | .AddSingleton(configuration) 57 | .AddSingleton(MockFactory()) 58 | .AddNightscoutClient((settings, configuration) => configuration.GetSection("nightscout").Bind(settings)) 59 | .AddTidepoolToNightScoutSyncer((settings, configuration) => configuration.Bind(settings)) 60 | .BuildServiceProvider(); 61 | 62 | _syncer = services.GetRequiredService(); 63 | _tidepool = services.GetRequiredService().CreateAsync().Result; 64 | } 65 | 66 | private bool AreEqualApproximately(double left, double right, double v) => 67 | Math.Abs(left - right) < v; 68 | 69 | [Fact] 70 | public async Task SyncProfiles() 71 | { 72 | // Arrange 73 | var nfi = new CultureInfo("en-US", false).NumberFormat; 74 | var settings = await _tidepool.GetPumpSettingsAsync(); 75 | var setting = settings.OrderByDescending(x => x.DeviceTime).FirstOrDefault(); 76 | 77 | // Act 78 | var profile = await _syncer.SyncProfiles(); 79 | 80 | // Assert 81 | profile.Store.Keys.Should().Contain(setting.BasalSchedules.Select(x => x.Key)); 82 | profile.Store.Keys.Should().Contain(setting.BgTargets.Select(x => x.Key)); 83 | profile.Store.Keys.Should().Contain(setting.CarbRatios.Select(x => x.Key)); 84 | profile.Store.Keys.Should().Contain(setting.InsulinSensitivities.Select(x => x.Key)); 85 | 86 | // basal 87 | foreach (var (name, schedule) in setting.BasalSchedules.Select(x => (x.Key, x.Value))) 88 | { 89 | var expectedBasal = schedule.Select(x => ((x.Start / 1000).ToString(nfi), x.Rate.ToString(nfi))); 90 | profile.Store[name].Basal 91 | .Select(x => (x.TimeAsSeconds, x.Value)) 92 | .Should().BeEquivalentTo(expectedBasal); 93 | } 94 | 95 | // bg targets 96 | foreach (var (name, targets) in setting.BgTargets.Select(x => (x.Key, x.Value))) 97 | { 98 | var expectedTimes = targets.Select(x => (x.Start / 1000).ToString(nfi)); 99 | var expectedTargets = targets.Select(x => x.Target); 100 | 101 | profile.Store[name].TargetLow.Select(x => x.TimeAsSeconds).Should() 102 | .BeEquivalentTo(profile.Store[name].TargetHigh.Select(x => x.TimeAsSeconds)); 103 | profile.Store[name].TargetLow.Select(x => x.Time).Should() 104 | .BeEquivalentTo(profile.Store[name].TargetHigh.Select(x => x.Time)); 105 | profile.Store[name].TargetLow.Select(x => x.TimeAsSeconds).Should().BeEquivalentTo(expectedTimes); 106 | 107 | 108 | // interval middle value whould be equal to Tidepool target 109 | profile.Store[name].TargetLow.Zip(profile.Store[name].TargetHigh) 110 | .Select(x => 111 | double.Parse(x.Second.Value, CultureInfo.InvariantCulture) - 112 | double.Parse(x.First.Value, CultureInfo.InvariantCulture)) 113 | .Should() 114 | .Equal(expectedTargets, (left, right) => AreEqualApproximately(left, right, 0.001)); 115 | } 116 | 117 | // carb ratios 118 | foreach (var (name, carbRatios) in setting.CarbRatios.Select(x => (x.Key, x.Value))) 119 | { 120 | var expectedRatios = carbRatios.Select(x => ((x.Start / 1000).ToString(nfi), x.Amount.ToString(nfi))); 121 | profile.Store[name].Carbratio 122 | .Select(x => (x.TimeAsSeconds, x.Value)) 123 | .Should().BeEquivalentTo(expectedRatios); 124 | } 125 | 126 | // insulin sensitivities 127 | foreach (var (name, sensitivities) in setting.InsulinSensitivities.Select(x => (x.Key, x.Value))) 128 | { 129 | var expectedSensitivities = 130 | sensitivities.Select(x => ((x.Start / 1000).ToString(nfi), x.Amount.ToString(nfi))); 131 | profile.Store[name].Sens 132 | .Select(x => (x.TimeAsSeconds, x.Value)) 133 | .Should().BeEquivalentTo(expectedSensitivities); 134 | } 135 | } 136 | 137 | [Fact] 138 | public async Task SyncAsync() 139 | { 140 | // Arrange 141 | var boluses = await _tidepool.GetBolusAsync(); 142 | var food = await _tidepool.GetFoodAsync(); 143 | var activities = await _tidepool.GetPhysicalActivityAsync(); 144 | var bgValues = await _tidepool.GetBgValues(); 145 | 146 | // Act 147 | var treatments = await _syncer.SyncAsync(); 148 | 149 | // Assert 150 | 151 | // Boluses 152 | foreach (var bolus in boluses) 153 | { 154 | treatments.Should().ContainSingle( 155 | t => t.Insulin == bolus.Normal && t.CreatedAt == bolus.Time, 156 | because: "Every bolus should be synced exactly once."); 157 | } 158 | 159 | // Food 160 | foreach (var item in food) 161 | { 162 | treatments.Should().ContainSingle( 163 | t => t.Carbs == item.Nutrition.Carbohydrate.Net && t.CreatedAt == item.Time, 164 | because: "Every food should be synced exactly once."); 165 | } 166 | 167 | // Activities 168 | foreach (var activity in activities) 169 | { 170 | treatments.Should().ContainSingle( 171 | t => t.Notes == activity.Name && t.Duration == activity.Duration.Value / 60 && 172 | t.CreatedAt == activity.Time, 173 | because: "Every activity should be synced exactly once."); 174 | } 175 | 176 | // BG Values 177 | foreach (var bgValue in bgValues) 178 | { 179 | treatments.Should().ContainSingle( 180 | t => t.Glucose == bgValue.Value.ToString(CultureInfo.InvariantCulture) && 181 | t.CreatedAt == bgValue.Time, 182 | because: "Every BG value should be synced exactly once."); 183 | } 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /src/Core/Services/TidepoolToNightScoutSyncer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Options; 7 | using TidepoolToNightScoutSync.Core.Model.Nightscout; 8 | using TidepoolToNightScoutSync.Core.Services.Nightscout; 9 | using TidepoolToNightScoutSync.Core.Services.Tidepool; 10 | 11 | namespace TidepoolToNightScoutSync.Core.Services; 12 | 13 | public class TidepoolToNightScoutSyncer( 14 | ITidepoolClientFactory factory, 15 | NightscoutClient nightscout, 16 | IOptions options) 17 | { 18 | private readonly TidepoolToNightScoutSyncerOptions _options = options.Value; 19 | private ITidepoolClient? _tidepool; 20 | 21 | public async Task SyncProfiles(DateTime? since = null, DateTime? till = null) 22 | { 23 | var nfi = new CultureInfo("en-US", false).NumberFormat; 24 | since ??= _options.Since ?? DateTime.Today; 25 | till ??= _options.Till; 26 | _tidepool ??= await factory.CreateAsync(); 27 | 28 | var settings = await _tidepool.GetPumpSettingsAsync(since, till); 29 | var setting = settings.MaxBy(x => x.DeviceTime); 30 | if (setting == null) return null; 31 | 32 | var profile = new Profile 33 | { 34 | DefaultProfile = setting.ActiveSchedule, 35 | StartDate = setting.DeviceTime, 36 | Units = setting.Units.Bg, 37 | Mills = new DateTimeOffset(setting.DeviceTime ?? DateTime.UtcNow).ToUnixTimeMilliseconds().ToString() 38 | }; 39 | 40 | // map basal schedules 41 | foreach (var (name, schedule) in setting.BasalSchedules.Select(x => (x.Key, x.Value))) 42 | { 43 | profile.Store.TryAdd(name, new ProfileInfo()); 44 | profile.Store[name].Basal.AddRange(schedule.Select(x => new Basal 45 | { 46 | Time = TimeSpan.FromSeconds(x.Start / 1000).ToString(@"hh\:mm"), 47 | TimeAsSeconds = (x.Start / 1000).ToString(), 48 | Value = x.Rate.ToString(nfi) 49 | })); 50 | } 51 | 52 | // map bg targets 53 | foreach (var (name, targets) in setting.BgTargets.Select(x => (x.Key, x.Value))) 54 | { 55 | profile.Store.TryAdd(name, new ProfileInfo()); 56 | foreach (var target in targets) 57 | { 58 | // convert from target glucose value to target glucose interval 59 | // e.g. 6,66089758925464 --> (3.7, 10.360897589254641) 60 | profile.Store[name].TargetLow.Add(new Target 61 | { 62 | Time = TimeSpan.FromSeconds(target.Start / 1000).ToString(@"hh\:mm"), 63 | TimeAsSeconds = (target.Start / 1000).ToString(), 64 | Value = _options.TargetLow.ToString(nfi), 65 | }); 66 | 67 | profile.Store[name].TargetHigh.Add(new Target 68 | { 69 | Time = TimeSpan.FromSeconds(target.Start / 1000).ToString(@"hh\:mm"), 70 | TimeAsSeconds = (target.Start / 1000).ToString(), 71 | Value = (_options.TargetLow + target.Target).ToString(nfi), 72 | }); 73 | } 74 | } 75 | 76 | // map carb ratios 77 | foreach (var (name, carbRatios) in setting.CarbRatios.Select(x => (x.Key, x.Value))) 78 | { 79 | profile.Store.TryAdd(name, new ProfileInfo()); 80 | profile.Store[name].Carbratio.AddRange(carbRatios.Select(x => new Carbratio 81 | { 82 | Time = TimeSpan.FromSeconds(x.Start / 1000).ToString(@"hh\:mm"), 83 | TimeAsSeconds = (x.Start / 1000).ToString(), 84 | Value = x.Amount.ToString(nfi) 85 | })); 86 | } 87 | 88 | // map insulin sensitivities 89 | foreach (var (name, sensitivities) in setting.InsulinSensitivities.Select(x => (x.Key, x.Value))) 90 | { 91 | profile.Store.TryAdd(name, new ProfileInfo()); 92 | profile.Store[name].Sens.AddRange(sensitivities.Select(x => new Sen 93 | { 94 | Time = TimeSpan.FromSeconds(x.Start / 1000).ToString(@"hh\:mm"), 95 | TimeAsSeconds = (x.Start / 1000).ToString(), 96 | Value = x.Amount.ToString(nfi) 97 | })); 98 | } 99 | 100 | // try to match on existing profile 101 | var profiles = await nightscout.GetProfiles(); 102 | profile.Id = profiles.FirstOrDefault(x => x.Mills == profile.Mills)?.Id; 103 | 104 | return await nightscout.SetProfile(profile); 105 | } 106 | 107 | public async Task> SyncAsync(DateTime? since = null, DateTime? till = null) 108 | { 109 | since ??= _options.Since ?? DateTime.Today; 110 | till ??= _options.Till; 111 | _tidepool ??= await factory.CreateAsync(); 112 | 113 | var status = await nightscout.GetStatus(); 114 | var nightScoutUnits = status.Settings?.Units ?? "mg/dl"; 115 | 116 | var boluses = (await _tidepool.GetBolusAsync(since, till)) 117 | .GroupBy(x => x.Time) 118 | .Select(x => x.First()) 119 | .ToDictionary(x => x.Time, x => x); 120 | 121 | var food = (await _tidepool.GetFoodAsync(since, till)) 122 | .GroupBy(x => x.Time) 123 | .Select(x => x.First()) 124 | .ToDictionary(x => x.Time, x => x); 125 | 126 | var activity = await _tidepool.GetPhysicalActivityAsync(since, till); 127 | 128 | var bgValues = await _tidepool.GetBgValues(since, till); 129 | 130 | var treatments = new Dictionary(); 131 | 132 | // standalone boluses and boluses with food 133 | foreach (var bolus in boluses.Values) 134 | { 135 | if (!bolus.Time.HasValue) 136 | { 137 | continue; 138 | } 139 | 140 | if (!treatments.TryGetValue(bolus.Time.Value, out var treatment)) 141 | { 142 | treatment = treatments[bolus.Time.Value] = new Treatment(); 143 | } 144 | 145 | treatment.Carbs = food.GetValueOrDefault(bolus.Time)?.Nutrition?.Carbohydrate?.Net; 146 | treatment.Insulin = bolus.Normal; 147 | treatment.Duration = bolus.Duration?.TotalMinutes; 148 | treatment.Relative = bolus.Extended; 149 | treatment.CreatedAt = bolus.Time; 150 | treatment.EnteredBy = "Tidepool"; 151 | } 152 | 153 | // food without boluses 154 | foreach (var item in food.Values) 155 | { 156 | if (!item.Time.HasValue) 157 | { 158 | continue; 159 | } 160 | 161 | if (!treatments.TryGetValue(item.Time.Value, out var treatment)) 162 | { 163 | treatment = treatments[item.Time.Value] = new Treatment(); 164 | } 165 | 166 | treatment.Carbs = item.Nutrition?.Carbohydrate?.Net; 167 | treatment.CreatedAt = item.Time; 168 | treatment.EnteredBy = "Tidepool"; 169 | } 170 | 171 | // physical activity 172 | foreach (var act in activity) 173 | { 174 | if (!act.Time.HasValue) 175 | { 176 | continue; 177 | } 178 | 179 | if (!treatments.TryGetValue(act.Time.Value, out var treatment)) 180 | { 181 | treatment = treatments[act.Time.Value] = new Treatment(); 182 | } 183 | 184 | treatment.Notes = act.Name; 185 | treatment.Duration = act.Duration?.Value / 60; 186 | treatment.EventType = "Exercise"; 187 | treatment.CreatedAt = act.Time; 188 | treatment.EnteredBy = "Tidepool"; 189 | } 190 | 191 | // bg values 192 | foreach (var bgValue in bgValues) 193 | { 194 | if (!bgValue.Time.HasValue) 195 | { 196 | continue; 197 | } 198 | 199 | if (!treatments.TryGetValue(bgValue.Time.Value, out var treatment)) 200 | { 201 | treatment = treatments[bgValue.Time.Value] = new Treatment(); 202 | } 203 | 204 | var glucose = ConvertBgValue(bgValue.Units, nightScoutUnits, bgValue.Value); 205 | treatment.Glucose = glucose.ToString(CultureInfo.InvariantCulture); 206 | treatment.Units = nightScoutUnits; 207 | treatment.CreatedAt = bgValue.Time; 208 | treatment.EnteredBy = "Tidepool"; 209 | } 210 | 211 | return await nightscout.AddTreatmentsAsync(treatments.Values); 212 | } 213 | 214 | private static double ConvertBgValue(string sourceUnit, string targetUnit, double value) 215 | { 216 | /* 217 | * https://tidepool.stoplight.io/docs/tidepool-api/7ca12b17275f0-units 218 | * The algorithm followed for conversion of blood glucose/related values from mg/dL to mmol/L is: 219 | * If units field is mg/dL divide the value by 18.01559 (the molar mass of glucose is 180.1559) 220 | * Store the resulting floating point precision value without rounding or truncation 221 | * The value has now been converted into mmol/L 222 | */ 223 | 224 | const double factor = 18.01559; 225 | var validUnits = new[] { "mmol/l", "mmol", "mg/dl" }.ToHashSet(StringComparer.OrdinalIgnoreCase); 226 | if (!validUnits.Contains(sourceUnit) || !validUnits.Contains(targetUnit)) 227 | { 228 | throw new NotSupportedException($"Conversion from {sourceUnit} to {targetUnit} is not supported."); 229 | } 230 | 231 | return (sourceUnit.ToLower(), targetUnit.ToLower()) switch 232 | { 233 | ("mmol/l" or "mmol", "mg/dl") => value * factor, 234 | ("mg/dl", "mmol/l" or "mmol") => value / factor, 235 | _ => value 236 | }; 237 | } 238 | } -------------------------------------------------------------------------------- /tests/Tests/Data/food.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "a3c03b820f6eb57ac8f48b4ebbac005e", 4 | "nutrition": { 5 | "carbohydrate": { 6 | "net": 60, 7 | "units": "grams" 8 | } 9 | }, 10 | "origin": { 11 | "id": "899B1738-ABF3-42AF-A1A1-6C4970A87159", 12 | "name": "com.apple.HealthKit", 13 | "payload": { 14 | "sourceRevision": { 15 | "operatingSystemVersion": "13.5.1", 16 | "productType": "iPhone9,3", 17 | "source": { 18 | "bundleIdentifier": "com.mysugr.companion.mySugr", 19 | "name": "mySugr" 20 | }, 21 | "version": "36760" 22 | } 23 | }, 24 | "type": "service" 25 | }, 26 | "payload": { 27 | "HKExternalUUID": "E110B10B-CCB1-4D18-A81F-E6B76A5D97E4" 28 | }, 29 | "time": "2020-08-08T17:37:40.089Z", 30 | "type": "food", 31 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 32 | }, 33 | { 34 | "id": "386b71170d5ff41810f4b1e4268bad77", 35 | "nutrition": { 36 | "carbohydrate": { 37 | "net": 26, 38 | "units": "grams" 39 | } 40 | }, 41 | "origin": { 42 | "id": "CDBE18E5-F030-4906-9ED5-258661D4DD91", 43 | "name": "com.apple.HealthKit", 44 | "payload": { 45 | "sourceRevision": { 46 | "operatingSystemVersion": "13.5.1", 47 | "productType": "iPhone9,3", 48 | "source": { 49 | "bundleIdentifier": "com.mysugr.companion.mySugr", 50 | "name": "mySugr" 51 | }, 52 | "version": "36760" 53 | } 54 | }, 55 | "type": "service" 56 | }, 57 | "payload": { 58 | "HKExternalUUID": "1C048E6B-03F1-43D8-B3F7-46345778E2C6" 59 | }, 60 | "time": "2020-08-08T16:38:25.000Z", 61 | "type": "food", 62 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 63 | }, 64 | { 65 | "id": "b490b71f3149800dc9e9086f6aa99454", 66 | "nutrition": { 67 | "carbohydrate": { 68 | "net": 24, 69 | "units": "grams" 70 | } 71 | }, 72 | "origin": { 73 | "id": "14A449FC-632C-4084-A103-B79CE0A94FC1", 74 | "name": "com.apple.HealthKit", 75 | "payload": { 76 | "sourceRevision": { 77 | "operatingSystemVersion": "13.5.1", 78 | "productType": "iPhone9,3", 79 | "source": { 80 | "bundleIdentifier": "com.mysugr.companion.mySugr", 81 | "name": "mySugr" 82 | }, 83 | "version": "36760" 84 | } 85 | }, 86 | "type": "service" 87 | }, 88 | "payload": { 89 | "HKExternalUUID": "F4A151F5-0DD0-4B35-8E56-B14290461D02" 90 | }, 91 | "time": "2020-08-08T13:19:24.990Z", 92 | "type": "food", 93 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 94 | }, 95 | { 96 | "id": "71a2a5d6666ecbb7630dfc98f0847622", 97 | "nutrition": { 98 | "carbohydrate": { 99 | "net": 70, 100 | "units": "grams" 101 | } 102 | }, 103 | "origin": { 104 | "id": "8CB815E7-AC24-442E-9F66-C2DDF6319C5C", 105 | "name": "com.apple.HealthKit", 106 | "payload": { 107 | "sourceRevision": { 108 | "operatingSystemVersion": "13.5.1", 109 | "productType": "iPhone9,3", 110 | "source": { 111 | "bundleIdentifier": "com.mysugr.companion.mySugr", 112 | "name": "mySugr" 113 | }, 114 | "version": "36760" 115 | } 116 | }, 117 | "type": "service" 118 | }, 119 | "payload": { 120 | "HKExternalUUID": "8167E180-C35C-4E72-8228-D7E40DEB8BE3" 121 | }, 122 | "time": "2020-08-08T10:00:16.352Z", 123 | "type": "food", 124 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 125 | }, 126 | { 127 | "id": "941b8d0ca75fbda3ff678c870164f83c", 128 | "nutrition": { 129 | "carbohydrate": { 130 | "net": 24, 131 | "units": "grams" 132 | } 133 | }, 134 | "origin": { 135 | "id": "973D51D4-8F64-4C92-AECE-F8F6664BBBD6", 136 | "name": "com.apple.HealthKit", 137 | "payload": { 138 | "sourceRevision": { 139 | "operatingSystemVersion": "13.5.1", 140 | "productType": "iPhone9,3", 141 | "source": { 142 | "bundleIdentifier": "com.mysugr.companion.mySugr", 143 | "name": "mySugr" 144 | }, 145 | "version": "36760" 146 | } 147 | }, 148 | "type": "service" 149 | }, 150 | "payload": { 151 | "HKExternalUUID": "F7437316-ACDF-4719-A35C-A517DDC1EDAB" 152 | }, 153 | "time": "2020-08-08T06:21:13.000Z", 154 | "type": "food", 155 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 156 | }, 157 | { 158 | "id": "17e67b62e115ff10d9ea11deb86c9376", 159 | "nutrition": { 160 | "carbohydrate": { 161 | "net": 24, 162 | "units": "grams" 163 | } 164 | }, 165 | "origin": { 166 | "id": "4A65C29E-D578-4327-8509-853584875783", 167 | "name": "com.apple.HealthKit", 168 | "payload": { 169 | "sourceRevision": { 170 | "operatingSystemVersion": "13.5.1", 171 | "productType": "iPhone9,3", 172 | "source": { 173 | "bundleIdentifier": "com.mysugr.companion.mySugr", 174 | "name": "mySugr" 175 | }, 176 | "version": "36760" 177 | } 178 | }, 179 | "type": "service" 180 | }, 181 | "payload": { 182 | "HKExternalUUID": "26B22313-8364-48B5-87E3-02A349ADC452" 183 | }, 184 | "time": "2020-08-07T20:23:05.880Z", 185 | "type": "food", 186 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 187 | }, 188 | { 189 | "id": "36826921df8dd9288c8c8cff584c56dd", 190 | "nutrition": { 191 | "carbohydrate": { 192 | "net": 48, 193 | "units": "grams" 194 | } 195 | }, 196 | "origin": { 197 | "id": "48534156-E377-409D-8B0C-7679151ACBDB", 198 | "name": "com.apple.HealthKit", 199 | "payload": { 200 | "sourceRevision": { 201 | "operatingSystemVersion": "13.5.1", 202 | "productType": "iPhone9,3", 203 | "source": { 204 | "bundleIdentifier": "com.mysugr.companion.mySugr", 205 | "name": "mySugr" 206 | }, 207 | "version": "36760" 208 | } 209 | }, 210 | "type": "service" 211 | }, 212 | "payload": { 213 | "HKExternalUUID": "CBA3CD4B-2F50-4796-90A5-83D168BFF553" 214 | }, 215 | "time": "2020-08-07T18:43:49.660Z", 216 | "type": "food", 217 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 218 | }, 219 | { 220 | "id": "e23f77e87d345b6c82c568dd57df42c4", 221 | "nutrition": { 222 | "carbohydrate": { 223 | "net": 24, 224 | "units": "grams" 225 | } 226 | }, 227 | "origin": { 228 | "id": "0F719CAC-F804-4047-AB94-32C24EAC6D75", 229 | "name": "com.apple.HealthKit", 230 | "payload": { 231 | "sourceRevision": { 232 | "operatingSystemVersion": "13.5.1", 233 | "productType": "iPhone9,3", 234 | "source": { 235 | "bundleIdentifier": "com.mysugr.companion.mySugr", 236 | "name": "mySugr" 237 | }, 238 | "version": "36760" 239 | } 240 | }, 241 | "type": "service" 242 | }, 243 | "payload": { 244 | "HKExternalUUID": "F9BFB648-BDDC-4009-AAB1-D836BF7F6071" 245 | }, 246 | "time": "2020-08-07T15:45:10.626Z", 247 | "type": "food", 248 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 249 | }, 250 | { 251 | "id": "c3c9886540125eb2c1a5abaa52dca068", 252 | "nutrition": { 253 | "carbohydrate": { 254 | "net": 24, 255 | "units": "grams" 256 | } 257 | }, 258 | "origin": { 259 | "id": "8FD56675-E777-469D-92D7-C568C03A8F16", 260 | "name": "com.apple.HealthKit", 261 | "payload": { 262 | "sourceRevision": { 263 | "operatingSystemVersion": "13.5.1", 264 | "productType": "iPhone9,3", 265 | "source": { 266 | "bundleIdentifier": "com.mysugr.companion.mySugr", 267 | "name": "mySugr" 268 | }, 269 | "version": "36760" 270 | } 271 | }, 272 | "type": "service" 273 | }, 274 | "payload": { 275 | "HKExternalUUID": "7185AA23-966F-4334-BA4C-094B3F282E1E" 276 | }, 277 | "time": "2020-08-07T13:06:02.871Z", 278 | "type": "food", 279 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 280 | }, 281 | { 282 | "id": "503e87c30ca1916a7270cda1b57e7004", 283 | "nutrition": { 284 | "carbohydrate": { 285 | "net": 48, 286 | "units": "grams" 287 | } 288 | }, 289 | "origin": { 290 | "id": "19AC286D-9B04-47FA-A4D7-205CC1364A81", 291 | "name": "com.apple.HealthKit", 292 | "payload": { 293 | "sourceRevision": { 294 | "operatingSystemVersion": "13.5.1", 295 | "productType": "iPhone9,3", 296 | "source": { 297 | "bundleIdentifier": "com.mysugr.companion.mySugr", 298 | "name": "mySugr" 299 | }, 300 | "version": "36760" 301 | } 302 | }, 303 | "type": "service" 304 | }, 305 | "payload": { 306 | "HKExternalUUID": "66CC60E2-6B17-4009-B573-24FC9CEDF599" 307 | }, 308 | "time": "2020-08-07T09:06:36.626Z", 309 | "type": "food", 310 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 311 | }, 312 | { 313 | "id": "f5483b0e7f2815dc174a8c567e563918", 314 | "nutrition": { 315 | "carbohydrate": { 316 | "net": 48, 317 | "units": "grams" 318 | } 319 | }, 320 | "origin": { 321 | "id": "C69D7F25-2E3B-4035-891B-1C4D4BDB0AF3", 322 | "name": "com.apple.HealthKit", 323 | "payload": { 324 | "sourceRevision": { 325 | "operatingSystemVersion": "13.5.1", 326 | "productType": "iPhone9,3", 327 | "source": { 328 | "bundleIdentifier": "com.mysugr.companion.mySugr", 329 | "name": "mySugr" 330 | }, 331 | "version": "36760" 332 | } 333 | }, 334 | "type": "service" 335 | }, 336 | "payload": { 337 | "HKExternalUUID": "DFE770B8-DADB-4B6E-BB28-7BC632E42111" 338 | }, 339 | "time": "2020-08-07T05:04:28.245Z", 340 | "type": "food", 341 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 342 | }, 343 | { 344 | "id": "d28bf5062f046c87cafb764e040ac628", 345 | "nutrition": { 346 | "carbohydrate": { 347 | "net": 52, 348 | "units": "grams" 349 | } 350 | }, 351 | "origin": { 352 | "id": "2E49F143-14C4-4E30-A694-87ABD2B88E5E", 353 | "name": "com.apple.HealthKit", 354 | "payload": { 355 | "sourceRevision": { 356 | "operatingSystemVersion": "13.5.1", 357 | "productType": "iPhone9,3", 358 | "source": { 359 | "bundleIdentifier": "com.mysugr.companion.mySugr", 360 | "name": "mySugr" 361 | }, 362 | "version": "36760" 363 | } 364 | }, 365 | "type": "service" 366 | }, 367 | "payload": { 368 | "HKExternalUUID": "AD2C6449-5672-4765-96D8-4D43E907BD5C" 369 | }, 370 | "time": "2020-08-06T18:00:50.270Z", 371 | "type": "food", 372 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 373 | }, 374 | { 375 | "id": "b8655197ee2c339c966a619bb13a9308", 376 | "nutrition": { 377 | "carbohydrate": { 378 | "net": 24, 379 | "units": "grams" 380 | } 381 | }, 382 | "origin": { 383 | "id": "DC773216-4C29-4AEA-A2B8-BC5701B539E5", 384 | "name": "com.apple.HealthKit", 385 | "payload": { 386 | "sourceRevision": { 387 | "operatingSystemVersion": "13.5.1", 388 | "productType": "iPhone9,3", 389 | "source": { 390 | "bundleIdentifier": "com.mysugr.companion.mySugr", 391 | "name": "mySugr" 392 | }, 393 | "version": "36760" 394 | } 395 | }, 396 | "type": "service" 397 | }, 398 | "payload": { 399 | "HKExternalUUID": "13048208-247F-41F7-8DDB-78AD0D7A9E5A" 400 | }, 401 | "time": "2020-08-06T14:24:29.175Z", 402 | "type": "food", 403 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 404 | }, 405 | { 406 | "id": "bf04cf079a282e8fe850e50e867fc9eb", 407 | "nutrition": { 408 | "carbohydrate": { 409 | "net": 24, 410 | "units": "grams" 411 | } 412 | }, 413 | "origin": { 414 | "id": "E98664D8-546F-4838-BBF7-21DDFC4DBFFA", 415 | "name": "com.apple.HealthKit", 416 | "payload": { 417 | "sourceRevision": { 418 | "operatingSystemVersion": "13.5.1", 419 | "productType": "iPhone9,3", 420 | "source": { 421 | "bundleIdentifier": "com.mysugr.companion.mySugr", 422 | "name": "mySugr" 423 | }, 424 | "version": "36760" 425 | } 426 | }, 427 | "type": "service" 428 | }, 429 | "payload": { 430 | "HKExternalUUID": "667E9A21-6656-4C6B-80FC-5A79831F7EA2" 431 | }, 432 | "time": "2020-08-06T11:04:07.887Z", 433 | "type": "food", 434 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 435 | }, 436 | { 437 | "id": "408de83a78355d2a46e3f3ad582f8c63", 438 | "nutrition": { 439 | "carbohydrate": { 440 | "net": 48, 441 | "units": "grams" 442 | } 443 | }, 444 | "origin": { 445 | "id": "B2F9F889-EB63-45B9-8E82-B8D627F44F5A", 446 | "name": "com.apple.HealthKit", 447 | "payload": { 448 | "sourceRevision": { 449 | "operatingSystemVersion": "13.5.1", 450 | "productType": "iPhone9,3", 451 | "source": { 452 | "bundleIdentifier": "com.mysugr.companion.mySugr", 453 | "name": "mySugr" 454 | }, 455 | "version": "36760" 456 | } 457 | }, 458 | "type": "service" 459 | }, 460 | "payload": { 461 | "HKExternalUUID": "EB7035C3-BA9E-4D1E-837E-EF3DF25D2394" 462 | }, 463 | "time": "2020-08-06T09:40:11.095Z", 464 | "type": "food", 465 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 466 | }, 467 | { 468 | "id": "b45815d2b65e10cc7909ef8f8f602c2a", 469 | "nutrition": { 470 | "carbohydrate": { 471 | "net": 24, 472 | "units": "grams" 473 | } 474 | }, 475 | "origin": { 476 | "id": "71AF619B-0845-4472-B6BA-0CA51C47FE10", 477 | "name": "com.apple.HealthKit", 478 | "payload": { 479 | "sourceRevision": { 480 | "operatingSystemVersion": "13.5.1", 481 | "productType": "iPhone9,3", 482 | "source": { 483 | "bundleIdentifier": "com.mysugr.companion.mySugr", 484 | "name": "mySugr" 485 | }, 486 | "version": "36760" 487 | } 488 | }, 489 | "type": "service" 490 | }, 491 | "payload": { 492 | "HKExternalUUID": "3C68DB4F-A81D-46C0-93E3-D764E39D2A56" 493 | }, 494 | "time": "2020-08-05T22:35:05.771Z", 495 | "type": "food", 496 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 497 | }, 498 | { 499 | "id": "7a8433061946216a3bf8d013f8274bd6", 500 | "nutrition": { 501 | "carbohydrate": { 502 | "net": 24, 503 | "units": "grams" 504 | } 505 | }, 506 | "origin": { 507 | "id": "D0F8BD00-BA86-40FF-BB71-DFB7704FCE6D", 508 | "name": "com.apple.HealthKit", 509 | "payload": { 510 | "sourceRevision": { 511 | "operatingSystemVersion": "13.5.1", 512 | "productType": "iPhone9,3", 513 | "source": { 514 | "bundleIdentifier": "com.mysugr.companion.mySugr", 515 | "name": "mySugr" 516 | }, 517 | "version": "36760" 518 | } 519 | }, 520 | "type": "service" 521 | }, 522 | "payload": { 523 | "HKExternalUUID": "BFABB749-4FF4-4050-A075-093403E545C7" 524 | }, 525 | "time": "2020-08-05T17:11:36.524Z", 526 | "type": "food", 527 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 528 | }, 529 | { 530 | "id": "b9d243cd3c59c2a340bad605b91aef51", 531 | "nutrition": { 532 | "carbohydrate": { 533 | "net": 23, 534 | "units": "grams" 535 | } 536 | }, 537 | "origin": { 538 | "id": "7EFCD935-350C-4C86-A19C-9393CB6D9746", 539 | "name": "com.apple.HealthKit", 540 | "payload": { 541 | "sourceRevision": { 542 | "operatingSystemVersion": "13.5.1", 543 | "productType": "iPhone9,3", 544 | "source": { 545 | "bundleIdentifier": "com.mysugr.companion.mySugr", 546 | "name": "mySugr" 547 | }, 548 | "version": "36760" 549 | } 550 | }, 551 | "type": "service" 552 | }, 553 | "payload": { 554 | "HKExternalUUID": "1EB32BBC-0B7B-44D4-9724-AE2BAD97C835" 555 | }, 556 | "time": "2020-08-05T14:36:00.000Z", 557 | "type": "food", 558 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 559 | }, 560 | { 561 | "id": "6d8bc342e675cabafdb9eed956b04cc4", 562 | "nutrition": { 563 | "carbohydrate": { 564 | "net": 33, 565 | "units": "grams" 566 | } 567 | }, 568 | "origin": { 569 | "id": "6910FD86-5115-4F3F-B650-C3D159C53338", 570 | "name": "com.apple.HealthKit", 571 | "payload": { 572 | "sourceRevision": { 573 | "operatingSystemVersion": "13.5.1", 574 | "productType": "iPhone9,3", 575 | "source": { 576 | "bundleIdentifier": "com.mysugr.companion.mySugr", 577 | "name": "mySugr" 578 | }, 579 | "version": "36760" 580 | } 581 | }, 582 | "type": "service" 583 | }, 584 | "payload": { 585 | "HKExternalUUID": "0CE0DDB2-A38A-4CDE-98A6-82EAE14D054D" 586 | }, 587 | "time": "2020-08-05T12:26:25.719Z", 588 | "type": "food", 589 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 590 | }, 591 | { 592 | "id": "24ab92b9c89f3678e9fbf00d2b81d579", 593 | "nutrition": { 594 | "carbohydrate": { 595 | "net": 48, 596 | "units": "grams" 597 | } 598 | }, 599 | "origin": { 600 | "id": "D2A2F0DA-0B03-43BC-9B10-015C006AC64F", 601 | "name": "com.apple.HealthKit", 602 | "payload": { 603 | "sourceRevision": { 604 | "operatingSystemVersion": "13.5.1", 605 | "productType": "iPhone9,3", 606 | "source": { 607 | "bundleIdentifier": "com.mysugr.companion.mySugr", 608 | "name": "mySugr" 609 | }, 610 | "version": "36760" 611 | } 612 | }, 613 | "type": "service" 614 | }, 615 | "payload": { 616 | "HKExternalUUID": "00DF4B4E-0D39-4C2A-AE7F-CB3620236962" 617 | }, 618 | "time": "2020-08-05T09:14:57.828Z", 619 | "type": "food", 620 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 621 | }, 622 | { 623 | "id": "e58162c14612a9efc51db91e70228d12", 624 | "nutrition": { 625 | "carbohydrate": { 626 | "net": 24, 627 | "units": "grams" 628 | } 629 | }, 630 | "origin": { 631 | "id": "EF13414B-64AA-435A-8693-159B5F4A6E4E", 632 | "name": "com.apple.HealthKit", 633 | "payload": { 634 | "sourceRevision": { 635 | "operatingSystemVersion": "13.5.1", 636 | "productType": "iPhone9,3", 637 | "source": { 638 | "bundleIdentifier": "com.mysugr.companion.mySugr", 639 | "name": "mySugr" 640 | }, 641 | "version": "36760" 642 | } 643 | }, 644 | "type": "service" 645 | }, 646 | "payload": { 647 | "HKExternalUUID": "A2B3F0F8-F792-4BED-A008-71CAB4C86B0B" 648 | }, 649 | "time": "2020-08-04T18:10:51.695Z", 650 | "type": "food", 651 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 652 | }, 653 | { 654 | "id": "ceb90d5ee2a9ab79b652ce6db2530527", 655 | "nutrition": { 656 | "carbohydrate": { 657 | "net": 23, 658 | "units": "grams" 659 | } 660 | }, 661 | "origin": { 662 | "id": "C4A44DBA-E763-4A4A-90D4-8F3A6D09AA99", 663 | "name": "com.apple.HealthKit", 664 | "payload": { 665 | "sourceRevision": { 666 | "operatingSystemVersion": "13.5.1", 667 | "productType": "iPhone9,3", 668 | "source": { 669 | "bundleIdentifier": "com.mysugr.companion.mySugr", 670 | "name": "mySugr" 671 | }, 672 | "version": "36760" 673 | } 674 | }, 675 | "type": "service" 676 | }, 677 | "payload": { 678 | "HKExternalUUID": "30B395B4-F63D-4DDE-BC0C-ED69DF5F8F14" 679 | }, 680 | "time": "2020-08-04T15:33:01.726Z", 681 | "type": "food", 682 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 683 | }, 684 | { 685 | "id": "9d680761f78381fc450cafb5ddce9e35", 686 | "nutrition": { 687 | "carbohydrate": { 688 | "net": 24, 689 | "units": "grams" 690 | } 691 | }, 692 | "origin": { 693 | "id": "6807CFCA-EB23-4E98-9AA0-6E66082491FA", 694 | "name": "com.apple.HealthKit", 695 | "payload": { 696 | "sourceRevision": { 697 | "operatingSystemVersion": "13.5.1", 698 | "productType": "iPhone9,3", 699 | "source": { 700 | "bundleIdentifier": "com.mysugr.companion.mySugr", 701 | "name": "mySugr" 702 | }, 703 | "version": "36760" 704 | } 705 | }, 706 | "type": "service" 707 | }, 708 | "payload": { 709 | "HKExternalUUID": "39C7E1C4-C069-4112-964E-AC84A6DFE18B" 710 | }, 711 | "time": "2020-08-04T15:21:34.383Z", 712 | "type": "food", 713 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 714 | }, 715 | { 716 | "id": "cd16e29f603b530cc33607ab7eb61590", 717 | "nutrition": { 718 | "carbohydrate": { 719 | "net": 36, 720 | "units": "grams" 721 | } 722 | }, 723 | "origin": { 724 | "id": "6576B16F-56CA-499C-BC57-E43850FBDAE6", 725 | "name": "com.apple.HealthKit", 726 | "payload": { 727 | "sourceRevision": { 728 | "operatingSystemVersion": "13.5.1", 729 | "productType": "iPhone9,3", 730 | "source": { 731 | "bundleIdentifier": "com.mysugr.companion.mySugr", 732 | "name": "mySugr" 733 | }, 734 | "version": "36760" 735 | } 736 | }, 737 | "type": "service" 738 | }, 739 | "payload": { 740 | "HKExternalUUID": "20FB1538-6629-4F73-8F2A-1F19214FA541" 741 | }, 742 | "time": "2020-08-04T09:57:20.415Z", 743 | "type": "food", 744 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 745 | }, 746 | { 747 | "id": "a3aca2e130831ea06a0c8473f22720e1", 748 | "nutrition": { 749 | "carbohydrate": { 750 | "net": 12, 751 | "units": "grams" 752 | } 753 | }, 754 | "origin": { 755 | "id": "5FD7DF9D-0BC6-473E-936A-8941979F86D9", 756 | "name": "com.apple.HealthKit", 757 | "payload": { 758 | "sourceRevision": { 759 | "operatingSystemVersion": "13.5.1", 760 | "productType": "iPhone9,3", 761 | "source": { 762 | "bundleIdentifier": "com.mysugr.companion.mySugr", 763 | "name": "mySugr" 764 | }, 765 | "version": "36760" 766 | } 767 | }, 768 | "type": "service" 769 | }, 770 | "payload": { 771 | "HKExternalUUID": "859C2DF7-8437-4036-914A-6CB4DB8995BF" 772 | }, 773 | "time": "2020-08-04T06:15:58.240Z", 774 | "type": "food", 775 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 776 | }, 777 | { 778 | "id": "97679b912e4b8489ac0741def6ba6b97", 779 | "nutrition": { 780 | "carbohydrate": { 781 | "net": 48, 782 | "units": "grams" 783 | } 784 | }, 785 | "origin": { 786 | "id": "0F2D5C9E-4B2C-4C6A-8404-FF0C16FFB025", 787 | "name": "com.apple.HealthKit", 788 | "payload": { 789 | "sourceRevision": { 790 | "operatingSystemVersion": "13.5.1", 791 | "productType": "iPhone9,3", 792 | "source": { 793 | "bundleIdentifier": "com.mysugr.companion.mySugr", 794 | "name": "mySugr" 795 | }, 796 | "version": "36760" 797 | } 798 | }, 799 | "type": "service" 800 | }, 801 | "payload": { 802 | "HKExternalUUID": "D53B13AF-1441-456C-BF26-4D68019D9138" 803 | }, 804 | "time": "2020-08-03T17:43:04.593Z", 805 | "type": "food", 806 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 807 | }, 808 | { 809 | "id": "63c6bb8afebc6464be934911d6222b10", 810 | "nutrition": { 811 | "carbohydrate": { 812 | "net": 28, 813 | "units": "grams" 814 | } 815 | }, 816 | "origin": { 817 | "id": "1AD63344-896B-4B49-AF6B-A2CF42097C8B", 818 | "name": "com.apple.HealthKit", 819 | "payload": { 820 | "sourceRevision": { 821 | "operatingSystemVersion": "13.5.1", 822 | "productType": "iPhone9,3", 823 | "source": { 824 | "bundleIdentifier": "com.mysugr.companion.mySugr", 825 | "name": "mySugr" 826 | }, 827 | "version": "36760" 828 | } 829 | }, 830 | "type": "service" 831 | }, 832 | "payload": { 833 | "HKExternalUUID": "94289E50-86AA-4345-B48D-3254BD9C196C" 834 | }, 835 | "time": "2020-08-03T11:36:34.868Z", 836 | "type": "food", 837 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 838 | }, 839 | { 840 | "id": "1e50ac3433f15e9335b7adcdaebb4f0b", 841 | "nutrition": { 842 | "carbohydrate": { 843 | "net": 60, 844 | "units": "grams" 845 | } 846 | }, 847 | "origin": { 848 | "id": "8938041B-EEE8-4B43-A701-50B73555D2AF", 849 | "name": "com.apple.HealthKit", 850 | "payload": { 851 | "sourceRevision": { 852 | "operatingSystemVersion": "13.5.1", 853 | "productType": "iPhone9,3", 854 | "source": { 855 | "bundleIdentifier": "com.mysugr.companion.mySugr", 856 | "name": "mySugr" 857 | }, 858 | "version": "36760" 859 | } 860 | }, 861 | "type": "service" 862 | }, 863 | "payload": { 864 | "HKExternalUUID": "C1211B52-5CA2-45CD-82B0-584AE0B96B02" 865 | }, 866 | "time": "2020-08-03T10:21:07.540Z", 867 | "type": "food", 868 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 869 | }, 870 | { 871 | "id": "ec6875cebd34233d352b529388d3e7fe", 872 | "nutrition": { 873 | "carbohydrate": { 874 | "net": 18, 875 | "units": "grams" 876 | } 877 | }, 878 | "origin": { 879 | "id": "2C43C6FD-DD1C-4927-BF4E-5A2FE9E79D38", 880 | "name": "com.apple.HealthKit", 881 | "payload": { 882 | "sourceRevision": { 883 | "operatingSystemVersion": "13.5.1", 884 | "productType": "iPhone9,3", 885 | "source": { 886 | "bundleIdentifier": "com.mysugr.companion.mySugr", 887 | "name": "mySugr" 888 | }, 889 | "version": "36760" 890 | } 891 | }, 892 | "type": "service" 893 | }, 894 | "payload": { 895 | "HKExternalUUID": "98DD12FF-DBD3-458D-A340-FE4908346D1D" 896 | }, 897 | "time": "2020-08-03T08:03:53.000Z", 898 | "type": "food", 899 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 900 | }, 901 | { 902 | "id": "6741270abda21f23495d788e8216cec8", 903 | "nutrition": { 904 | "carbohydrate": { 905 | "net": 24, 906 | "units": "grams" 907 | } 908 | }, 909 | "origin": { 910 | "id": "67B0104A-4EEB-4A71-98FF-7D6B7CD792E9", 911 | "name": "com.apple.HealthKit", 912 | "payload": { 913 | "sourceRevision": { 914 | "operatingSystemVersion": "13.5.1", 915 | "productType": "iPhone9,3", 916 | "source": { 917 | "bundleIdentifier": "com.mysugr.companion.mySugr", 918 | "name": "mySugr" 919 | }, 920 | "version": "36760" 921 | } 922 | }, 923 | "type": "service" 924 | }, 925 | "payload": { 926 | "HKExternalUUID": "A0C2E6DD-ED0F-4412-A370-1B0D87A41963" 927 | }, 928 | "time": "2020-08-03T04:55:18.196Z", 929 | "type": "food", 930 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 931 | }, 932 | { 933 | "id": "d8f0001445eb141cb734630ecce1b287", 934 | "nutrition": { 935 | "carbohydrate": { 936 | "net": 72, 937 | "units": "grams" 938 | } 939 | }, 940 | "origin": { 941 | "id": "E6CE043B-91D2-4293-9193-C80BD62E6EDA", 942 | "name": "com.apple.HealthKit", 943 | "payload": { 944 | "sourceRevision": { 945 | "operatingSystemVersion": "13.5.1", 946 | "productType": "iPhone9,3", 947 | "source": { 948 | "bundleIdentifier": "com.mysugr.companion.mySugr", 949 | "name": "mySugr" 950 | }, 951 | "version": "36760" 952 | } 953 | }, 954 | "type": "service" 955 | }, 956 | "payload": { 957 | "HKExternalUUID": "7BF63A62-1DD9-4575-BA3C-FF41BFC907A6" 958 | }, 959 | "time": "2020-08-02T17:36:05.453Z", 960 | "type": "food", 961 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 962 | }, 963 | { 964 | "id": "0fd1e648b99d649580a259829c46e119", 965 | "nutrition": { 966 | "carbohydrate": { 967 | "net": 36, 968 | "units": "grams" 969 | } 970 | }, 971 | "origin": { 972 | "id": "E7CA67CA-CDAA-4337-A706-92BAB1BD6C47", 973 | "name": "com.apple.HealthKit", 974 | "payload": { 975 | "sourceRevision": { 976 | "operatingSystemVersion": "13.5.1", 977 | "productType": "iPhone9,3", 978 | "source": { 979 | "bundleIdentifier": "com.mysugr.companion.mySugr", 980 | "name": "mySugr" 981 | }, 982 | "version": "36760" 983 | } 984 | }, 985 | "type": "service" 986 | }, 987 | "payload": { 988 | "HKExternalUUID": "FD1F2874-03E2-4291-8F5B-8F24F6A359B8" 989 | }, 990 | "time": "2020-08-02T11:06:30.759Z", 991 | "type": "food", 992 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 993 | }, 994 | { 995 | "id": "4af8659eec24111d00442ca8987afd7c", 996 | "nutrition": { 997 | "carbohydrate": { 998 | "net": 48, 999 | "units": "grams" 1000 | } 1001 | }, 1002 | "origin": { 1003 | "id": "BFC1AA8E-F657-4BE9-8193-4590DF7C595B", 1004 | "name": "com.apple.HealthKit", 1005 | "payload": { 1006 | "sourceRevision": { 1007 | "operatingSystemVersion": "13.5.1", 1008 | "productType": "iPhone9,3", 1009 | "source": { 1010 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1011 | "name": "mySugr" 1012 | }, 1013 | "version": "36760" 1014 | } 1015 | }, 1016 | "type": "service" 1017 | }, 1018 | "payload": { 1019 | "HKExternalUUID": "EC4F3844-054C-4CD7-896F-A457305BB9D0" 1020 | }, 1021 | "time": "2020-08-02T09:56:41.780Z", 1022 | "type": "food", 1023 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1024 | }, 1025 | { 1026 | "id": "1b6900f39a5dbfd31337764e52d6bcc4", 1027 | "nutrition": { 1028 | "carbohydrate": { 1029 | "net": 24, 1030 | "units": "grams" 1031 | } 1032 | }, 1033 | "origin": { 1034 | "id": "926772F7-12D6-42E9-8883-C386DA959BDC", 1035 | "name": "com.apple.HealthKit", 1036 | "payload": { 1037 | "sourceRevision": { 1038 | "operatingSystemVersion": "13.5.1", 1039 | "productType": "iPhone9,3", 1040 | "source": { 1041 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1042 | "name": "mySugr" 1043 | }, 1044 | "version": "36760" 1045 | } 1046 | }, 1047 | "type": "service" 1048 | }, 1049 | "payload": { 1050 | "HKExternalUUID": "E68D93CA-4F97-452B-8319-954727D2AB81" 1051 | }, 1052 | "time": "2020-08-02T05:37:42.962Z", 1053 | "type": "food", 1054 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1055 | }, 1056 | { 1057 | "id": "503377882dc66da1cc5059492dc74c59", 1058 | "nutrition": { 1059 | "carbohydrate": { 1060 | "net": 34, 1061 | "units": "grams" 1062 | } 1063 | }, 1064 | "origin": { 1065 | "id": "5AF380CA-6430-4735-A4B0-79F62528CA5F", 1066 | "name": "com.apple.HealthKit", 1067 | "payload": { 1068 | "sourceRevision": { 1069 | "operatingSystemVersion": "13.5.1", 1070 | "productType": "iPhone9,3", 1071 | "source": { 1072 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1073 | "name": "mySugr" 1074 | }, 1075 | "version": "36760" 1076 | } 1077 | }, 1078 | "type": "service" 1079 | }, 1080 | "payload": { 1081 | "HKExternalUUID": "ED7A6A48-3F76-440C-BB63-5DE689646CCB" 1082 | }, 1083 | "time": "2020-08-01T17:27:29.094Z", 1084 | "type": "food", 1085 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1086 | }, 1087 | { 1088 | "id": "0e0fa644abd53c722edde1679295aab5", 1089 | "nutrition": { 1090 | "carbohydrate": { 1091 | "net": 24, 1092 | "units": "grams" 1093 | } 1094 | }, 1095 | "origin": { 1096 | "id": "776CC493-E2C2-4B45-9414-E5E760936981", 1097 | "name": "com.apple.HealthKit", 1098 | "payload": { 1099 | "sourceRevision": { 1100 | "operatingSystemVersion": "13.5.1", 1101 | "productType": "iPhone9,3", 1102 | "source": { 1103 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1104 | "name": "mySugr" 1105 | }, 1106 | "version": "36760" 1107 | } 1108 | }, 1109 | "type": "service" 1110 | }, 1111 | "payload": { 1112 | "HKExternalUUID": "DD04A912-3213-476B-B88A-6E4F58F9972E" 1113 | }, 1114 | "time": "2020-08-01T17:13:04.695Z", 1115 | "type": "food", 1116 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1117 | }, 1118 | { 1119 | "id": "402e18578b04701a1980bce56abfa076", 1120 | "nutrition": { 1121 | "carbohydrate": { 1122 | "net": 24, 1123 | "units": "grams" 1124 | } 1125 | }, 1126 | "origin": { 1127 | "id": "B56A9320-DF71-4D9E-B76D-8282B0E57D45", 1128 | "name": "com.apple.HealthKit", 1129 | "payload": { 1130 | "sourceRevision": { 1131 | "operatingSystemVersion": "13.5.1", 1132 | "productType": "iPhone9,3", 1133 | "source": { 1134 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1135 | "name": "mySugr" 1136 | }, 1137 | "version": "36760" 1138 | } 1139 | }, 1140 | "type": "service" 1141 | }, 1142 | "payload": { 1143 | "HKExternalUUID": "17AFDCAD-9521-4E37-98C9-3531048EAA52" 1144 | }, 1145 | "time": "2020-08-01T14:47:19.499Z", 1146 | "type": "food", 1147 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1148 | }, 1149 | { 1150 | "id": "463dae51ca1a95738ed0cb6843da86b2", 1151 | "nutrition": { 1152 | "carbohydrate": { 1153 | "net": 12, 1154 | "units": "grams" 1155 | } 1156 | }, 1157 | "origin": { 1158 | "id": "6E3E7CEE-9EEB-488D-89AD-4D8F138759BF", 1159 | "name": "com.apple.HealthKit", 1160 | "payload": { 1161 | "sourceRevision": { 1162 | "operatingSystemVersion": "13.5.1", 1163 | "productType": "iPhone9,3", 1164 | "source": { 1165 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1166 | "name": "mySugr" 1167 | }, 1168 | "version": "36760" 1169 | } 1170 | }, 1171 | "type": "service" 1172 | }, 1173 | "payload": { 1174 | "HKExternalUUID": "64597C08-C676-47D2-8F84-73C828B3CA05" 1175 | }, 1176 | "time": "2020-08-01T09:21:33.137Z", 1177 | "type": "food", 1178 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1179 | }, 1180 | { 1181 | "id": "92c151e54b33e2371d5123cc5eb3fe70", 1182 | "nutrition": { 1183 | "carbohydrate": { 1184 | "net": 48, 1185 | "units": "grams" 1186 | } 1187 | }, 1188 | "origin": { 1189 | "id": "1477D083-C52E-4872-8867-606B4760456B", 1190 | "name": "com.apple.HealthKit", 1191 | "payload": { 1192 | "sourceRevision": { 1193 | "operatingSystemVersion": "13.5.1", 1194 | "productType": "iPhone9,3", 1195 | "source": { 1196 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1197 | "name": "mySugr" 1198 | }, 1199 | "version": "36760" 1200 | } 1201 | }, 1202 | "type": "service" 1203 | }, 1204 | "payload": { 1205 | "HKExternalUUID": "5C08E699-6D25-49E7-908C-D574564CE4C4" 1206 | }, 1207 | "time": "2020-08-01T08:50:53.069Z", 1208 | "type": "food", 1209 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1210 | }, 1211 | { 1212 | "id": "b8bc3a015b593e6176d159e1b9dc7606", 1213 | "nutrition": { 1214 | "carbohydrate": { 1215 | "net": 24, 1216 | "units": "grams" 1217 | } 1218 | }, 1219 | "origin": { 1220 | "id": "9B961DD0-8F25-4F7A-859F-0509D37C0500", 1221 | "name": "com.apple.HealthKit", 1222 | "payload": { 1223 | "sourceRevision": { 1224 | "operatingSystemVersion": "13.5.1", 1225 | "productType": "iPhone9,3", 1226 | "source": { 1227 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1228 | "name": "mySugr" 1229 | }, 1230 | "version": "36760" 1231 | } 1232 | }, 1233 | "type": "service" 1234 | }, 1235 | "payload": { 1236 | "HKExternalUUID": "25708880-A555-4AF0-99A0-7326B7AEEE9D" 1237 | }, 1238 | "time": "2020-08-01T05:17:18.110Z", 1239 | "type": "food", 1240 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1241 | }, 1242 | { 1243 | "id": "e52c6541b645f810ad3001407efd6575", 1244 | "nutrition": { 1245 | "carbohydrate": { 1246 | "net": 24, 1247 | "units": "grams" 1248 | } 1249 | }, 1250 | "origin": { 1251 | "id": "61CFF86E-D4B3-480B-85A2-046D3EA8DBDB", 1252 | "name": "com.apple.HealthKit", 1253 | "payload": { 1254 | "sourceRevision": { 1255 | "operatingSystemVersion": "13.5.1", 1256 | "productType": "iPhone9,3", 1257 | "source": { 1258 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1259 | "name": "mySugr" 1260 | }, 1261 | "version": "36760" 1262 | } 1263 | }, 1264 | "type": "service" 1265 | }, 1266 | "payload": { 1267 | "HKExternalUUID": "9117A9FD-639F-4FA6-AE4A-D15E5E9879E8" 1268 | }, 1269 | "time": "2020-07-31T20:24:08.998Z", 1270 | "type": "food", 1271 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1272 | }, 1273 | { 1274 | "id": "95015d6450ada5306e95cadd05027fa8", 1275 | "nutrition": { 1276 | "carbohydrate": { 1277 | "net": 48, 1278 | "units": "grams" 1279 | } 1280 | }, 1281 | "origin": { 1282 | "id": "161A9A83-A7CD-470F-A2E2-DA64B1D50379", 1283 | "name": "com.apple.HealthKit", 1284 | "payload": { 1285 | "sourceRevision": { 1286 | "operatingSystemVersion": "13.5.1", 1287 | "productType": "iPhone9,3", 1288 | "source": { 1289 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1290 | "name": "mySugr" 1291 | }, 1292 | "version": "36760" 1293 | } 1294 | }, 1295 | "type": "service" 1296 | }, 1297 | "payload": { 1298 | "HKExternalUUID": "A82D9A67-FA0A-47D3-83F5-F51081A45773" 1299 | }, 1300 | "time": "2020-07-31T16:34:32.431Z", 1301 | "type": "food", 1302 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1303 | }, 1304 | { 1305 | "id": "64a8a4f7e9a9d56fbb2cc8326e924bc5", 1306 | "nutrition": { 1307 | "carbohydrate": { 1308 | "net": 28, 1309 | "units": "grams" 1310 | } 1311 | }, 1312 | "origin": { 1313 | "id": "8768D4BB-2B92-4185-9218-A667A9AF9551", 1314 | "name": "com.apple.HealthKit", 1315 | "payload": { 1316 | "sourceRevision": { 1317 | "operatingSystemVersion": "13.5.1", 1318 | "productType": "iPhone9,3", 1319 | "source": { 1320 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1321 | "name": "mySugr" 1322 | }, 1323 | "version": "36760" 1324 | } 1325 | }, 1326 | "type": "service" 1327 | }, 1328 | "payload": { 1329 | "HKExternalUUID": "A237BCED-AAF6-4973-B964-59DB3A3A958F" 1330 | }, 1331 | "time": "2020-07-31T16:05:11.992Z", 1332 | "type": "food", 1333 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1334 | }, 1335 | { 1336 | "id": "fe800840ff15331727631055d7f2dc0f", 1337 | "nutrition": { 1338 | "carbohydrate": { 1339 | "net": 24, 1340 | "units": "grams" 1341 | } 1342 | }, 1343 | "origin": { 1344 | "id": "49B103DF-7080-4E9B-A0EA-241776172BC7", 1345 | "name": "com.apple.HealthKit", 1346 | "payload": { 1347 | "sourceRevision": { 1348 | "operatingSystemVersion": "13.5.1", 1349 | "productType": "iPhone9,3", 1350 | "source": { 1351 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1352 | "name": "mySugr" 1353 | }, 1354 | "version": "36760" 1355 | } 1356 | }, 1357 | "type": "service" 1358 | }, 1359 | "payload": { 1360 | "HKExternalUUID": "6C64CE20-3DDE-471B-BC78-A02CDCEAAACD" 1361 | }, 1362 | "time": "2020-07-31T13:14:13.670Z", 1363 | "type": "food", 1364 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1365 | }, 1366 | { 1367 | "id": "cc7713e7bdfdf73d4d5146ca2d17fcac", 1368 | "nutrition": { 1369 | "carbohydrate": { 1370 | "net": 56, 1371 | "units": "grams" 1372 | } 1373 | }, 1374 | "origin": { 1375 | "id": "62DA0AA2-C1C9-457E-98D3-4A0A230DB9EC", 1376 | "name": "com.apple.HealthKit", 1377 | "payload": { 1378 | "sourceRevision": { 1379 | "operatingSystemVersion": "13.5.1", 1380 | "productType": "iPhone9,3", 1381 | "source": { 1382 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1383 | "name": "mySugr" 1384 | }, 1385 | "version": "36760" 1386 | } 1387 | }, 1388 | "type": "service" 1389 | }, 1390 | "payload": { 1391 | "HKExternalUUID": "7C28D177-9E37-4F8D-91C1-D1F89D858228" 1392 | }, 1393 | "time": "2020-07-31T10:01:15.778Z", 1394 | "type": "food", 1395 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1396 | }, 1397 | { 1398 | "id": "393a53df8eba7b6b7fdc7c79d7b0d034", 1399 | "nutrition": { 1400 | "carbohydrate": { 1401 | "net": 24, 1402 | "units": "grams" 1403 | } 1404 | }, 1405 | "origin": { 1406 | "id": "B6E9A025-4B4B-4503-8ED0-4ABDD7E10D9B", 1407 | "name": "com.apple.HealthKit", 1408 | "payload": { 1409 | "sourceRevision": { 1410 | "operatingSystemVersion": "13.5.1", 1411 | "productType": "iPhone9,3", 1412 | "source": { 1413 | "bundleIdentifier": "com.mysugr.companion.mySugr", 1414 | "name": "mySugr" 1415 | }, 1416 | "version": "36760" 1417 | } 1418 | }, 1419 | "type": "service" 1420 | }, 1421 | "payload": { 1422 | "HKExternalUUID": "DFD96B4E-D25B-4249-B2F4-C2D125AC75AA" 1423 | }, 1424 | "time": "2020-07-31T07:55:11.724Z", 1425 | "type": "food", 1426 | "uploadId": "2f1be6a1baed52801af2e992ac7da1fb" 1427 | } 1428 | ] --------------------------------------------------------------------------------