merge experimental #1
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
# Create your models here.
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -108,7 +108,8 @@ def register_continue(request):
|
|||||||
instance.save()
|
instance.save()
|
||||||
request.session.flush()
|
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
|
# TODO redirect user to some other page and ask them to log in
|
||||||
return redirect('dashboard')
|
return redirect('dashboard')
|
||||||
|
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
@ -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.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
@ -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.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
|||||||
name='BloodPressureRecord',
|
name='BloodPressureRecord',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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_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)')),
|
('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)),
|
('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',
|
name='BodyTempRecord',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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_celsius', models.PositiveIntegerField(validators=[medwings.validators.BodyTempRecordValidator.value_celsius], verbose_name='Body Temperature (°C)')),
|
('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)),
|
('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',
|
name='HeartRateRecord',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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)')),
|
('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)),
|
('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',
|
name='Spo2LevelRecord',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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 (%)')),
|
('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)),
|
('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',
|
name='RespirationScoreRecord',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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')),
|
('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)),
|
('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',
|
name='MewsRecord',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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')),
|
('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')),
|
('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')),
|
('body_temp_record', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='medwings.bodytemprecord')),
|
||||||
|
@ -19,23 +19,29 @@ class Profile(models.Model):
|
|||||||
|
|
||||||
class BloodPressureRecord(models.Model):
|
class BloodPressureRecord(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
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_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)")
|
value_diastolic_mmhg = models.PositiveIntegerField(validators=[validators.BloodPressureRecordValidator.value_diastolic_mmhg], verbose_name="Diastolic Blood Pressure (mmhg)")
|
||||||
|
|
||||||
|
|
||||||
class BodyTempRecord(models.Model):
|
class BodyTempRecord(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
recorded = models.DateTimeField(validators=[validators.BodyTempRecordValidator.recorded], verbose_name="Time at which measurement was taken")
|
recorded = models.DateTimeField(validators=[validators.BodyTempRecordValidator.recorded], unique=True, verbose_name="Time at which measurement was taken")
|
||||||
value_celsius = models.PositiveIntegerField(validators=[validators.BodyTempRecordValidator.value_celsius], verbose_name="Body Temperature (\u00B0C)")
|
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):
|
class HeartRateRecord(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
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)")
|
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):
|
class RespirationScoreRecord(models.Model):
|
||||||
SEVERITY_NONE = 0
|
SEVERITY_NONE = 0
|
||||||
SEVERITY_LOW = 1
|
SEVERITY_LOW = 1
|
||||||
@ -47,19 +53,13 @@ class RespirationScoreRecord(models.Model):
|
|||||||
]
|
]
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
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")
|
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):
|
class MewsRecord(models.Model):
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
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")
|
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)
|
blood_pressure_record = models.ForeignKey(BloodPressureRecord, on_delete=models.CASCADE)
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -5,6 +5,7 @@ Django==4.2.3
|
|||||||
idna==3.4
|
idna==3.4
|
||||||
psycopg==3.1.9
|
psycopg==3.1.9
|
||||||
psycopg-binary==3.1.9
|
psycopg-binary==3.1.9
|
||||||
|
pytz==2023.3
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
sqlparse==0.4.4
|
sqlparse==0.4.4
|
||||||
typing_extensions==4.7.1
|
typing_extensions==4.7.1
|
||||||
|
210
app/withings/README.md
Normal file
210
app/withings/README.md
Normal file
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -1,3 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
@ -1,11 +1,15 @@
|
|||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import pytz
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
from medwings import models as mm
|
||||||
|
|
||||||
def fetch_initial_tokens(authorization_code, redirect_uri):
|
def fetch_initial_tokens(authorization_code, redirect_uri):
|
||||||
data = {
|
data = {
|
||||||
'action': 'requesttoken',
|
'action': 'requesttoken',
|
||||||
@ -49,3 +53,64 @@ def save_tokens_to_session(request, response_data):
|
|||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
request.session['withings_access_token_expiry'] = (now + timedelta(seconds=response_data['body']['expires_in'])).isoformat()
|
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()
|
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
|
||||||
|
@ -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.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@ -19,6 +19,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
|
('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')),
|
('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(
|
migrations.CreateModel(
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models, IntegrityError
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
|
||||||
|
|
||||||
class AccessToken(models.Model):
|
class AccessToken(models.Model):
|
||||||
account = models.OneToOneField("ApiAccount", on_delete=models.CASCADE, primary_key=True)
|
account = models.OneToOneField("ApiAccount", on_delete=models.CASCADE, primary_key=True)
|
||||||
@ -24,6 +24,7 @@ class RefreshToken(models.Model):
|
|||||||
class ApiAccount(models.Model):
|
class ApiAccount(models.Model):
|
||||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||||
userid = models.PositiveIntegerField(verbose_name="Withings API User ID")
|
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):
|
def refresh_tokens(self):
|
||||||
data = {
|
data = {
|
||||||
@ -49,3 +50,41 @@ class ApiAccount(models.Model):
|
|||||||
self.refreshtoken.expires = now + timedelta(days=365)
|
self.refreshtoken.expires = now + timedelta(days=365)
|
||||||
self.accesstoken.save()
|
self.accesstoken.save()
|
||||||
self.refreshtoken.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()
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
|
Loading…
Reference in New Issue
Block a user