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:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- 2007:2007
|
||||
depends_on:
|
||||
- influxdbV1
|
||||
volumes:
|
||||
- .:/app
|
||||
influxdbV1:
|
||||
image: influxdb:1.8.4
|
||||
container_name: reachableceo-health-data
|
||||
ports:
|
||||
- "8086:8086"
|
||||
- "28086:8086"
|
||||
volumes:
|
||||
- reachableceo-health-data:/var/lib/influxdb
|
||||
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
|
||||
container_name: cleanslate-client
|
||||
ports:
|
||||
- '2010:3000'
|
||||
- '2006:3000'
|
||||
depends_on:
|
||||
- cleanslate-database
|
||||
- cleanslate-graphql-server
|
||||
|
@@ -4,7 +4,7 @@ services:
|
||||
image: ghcr.io/majorpeter/atomic-tracker:latest
|
||||
container_name: reachableceo-atomichabits
|
||||
ports:
|
||||
- "2008:8080"
|
||||
- "2005:8080"
|
||||
volumes:
|
||||
- tsys-atomichabits:/config
|
||||
restart: always
|
||||
|
@@ -38,22 +38,22 @@
|
||||
{
|
||||
"name": "Reactive Resume",
|
||||
"category": "",
|
||||
"url": "http://CharlesDevServer.knel.net:2007",
|
||||
"url": "http://CharlesDevServer.knel.net:2003",
|
||||
},
|
||||
{
|
||||
"name": "Atomic Habits",
|
||||
"category": "",
|
||||
"url": "http://CharlesDevServer.knel.net:2008"
|
||||
"url": "http://CharlesDevServer.knel.net:2005"
|
||||
},
|
||||
{
|
||||
"name": "Cleanslate",
|
||||
"category": "",
|
||||
"url": "http://CharlesDevServer.knel.net:2010"
|
||||
"url": "http://CharlesDevServer.knel.net:2006"
|
||||
},
|
||||
{
|
||||
"name": "Apple Health Exporter",
|
||||
"category": "",
|
||||
"url": "http://CharlesDevServer.knel.net:4000",
|
||||
"url": "http://CharlesDevServer.knel.net:2007",
|
||||
},
|
||||
],
|
||||
"notes": [
|
||||
|
@@ -52,7 +52,7 @@ services:
|
||||
container_name: tsys-reactiveresume-app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "2007:3000"
|
||||
- "2003:3000"
|
||||
depends_on:
|
||||
- reactiveresume-postgres
|
||||
- reactiveresume-minio
|
||||
|
Reference in New Issue
Block a user