apple health data will soon be out in the world...
This commit is contained in:
129
inprep/applehealth-grafana/.gitignore
vendored
Executable file
129
inprep/applehealth-grafana/.gitignore
vendored
Executable file
@@ -0,0 +1,129 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
6
inprep/applehealth-grafana/Dockerfile
Normal file
6
inprep/applehealth-grafana/Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
FROM python:3.9
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
COPY . .
|
||||||
|
CMD ["python", "app.py"]
|
21
inprep/applehealth-grafana/LICENSE
Executable file
21
inprep/applehealth-grafana/LICENSE
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Ivaylo Pavlov
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
1
inprep/applehealth-grafana/README.md
Executable file
1
inprep/applehealth-grafana/README.md
Executable file
@@ -0,0 +1 @@
|
|||||||
|
A step by step setup guide is available in this [blog post on Ivo's Blog](https://www.ivaylopavlov.com/charting-apple-healthkit-data-in-grafana/).
|
107
inprep/applehealth-grafana/app.py
Executable file
107
inprep/applehealth-grafana/app.py
Executable file
@@ -0,0 +1,107 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import socket
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import request, Flask
|
||||||
|
from influxdb import InfluxDBClient
|
||||||
|
from geolib import geohash
|
||||||
|
|
||||||
|
DATAPOINTS_CHUNK = 80000
|
||||||
|
|
||||||
|
logger = logging.getLogger("console-output")
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
handler.setLevel(logging.DEBUG)
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.debug = True
|
||||||
|
|
||||||
|
client = InfluxDBClient(host='localhost', port=28086)
|
||||||
|
client.create_database('db')
|
||||||
|
client.switch_database('db')
|
||||||
|
|
||||||
|
@app.route('/collect', methods=['POST', 'GET'])
|
||||||
|
def collect():
|
||||||
|
logger.info(f"Request received")
|
||||||
|
|
||||||
|
healthkit_data = None
|
||||||
|
transformed_data = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
healthkit_data = json.loads(request.data)
|
||||||
|
except:
|
||||||
|
return "Invalid JSON Received", 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"Ingesting Metrics")
|
||||||
|
for metric in healthkit_data.get("data", {}).get("metrics", []):
|
||||||
|
number_fields = []
|
||||||
|
string_fields = []
|
||||||
|
for datapoint in metric["data"]:
|
||||||
|
metric_fields = set(datapoint.keys())
|
||||||
|
metric_fields.remove("date")
|
||||||
|
for mfield in metric_fields:
|
||||||
|
if type(datapoint[mfield]) == int or type(datapoint[mfield]) == float:
|
||||||
|
number_fields.append(mfield)
|
||||||
|
else:
|
||||||
|
string_fields.append(mfield)
|
||||||
|
point = {
|
||||||
|
"measurement": metric["name"],
|
||||||
|
"time": datapoint["date"],
|
||||||
|
"tags": {str(nfield): str(datapoint[nfield]) for nfield in string_fields},
|
||||||
|
"fields": {str(nfield): float(datapoint[nfield]) for nfield in number_fields}
|
||||||
|
}
|
||||||
|
transformed_data.append(point)
|
||||||
|
number_fields.clear()
|
||||||
|
string_fields.clear()
|
||||||
|
|
||||||
|
logger.info(f"Data Transformation Complete")
|
||||||
|
logger.info(f"Number of data points to write: {len(transformed_data)}")
|
||||||
|
logger.info(f"DB Write Started")
|
||||||
|
|
||||||
|
for i in range(0, len(transformed_data), DATAPOINTS_CHUNK):
|
||||||
|
logger.info(f"DB Writing chunk")
|
||||||
|
client.write_points(transformed_data[i:i + DATAPOINTS_CHUNK])
|
||||||
|
|
||||||
|
logger.info(f"DB Metrics Write Complete")
|
||||||
|
logger.info(f"Ingesting Workouts Routes")
|
||||||
|
|
||||||
|
transformed_workout_data = []
|
||||||
|
|
||||||
|
for workout in healthkit_data.get("data", {}).get("workouts", []):
|
||||||
|
tags = {
|
||||||
|
"id": workout["name"] + "-" + workout["start"] + "-" + workout["end"]
|
||||||
|
}
|
||||||
|
for gps_point in workout["route"]:
|
||||||
|
point = {
|
||||||
|
"measurement": "workouts",
|
||||||
|
"time": gps_point["timestamp"],
|
||||||
|
"tags": tags,
|
||||||
|
"fields": {
|
||||||
|
"lat": gps_point["lat"],
|
||||||
|
"lng": gps_point["lon"],
|
||||||
|
"geohash": geohash.encode(gps_point["lat"], gps_point["lon"], 7),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transformed_workout_data.append(point)
|
||||||
|
|
||||||
|
for i in range(0, len(transformed_workout_data), DATAPOINTS_CHUNK):
|
||||||
|
logger.info(f"DB Writing chunk")
|
||||||
|
client.write_points(transformed_workout_data[i:i + DATAPOINTS_CHUNK])
|
||||||
|
|
||||||
|
logger.info(f"Ingesting Workouts Complete")
|
||||||
|
except:
|
||||||
|
logger.exception("Caught Exception. See stacktrace for details.")
|
||||||
|
return "Server Error", 500
|
||||||
|
|
||||||
|
return "Success", 200
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
ip_address = socket.gethostbyname(hostname)
|
||||||
|
logger.info(f"Local Network Endpoint: http://{ip_address}/collect")
|
||||||
|
app.run(host='0.0.0.0', port=5353)
|
@@ -1,9 +1,17 @@
|
|||||||
services:
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- 2007:2007
|
||||||
|
depends_on:
|
||||||
|
- influxdbV1
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
influxdbV1:
|
influxdbV1:
|
||||||
image: influxdb:1.8.4
|
image: influxdb:1.8.4
|
||||||
container_name: reachableceo-health-data
|
container_name: reachableceo-health-data
|
||||||
ports:
|
ports:
|
||||||
- "8086:8086"
|
- "28086:8086"
|
||||||
volumes:
|
volumes:
|
||||||
- reachableceo-health-data:/var/lib/influxdb
|
- reachableceo-health-data:/var/lib/influxdb
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
3
inprep/applehealth-grafana/requirements.txt
Executable file
3
inprep/applehealth-grafana/requirements.txt
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
flask
|
||||||
|
influxdb
|
||||||
|
geolib
|
37
inprep/applehealth-grafana/sample-data.json
Executable file
37
inprep/applehealth-grafana/sample-data.json
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"data" : {
|
||||||
|
"workouts" : [],
|
||||||
|
"metrics" : [
|
||||||
|
{
|
||||||
|
"data" : [
|
||||||
|
{
|
||||||
|
"qty" : 34,
|
||||||
|
"date" : "2021-03-14 00:01:00 +0000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date" : "2021-03-14 00:51:00 +0000",
|
||||||
|
"qty" : 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"qty" : 8.2796763649358063,
|
||||||
|
"date" : "2021-03-14 01:13:00 +0000"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date" : "2021-03-14 01:14:00 +0000",
|
||||||
|
"qty" : 17.719295979506633
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date" : "2021-03-14 01:15:00 +0000",
|
||||||
|
"qty" : 17.719295979506636
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date" : "2021-03-14 01:16:00 +0000",
|
||||||
|
"qty" : 7.2817316760509208
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name" : "step_count",
|
||||||
|
"units" : "count"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@@ -78,7 +78,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
container_name: cleanslate-client
|
container_name: cleanslate-client
|
||||||
ports:
|
ports:
|
||||||
- '2010:3000'
|
- '2006:3000'
|
||||||
depends_on:
|
depends_on:
|
||||||
- cleanslate-database
|
- cleanslate-database
|
||||||
- cleanslate-graphql-server
|
- cleanslate-graphql-server
|
||||||
|
@@ -4,7 +4,7 @@ services:
|
|||||||
image: ghcr.io/majorpeter/atomic-tracker:latest
|
image: ghcr.io/majorpeter/atomic-tracker:latest
|
||||||
container_name: reachableceo-atomichabits
|
container_name: reachableceo-atomichabits
|
||||||
ports:
|
ports:
|
||||||
- "2008:8080"
|
- "2005:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- tsys-atomichabits:/config
|
- tsys-atomichabits:/config
|
||||||
restart: always
|
restart: always
|
||||||
|
@@ -38,22 +38,22 @@
|
|||||||
{
|
{
|
||||||
"name": "Reactive Resume",
|
"name": "Reactive Resume",
|
||||||
"category": "",
|
"category": "",
|
||||||
"url": "http://CharlesDevServer.knel.net:2007",
|
"url": "http://CharlesDevServer.knel.net:2003",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Atomic Habits",
|
"name": "Atomic Habits",
|
||||||
"category": "",
|
"category": "",
|
||||||
"url": "http://CharlesDevServer.knel.net:2008"
|
"url": "http://CharlesDevServer.knel.net:2005"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Cleanslate",
|
"name": "Cleanslate",
|
||||||
"category": "",
|
"category": "",
|
||||||
"url": "http://CharlesDevServer.knel.net:2010"
|
"url": "http://CharlesDevServer.knel.net:2006"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Apple Health Exporter",
|
"name": "Apple Health Exporter",
|
||||||
"category": "",
|
"category": "",
|
||||||
"url": "http://CharlesDevServer.knel.net:4000",
|
"url": "http://CharlesDevServer.knel.net:2007",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"notes": [
|
"notes": [
|
||||||
|
@@ -52,7 +52,7 @@ services:
|
|||||||
container_name: tsys-reactiveresume-app
|
container_name: tsys-reactiveresume-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "2007:3000"
|
- "2003:3000"
|
||||||
depends_on:
|
depends_on:
|
||||||
- reactiveresume-postgres
|
- reactiveresume-postgres
|
||||||
- reactiveresume-minio
|
- reactiveresume-minio
|
||||||
|
Reference in New Issue
Block a user