From 1816fc6e2ea43c8fd4adadb62794b990390bc30d Mon Sep 17 00:00:00 2001 From: Julian Lobbes Date: Sat, 29 Jul 2023 20:43:41 +0200 Subject: [PATCH] feat: implement sync with withings cloud --- app/authentication/admin.py | 3 - app/authentication/models.py | 3 - app/authentication/tests.py | 3 - app/authentication/views.py | 3 +- app/gotify/admin.py | 3 - app/gotify/migrations/0001_initial.py | 2 +- app/gotify/tests.py | 3 - app/gotify/views.py | 3 - app/medwings/admin.py | 3 - app/medwings/migrations/0001_initial.py | 16 +- app/medwings/models.py | 24 +-- app/medwings/tests.py | 3 - app/requirements.txt | 1 + app/withings/README.md | 210 ++++++++++++++++++++++++ app/withings/admin.py | 3 - app/withings/api.py | 67 +++++++- app/withings/migrations/0001_initial.py | 3 +- app/withings/models.py | 47 +++++- app/withings/tests.py | 3 - app/withings/views.py | 3 - 20 files changed, 345 insertions(+), 61 deletions(-) delete mode 100644 app/authentication/admin.py delete mode 100644 app/authentication/models.py delete mode 100644 app/authentication/tests.py delete mode 100644 app/gotify/admin.py delete mode 100644 app/gotify/tests.py delete mode 100644 app/gotify/views.py delete mode 100644 app/medwings/admin.py delete mode 100644 app/medwings/tests.py create mode 100644 app/withings/README.md delete mode 100644 app/withings/admin.py delete mode 100644 app/withings/tests.py delete mode 100644 app/withings/views.py diff --git a/app/authentication/admin.py b/app/authentication/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/app/authentication/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/app/authentication/models.py b/app/authentication/models.py deleted file mode 100644 index 71a8362..0000000 --- a/app/authentication/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/app/authentication/tests.py b/app/authentication/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/app/authentication/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/app/authentication/views.py b/app/authentication/views.py index 652461e..294a2a3 100644 --- a/app/authentication/views.py +++ b/app/authentication/views.py @@ -108,7 +108,8 @@ def register_continue(request): instance.save() request.session.flush() - # TODO sync withings health data + withings_api_account.update_records() + # TODO redirect user to some other page and ask them to log in return redirect('dashboard') diff --git a/app/gotify/admin.py b/app/gotify/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/app/gotify/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/app/gotify/migrations/0001_initial.py b/app/gotify/migrations/0001_initial.py index e387c92..28c0423 100644 --- a/app/gotify/migrations/0001_initial.py +++ b/app/gotify/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.3 on 2023-07-27 14:35 +# Generated by Django 4.2.3 on 2023-07-29 18:28 from django.conf import settings from django.db import migrations, models diff --git a/app/gotify/tests.py b/app/gotify/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/app/gotify/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/app/gotify/views.py b/app/gotify/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/app/gotify/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/app/medwings/admin.py b/app/medwings/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/app/medwings/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/app/medwings/migrations/0001_initial.py b/app/medwings/migrations/0001_initial.py index fb71fd4..7a90529 100644 --- a/app/medwings/migrations/0001_initial.py +++ b/app/medwings/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.3 on 2023-07-27 14:35 +# Generated by Django 4.2.3 on 2023-07-29 18:28 from django.conf import settings from django.db import migrations, models @@ -20,7 +20,7 @@ class Migration(migrations.Migration): name='BloodPressureRecord', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('recorded', models.DateTimeField(validators=[medwings.validators.AbstractRecordValidator.recorded], verbose_name='Time at which measurement was taken')), + ('recorded', models.DateTimeField(unique=True, validators=[medwings.validators.AbstractRecordValidator.recorded], verbose_name='Time at which measurement was taken')), ('value_systolic_mmhg', models.PositiveIntegerField(validators=[medwings.validators.BloodPressureRecordValidator.value_systolic_mmhg], verbose_name='Systolic Blood Pressure (mmhg)')), ('value_diastolic_mmhg', models.PositiveIntegerField(validators=[medwings.validators.BloodPressureRecordValidator.value_diastolic_mmhg], verbose_name='Diastolic Blood Pressure (mmhg)')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), @@ -30,8 +30,8 @@ class Migration(migrations.Migration): name='BodyTempRecord', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('recorded', models.DateTimeField(validators=[medwings.validators.AbstractRecordValidator.recorded], verbose_name='Time at which measurement was taken')), - ('value_celsius', models.PositiveIntegerField(validators=[medwings.validators.BodyTempRecordValidator.value_celsius], verbose_name='Body Temperature (°C)')), + ('recorded', models.DateTimeField(unique=True, validators=[medwings.validators.AbstractRecordValidator.recorded], verbose_name='Time at which measurement was taken')), + ('value_celsius', models.DecimalField(decimal_places=2, max_digits=5, unique=True, validators=[medwings.validators.BodyTempRecordValidator.value_celsius], verbose_name='Body Temperature (°C)')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ), @@ -39,7 +39,7 @@ class Migration(migrations.Migration): name='HeartRateRecord', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('recorded', models.DateTimeField(validators=[medwings.validators.AbstractRecordValidator.recorded], verbose_name='Time at which measurement was taken')), + ('recorded', models.DateTimeField(unique=True, validators=[medwings.validators.AbstractRecordValidator.recorded], verbose_name='Time at which measurement was taken')), ('value_bpm', models.PositiveIntegerField(validators=[medwings.validators.HeartRateRecordValidator.value_bpm], verbose_name='Heart Rate (bpm)')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], @@ -56,7 +56,7 @@ class Migration(migrations.Migration): name='Spo2LevelRecord', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('recorded', models.DateTimeField(validators=[medwings.validators.AbstractRecordValidator.recorded], verbose_name='Time at which measurement was taken')), + ('recorded', models.DateTimeField(unique=True, validators=[medwings.validators.AbstractRecordValidator.recorded], verbose_name='Time at which measurement was taken')), ('value_percent', models.PositiveIntegerField(validators=[medwings.validators.Spo2LevelRecordValidator.value_percent], verbose_name='SPO2 (%)')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], @@ -65,7 +65,7 @@ class Migration(migrations.Migration): name='RespirationScoreRecord', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('recorded', models.DateTimeField(validators=[medwings.validators.AbstractRecordValidator.recorded], verbose_name='Time at which measurement was taken')), + ('recorded', models.DateTimeField(unique=True, validators=[medwings.validators.AbstractRecordValidator.recorded], verbose_name='Time at which measurement was taken')), ('value_severity', models.PositiveIntegerField(choices=[(0, 'No shortness of breath'), (1, 'A little shortness of breath'), (2, 'Severe shortness of breath')], validators=[medwings.validators.RespirationScoreRecordValidator.value_severity], verbose_name='Shortness Of Breath Severity')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], @@ -74,7 +74,7 @@ class Migration(migrations.Migration): name='MewsRecord', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('recorded', models.DateTimeField(validators=[medwings.validators.AbstractRecordValidator.recorded], verbose_name='Time at which measurement was calculated')), + ('recorded', models.DateTimeField(unique=True, validators=[medwings.validators.AbstractRecordValidator.recorded], verbose_name='Time at which measurement was calculated')), ('value_n', models.PositiveIntegerField(validators=[medwings.validators.MewsRecordValidator.value_n], verbose_name='Modified Early Warning Score')), ('blood_pressure_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='medwings.bloodpressurerecord')), ('body_temp_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='medwings.bodytemprecord')), diff --git a/app/medwings/models.py b/app/medwings/models.py index 7ea6093..e7df6f9 100644 --- a/app/medwings/models.py +++ b/app/medwings/models.py @@ -19,23 +19,29 @@ class Profile(models.Model): class BloodPressureRecord(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - recorded = models.DateTimeField(validators=[validators.BloodPressureRecordValidator.recorded], verbose_name="Time at which measurement was taken") + recorded = models.DateTimeField(validators=[validators.BloodPressureRecordValidator.recorded], unique=True, verbose_name="Time at which measurement was taken") value_systolic_mmhg = models.PositiveIntegerField(validators=[validators.BloodPressureRecordValidator.value_systolic_mmhg], verbose_name="Systolic Blood Pressure (mmhg)") value_diastolic_mmhg = models.PositiveIntegerField(validators=[validators.BloodPressureRecordValidator.value_diastolic_mmhg], verbose_name="Diastolic Blood Pressure (mmhg)") class BodyTempRecord(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - recorded = models.DateTimeField(validators=[validators.BodyTempRecordValidator.recorded], verbose_name="Time at which measurement was taken") - value_celsius = models.PositiveIntegerField(validators=[validators.BodyTempRecordValidator.value_celsius], verbose_name="Body Temperature (\u00B0C)") + recorded = models.DateTimeField(validators=[validators.BodyTempRecordValidator.recorded], unique=True, verbose_name="Time at which measurement was taken") + value_celsius = models.DecimalField(max_digits=5, decimal_places=2, validators=[validators.BodyTempRecordValidator.value_celsius], unique=True, verbose_name="Body Temperature (\u00B0C)") class HeartRateRecord(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - recorded = models.DateTimeField(validators=[validators.HeartRateRecordValidator.recorded], verbose_name="Time at which measurement was taken") + recorded = models.DateTimeField(validators=[validators.HeartRateRecordValidator.recorded], unique=True, verbose_name="Time at which measurement was taken") value_bpm = models.PositiveIntegerField(validators=[validators.HeartRateRecordValidator.value_bpm], verbose_name="Heart Rate (bpm)") +class Spo2LevelRecord(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + recorded = models.DateTimeField(validators=[validators.Spo2LevelRecordValidator.recorded], unique=True, verbose_name="Time at which measurement was taken") + value_percent = models.PositiveIntegerField(validators=[validators.Spo2LevelRecordValidator.value_percent], verbose_name="SPO2 (\u0025)") + + class RespirationScoreRecord(models.Model): SEVERITY_NONE = 0 SEVERITY_LOW = 1 @@ -47,19 +53,13 @@ class RespirationScoreRecord(models.Model): ] user = models.ForeignKey(User, on_delete=models.CASCADE) - recorded = models.DateTimeField(validators=[validators.RespirationScoreRecordValidator.recorded], verbose_name="Time at which measurement was taken") + recorded = models.DateTimeField(validators=[validators.RespirationScoreRecordValidator.recorded], unique=True, verbose_name="Time at which measurement was taken") value_severity = models.PositiveIntegerField(choices=SEVERITY_CHOICES, validators=[validators.RespirationScoreRecordValidator.value_severity], verbose_name="Shortness Of Breath Severity") -class Spo2LevelRecord(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE) - recorded = models.DateTimeField(validators=[validators.Spo2LevelRecordValidator.recorded], verbose_name="Time at which measurement was taken") - value_percent = models.PositiveIntegerField(validators=[validators.Spo2LevelRecordValidator.value_percent], verbose_name="SPO2 (\u0025)") - - class MewsRecord(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) - recorded = models.DateTimeField(validators=[validators.MewsRecordValidator.recorded], verbose_name="Time at which measurement was calculated") + recorded = models.DateTimeField(validators=[validators.MewsRecordValidator.recorded], unique=True, verbose_name="Time at which measurement was calculated") value_n = models.PositiveIntegerField(validators=[validators.MewsRecordValidator.value_n], verbose_name="Modified Early Warning Score") blood_pressure_record = models.ForeignKey(BloodPressureRecord, on_delete=models.CASCADE) diff --git a/app/medwings/tests.py b/app/medwings/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/app/medwings/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/app/requirements.txt b/app/requirements.txt index 050dcb4..d4dd1f1 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -5,6 +5,7 @@ Django==4.2.3 idna==3.4 psycopg==3.1.9 psycopg-binary==3.1.9 +pytz==2023.3 requests==2.31.0 sqlparse==0.4.4 typing_extensions==4.7.1 diff --git a/app/withings/README.md b/app/withings/README.md new file mode 100644 index 0000000..fa12e74 --- /dev/null +++ b/app/withings/README.md @@ -0,0 +1,210 @@ +# Withings API + +## Token expiry + +When the access token expires, HTTP status `200 OK` is returned, but the response body is as follows: + +```json +{ + "status": 401, + "body": {}, + "error": "XRequestID: Not provided invalid_token: The access token provided is invalid" +} +``` + +## Fetching health data + +Health records can be fetched via GET request as follows: + +```http +https://wbsapi.withings.net/measure?action=getmeas&meastypes=9,10,54,71,11 +``` + +The type of vitals measurement is mapped as follows: + +| Code | Type | Unit | +|------|--------------------------|------| +| 9 | Diastolic Blood Pressure | mmHg | +| 10 | Systolic Blood Pressure | mmHg | +| 11 | Heart Rate | bpm | +| 54 | SP02 | % | +| 71 | Body Temperature | °C | + +Note the `unit`-field in the response. +For body temperature, the `unit`-field has the value `-3`. +This means that to get the body temperature in °C, you must multiply the `value` by `10^(-3)`. + +The time of measurement can be parsed from the `measuregrps`'s `date` field. + +A successful response looks like so: + +```json +{ + "status": 0, + "body": { + "updatetime": 1690491663, + "timezone": "Europe/Berlin", + "measuregrps": [ + { + "grpid": 4716596696, + "attrib": 0, + "date": 1690491576, + "created": 1690491663, + "modified": 1690491663, + "category": 1, + "deviceid": "c405eb8e0e053f6601e151c7e43c04e29aad6956", + "hash_deviceid": "c405eb8e0e053f6601e151c7e43c04e29aad6956", + "measures": [ + { + "value": 89, + "type": 9, + "unit": 0, + "algo": 0, + "fm": 3 + }, + { + "value": 109, + "type": 10, + "unit": 0, + "algo": 0, + "fm": 3 + }, + { + "value": 88, + "type": 11, + "unit": 0, + "algo": 0, + "fm": 3 + } + ], + "modelid": 44, + "model": "BPM Core", + "comment": null + }, + { + "grpid": 4716596681, + "attrib": 0, + "date": 1690491236, + "created": 1690491662, + "modified": 1690491662, + "category": 1, + "deviceid": "c405eb8e0e053f6601e151c7e43c04e29aad6956", + "hash_deviceid": "c405eb8e0e053f6601e151c7e43c04e29aad6956", + "measures": [ + { + "value": 65, + "type": 9, + "unit": 0, + "algo": 0, + "fm": 3 + }, + { + "value": 92, + "type": 10, + "unit": 0, + "algo": 0, + "fm": 3 + }, + { + "value": 88, + "type": 11, + "unit": 0, + "algo": 0, + "fm": 3 + } + ], + "modelid": 44, + "model": "BPM Core", + "comment": null + }, + { + "grpid": 4712963495, + "attrib": 0, + "date": 1690375238, + "created": 1690375243, + "modified": 1690375243, + "category": 1, + "deviceid": "dbf7f61809d5fb350a16a50f6af6e826f0746082", + "hash_deviceid": "dbf7f61809d5fb350a16a50f6af6e826f0746082", + "measures": [ + { + "value": 99, + "type": 54, + "unit": 0, + "algo": 33619971, + "fm": 3, + "apppfmid": 9, + "appliver": 2741, + "algo_params": { + "1": 0, + "2": 15 + } + } + ], + "modelid": null, + "model": null, + "comment": null, + "is_inconclusive": false + }, + { + "grpid": 4712927310, + "attrib": 1, + "date": 1690374434, + "created": 1690374456, + "modified": 1690374486, + "category": 1, + "deviceid": "1d453daf947378fac40677e7a085eea73750b061", + "hash_deviceid": "1d453daf947378fac40677e7a085eea73750b061", + "measures": [ + { + "value": 37370, + "type": 71, + "unit": -3, + "algo": 0, + "fm": 0 + } + ], + "modelid": null, + "model": null, + "comment": null + }, + { + "grpid": 4712911433, + "attrib": 0, + "date": 1690373994, + "created": 1690374078, + "modified": 1690374078, + "category": 1, + "deviceid": "c405eb8e0e053f6601e151c7e43c04e29aad6956", + "hash_deviceid": "c405eb8e0e053f6601e151c7e43c04e29aad6956", + "measures": [ + { + "value": 88, + "type": 9, + "unit": 0, + "algo": 0, + "fm": 3 + }, + { + "value": 124, + "type": 10, + "unit": 0, + "algo": 0, + "fm": 3 + }, + { + "value": 70, + "type": 11, + "unit": 0, + "algo": 0, + "fm": 3 + } + ], + "modelid": null, + "model": null, + "comment": null + } + ] + } +} +``` diff --git a/app/withings/admin.py b/app/withings/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/app/withings/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/app/withings/api.py b/app/withings/api.py index f9e2e8d..8d4fdfc 100644 --- a/app/withings/api.py +++ b/app/withings/api.py @@ -1,11 +1,15 @@ -from datetime import timedelta +from datetime import datetime, timedelta from random import randint import requests +import pytz from django.conf import settings from django.utils import timezone +from django.contrib.auth.models import User from urllib.parse import urlencode +from medwings import models as mm + def fetch_initial_tokens(authorization_code, redirect_uri): data = { 'action': 'requesttoken', @@ -49,3 +53,64 @@ def save_tokens_to_session(request, response_data): now = timezone.now() request.session['withings_access_token_expiry'] = (now + timedelta(seconds=response_data['body']['expires_in'])).isoformat() request.session['withings_refresh_token_expiry'] = (now + timedelta(days=365)).isoformat() + + +def parse_getmeas_response(response_body: dict, user: User) -> list: + body = response_body['body'] + records = [] + + timezone = pytz.timezone(body['timezone']) + + for measure_group in body['measuregrps']: + recorded = timezone.localize(datetime.fromtimestamp(measure_group['date'])) + + blood_pressure_systolic_value = None + blood_pressure_diastolic_value = None + body_temperature_value = None + heart_rate_value = None + spo2_level_value = None + for measure in measure_group['measures']: + measure_type = measure['type'] + measure_value = measure['value'] + measure_unit = measure['unit'] + + measure_value_adjusted = measure_value * (10 ** measure_unit) + + if measure_type == 9: + blood_pressure_diastolic_value = measure_value_adjusted + elif measure_type == 10: + blood_pressure_systolic_value = measure_value_adjusted + elif measure_type == 11: + heart_rate_value = measure_value_adjusted + elif measure_type == 54: + spo2_level_value = measure_value_adjusted + elif measure_type == 71: + body_temperature_value = measure_value_adjusted + + if blood_pressure_systolic_value and blood_pressure_diastolic_value: + records.append(mm.BloodPressureRecord( + user=user, + recorded=recorded, + value_systolic_mmhg=blood_pressure_systolic_value, + value_diastolic_mmhg=blood_pressure_diastolic_value + )) + if body_temperature_value: + records.append(mm.BodyTempRecord( + user=user, + recorded=recorded, + value_celsius=body_temperature_value + )) + if heart_rate_value: + records.append(mm.HeartRateRecord( + user=user, + recorded=recorded, + value_bpm=heart_rate_value + )) + if spo2_level_value: + records.append(mm.Spo2LevelRecord( + user=user, + recorded=recorded, + value_percent=spo2_level_value + )) + + return records diff --git a/app/withings/migrations/0001_initial.py b/app/withings/migrations/0001_initial.py index 9be9273..5073b30 100644 --- a/app/withings/migrations/0001_initial.py +++ b/app/withings/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.3 on 2023-07-27 14:35 +# Generated by Django 4.2.3 on 2023-07-29 18:35 from django.conf import settings from django.db import migrations, models @@ -19,6 +19,7 @@ class Migration(migrations.Migration): fields=[ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), ('userid', models.PositiveIntegerField(verbose_name='Withings API User ID')), + ('last_update', models.DateTimeField(default=None, null=True, verbose_name='Time of last synchronization with Withings API')), ], ), migrations.CreateModel( diff --git a/app/withings/models.py b/app/withings/models.py index 5406449..23d35d3 100644 --- a/app/withings/models.py +++ b/app/withings/models.py @@ -1,13 +1,13 @@ -from datetime import timedelta -import logging +from datetime import datetime, timedelta -from django.db import models +from django.db import models, IntegrityError from django.contrib.auth.models import User from django.conf import settings from django.utils import timezone - import requests +from . import api + class AccessToken(models.Model): account = models.OneToOneField("ApiAccount", on_delete=models.CASCADE, primary_key=True) @@ -24,6 +24,7 @@ class RefreshToken(models.Model): class ApiAccount(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) userid = models.PositiveIntegerField(verbose_name="Withings API User ID") + last_update = models.DateTimeField(null=True, default=None, verbose_name="Time of last synchronization with Withings API") def refresh_tokens(self): data = { @@ -49,3 +50,41 @@ class ApiAccount(models.Model): self.refreshtoken.expires = now + timedelta(days=365) self.accesstoken.save() self.refreshtoken.save() + + + def get_measurements(self, since: datetime | None = None) -> list: + if self.accesstoken.expires < timezone.now(): + self.refresh_tokens() + + params={ + 'action': 'getmeas', + 'meastypes': '9,10,11,54,71' + } + if since: + params['lastupdate'] = str(int(since.timestamp())) + + response = requests.get( + url="https://wbsapi.withings.net/measure", + params=params, + headers={ + 'Authorization': f"Bearer {self.accesstoken.value}" + } + ) + if response is not None: + response.raise_for_status() + data = response.json() + if data['status'] != 0: + raise RuntimeError(f"Received status {data['status']} while retrieving measurements: {data['error']}") + + return api.parse_getmeas_response(data, self.user) + + + def update_records(self): + records = self.get_measurements(self.last_update) + for record in records: + try: + record.save() + except IntegrityError: + pass + self.last_update = timezone.now() + self.save() diff --git a/app/withings/tests.py b/app/withings/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/app/withings/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/app/withings/views.py b/app/withings/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/app/withings/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here.