├── .dockerignore ├── .env.example ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── Dockerfile.grafana ├── README.md ├── dashboard.json ├── db.py ├── docker-compose.yaml ├── docs ├── config.png ├── dashboard.jpeg ├── import.png └── postgresql.jpeg ├── poetry.lock ├── pyproject.toml └── server.py /.dockerignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | ### Python Patch ### 167 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 168 | poetry.toml 169 | 170 | # ruff 171 | .ruff_cache/ 172 | 173 | # LSP config files 174 | pyrightconfig.json 175 | 176 | # End of https://www.toptal.com/developers/gitignore/api/python 177 | n 178 | data 179 | grafana -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres 8 | GF_SECURITY_ADMIN_USER="admin" 9 | GF_SECURITY_ADMIN_PASSWORD="admin" # You should change this -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build Image CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | build-and-push-image: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 33 | with: 34 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 35 | 36 | - name: Build and push Docker image 37 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 38 | with: 39 | context: . 40 | push: true 41 | tags: ${{ steps.meta.outputs.tags }} 42 | labels: ${{ steps.meta.outputs.labels }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | ### Python Patch ### 167 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 168 | poetry.toml 169 | 170 | # ruff 171 | .ruff_cache/ 172 | 173 | # LSP config files 174 | pyrightconfig.json 175 | 176 | # End of https://www.toptal.com/developers/gitignore/api/python 177 | n 178 | data 179 | grafana -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.black-formatter" 4 | }, 5 | "python.formatting.provider": "none" 6 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | WORKDIR /app 3 | COPY poetry.lock pyproject.toml /app/ 4 | RUN pip install poetry &&\ 5 | poetry config virtualenvs.create false &&\ 6 | poetry install --no-dev 7 | COPY . /app 8 | ENV PORT=8000 9 | EXPOSE 8000 10 | ENV HOST="::" 11 | CMD bash -c "python -m uvicorn server:app --host $HOST --port $PORT" -------------------------------------------------------------------------------- /Dockerfile.grafana: -------------------------------------------------------------------------------- 1 | ARG VERSION=latest 2 | FROM grafana/grafana-enterprise:${VERSION} 3 | USER root -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Welcome to apple-health-exporter 👋

2 | 3 |

4 | 5 | License: MIT 6 | 7 | 8 | Twitter: fuergaosi 9 | 10 |

11 | 12 | > Explore your apple health with Grafana 13 | 14 | ![Dashbaord](./docs/dashboard.jpeg) 15 | ### 🏠 [Homepage](https://github.com/fuergaosi233/apple-health-exporter) | ✨ [Demo](https://grafana-health.y1s1.host/goto/egkRFfmIR?orgId=1) 16 | 17 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/qZmI-e?referralCode=FaJtD_) 18 | 19 | ## Install 20 | 21 | ```sh 22 | copy .env.example .env 23 | # edit .env 24 | docker compose up -d 25 | ``` 26 | 27 | ## Config Grafana 28 | 29 | 30 | 1. Config Postgresql DB (Timescale) 31 | ![DB](./docs/postgresql.jpeg) 32 | 1. Import `dashboard.json` to your dashboard 33 | ![DB](./docs/import.png) 34 | 1. Enjoy it 35 | 36 | ## Start Sync data 37 | 38 | 1. Download [Health Auto Export - JSON+CSV](https://apps.apple.com/us/app/health-auto-export-json-csv/id1115567069) from App Store 39 | 2. Config Automations like this 40 | ![Config](./docs/config.png) 41 | > You might have to pay for it. There's a not free app. You can use `Shortcuts` to do this. But I don't know how to do it. If you know, please tell me. 42 | 43 | URL is `/upload` 44 | Such as 45 | `http://localhost:8000/upload` 46 | `https://xxx.railway.app/upload` 47 | 48 | 3. Click `Update` 49 | 50 | 👤 **Holegots** 51 | 52 | * Twitter: [@fuergaosi](https://twitter.com/fuergaosi) 53 | * Github: [@fuergaosi233](https://github.com/fuergaosi233) 54 | 55 | ## Show your support 56 | 57 | Give a ⭐️ if this project helped you! 58 | 59 | *** 60 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ -------------------------------------------------------------------------------- /dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "annotations": { 3 | "list": [ 4 | { 5 | "builtIn": 1, 6 | "datasource": { 7 | "type": "grafana", 8 | "uid": "-- Grafana --" 9 | }, 10 | "enable": true, 11 | "hide": true, 12 | "iconColor": "rgba(0, 211, 255, 1)", 13 | "name": "Annotations & Alerts", 14 | "type": "dashboard" 15 | } 16 | ] 17 | }, 18 | "editable": true, 19 | "fiscalYearStartMonth": 0, 20 | "graphTooltip": 0, 21 | "id": 1, 22 | "links": [], 23 | "liveNow": false, 24 | "panels": [ 25 | { 26 | "datasource": {}, 27 | "fieldConfig": { 28 | "defaults": { 29 | "color": { 30 | "mode": "palette-classic" 31 | }, 32 | "custom": { 33 | "axisCenteredZero": false, 34 | "axisColorMode": "text", 35 | "axisLabel": "", 36 | "axisPlacement": "auto", 37 | "barAlignment": 0, 38 | "drawStyle": "line", 39 | "fillOpacity": 0, 40 | "gradientMode": "none", 41 | "hideFrom": { 42 | "legend": false, 43 | "tooltip": false, 44 | "viz": false 45 | }, 46 | "insertNulls": false, 47 | "lineInterpolation": "smooth", 48 | "lineStyle": { 49 | "dash": [ 50 | 10, 51 | 10 52 | ], 53 | "fill": "dash" 54 | }, 55 | "lineWidth": 1, 56 | "pointSize": 5, 57 | "scaleDistribution": { 58 | "type": "linear" 59 | }, 60 | "showPoints": "auto", 61 | "spanNulls": false, 62 | "stacking": { 63 | "group": "A", 64 | "mode": "none" 65 | }, 66 | "thresholdsStyle": { 67 | "mode": "off" 68 | } 69 | }, 70 | "mappings": [], 71 | "thresholds": { 72 | "mode": "absolute", 73 | "steps": [ 74 | { 75 | "color": "green", 76 | "value": null 77 | }, 78 | { 79 | "color": "red", 80 | "value": 80 81 | } 82 | ] 83 | } 84 | }, 85 | "overrides": [] 86 | }, 87 | "gridPos": { 88 | "h": 12, 89 | "w": 7, 90 | "x": 0, 91 | "y": 0 92 | }, 93 | "id": 1, 94 | "options": { 95 | "legend": { 96 | "calcs": [], 97 | "displayMode": "list", 98 | "placement": "bottom", 99 | "showLegend": true 100 | }, 101 | "tooltip": { 102 | "mode": "single", 103 | "sort": "none" 104 | } 105 | }, 106 | "pluginVersion": "10.2.0-60477", 107 | "targets": [ 108 | { 109 | "datasource": { 110 | "type": "postgres", 111 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 112 | }, 113 | "editorMode": "code", 114 | "format": "time_series", 115 | "rawQuery": true, 116 | "rawSql": "SELECT\n (data ->> 'qty') :: float AS kg,\n timestamp AS time\nFROM\n metrics\nWHERE\n name = 'weight_body_mass'\n AND \"timestamp\" >= $__timeFrom()\n AND \"timestamp\" <= $__timeTo();", 117 | "refId": "A", 118 | "sql": { 119 | "columns": [ 120 | { 121 | "parameters": [], 122 | "type": "function" 123 | } 124 | ], 125 | "groupBy": [ 126 | { 127 | "property": { 128 | "type": "string" 129 | }, 130 | "type": "groupBy" 131 | } 132 | ], 133 | "limit": 50 134 | } 135 | } 136 | ], 137 | "title": "体重/身体质量", 138 | "type": "timeseries" 139 | }, 140 | { 141 | "datasource": {}, 142 | "fieldConfig": { 143 | "defaults": { 144 | "color": { 145 | "mode": "palette-classic" 146 | }, 147 | "custom": { 148 | "axisCenteredZero": false, 149 | "axisColorMode": "text", 150 | "axisLabel": "", 151 | "axisPlacement": "auto", 152 | "fillOpacity": 80, 153 | "gradientMode": "none", 154 | "hideFrom": { 155 | "legend": false, 156 | "tooltip": false, 157 | "viz": false 158 | }, 159 | "lineWidth": 1, 160 | "scaleDistribution": { 161 | "type": "linear" 162 | }, 163 | "thresholdsStyle": { 164 | "mode": "off" 165 | } 166 | }, 167 | "mappings": [], 168 | "thresholds": { 169 | "mode": "absolute", 170 | "steps": [ 171 | { 172 | "color": "green", 173 | "value": null 174 | }, 175 | { 176 | "color": "red", 177 | "value": 80 178 | } 179 | ] 180 | } 181 | }, 182 | "overrides": [] 183 | }, 184 | "gridPos": { 185 | "h": 12, 186 | "w": 7, 187 | "x": 7, 188 | "y": 0 189 | }, 190 | "id": 4, 191 | "options": { 192 | "barRadius": 0, 193 | "barWidth": 1, 194 | "fullHighlight": false, 195 | "groupWidth": 0.7, 196 | "legend": { 197 | "calcs": [], 198 | "displayMode": "list", 199 | "placement": "bottom", 200 | "showLegend": true 201 | }, 202 | "orientation": "auto", 203 | "showValue": "always", 204 | "stacking": "none", 205 | "tooltip": { 206 | "mode": "single", 207 | "sort": "none" 208 | }, 209 | "xTickLabelRotation": 0, 210 | "xTickLabelSpacing": 200 211 | }, 212 | "pluginVersion": "10.2.0-60477", 213 | "targets": [ 214 | { 215 | "datasource": { 216 | "type": "postgres", 217 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 218 | }, 219 | "editorMode": "code", 220 | "format": "time_series", 221 | "rawQuery": true, 222 | "rawSql": "SELECT\n COALESCE(SUM((data ->> 'qty') :: float), 0) AS km,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'walking_running_distance'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 223 | "refId": "A", 224 | "sql": { 225 | "columns": [ 226 | { 227 | "parameters": [], 228 | "type": "function" 229 | } 230 | ], 231 | "groupBy": [ 232 | { 233 | "property": { 234 | "type": "string" 235 | }, 236 | "type": "groupBy" 237 | } 238 | ], 239 | "limit": 50 240 | } 241 | } 242 | ], 243 | "title": "步行/跑步距离", 244 | "type": "barchart" 245 | }, 246 | { 247 | "datasource": {}, 248 | "fieldConfig": { 249 | "defaults": { 250 | "color": { 251 | "mode": "palette-classic" 252 | }, 253 | "custom": { 254 | "axisCenteredZero": false, 255 | "axisColorMode": "text", 256 | "axisLabel": "", 257 | "axisPlacement": "auto", 258 | "fillOpacity": 80, 259 | "gradientMode": "hue", 260 | "hideFrom": { 261 | "legend": false, 262 | "tooltip": false, 263 | "viz": false 264 | }, 265 | "lineWidth": 1, 266 | "scaleDistribution": { 267 | "type": "linear" 268 | }, 269 | "thresholdsStyle": { 270 | "mode": "off" 271 | } 272 | }, 273 | "mappings": [], 274 | "thresholds": { 275 | "mode": "absolute", 276 | "steps": [ 277 | { 278 | "color": "green", 279 | "value": null 280 | }, 281 | { 282 | "color": "red", 283 | "value": 80 284 | } 285 | ] 286 | } 287 | }, 288 | "overrides": [] 289 | }, 290 | "gridPos": { 291 | "h": 12, 292 | "w": 8, 293 | "x": 14, 294 | "y": 0 295 | }, 296 | "id": 7, 297 | "options": { 298 | "barRadius": 0, 299 | "barWidth": 0.97, 300 | "fullHighlight": false, 301 | "groupWidth": 0.7, 302 | "legend": { 303 | "calcs": [], 304 | "displayMode": "list", 305 | "placement": "bottom", 306 | "showLegend": true 307 | }, 308 | "orientation": "auto", 309 | "showValue": "auto", 310 | "stacking": "none", 311 | "tooltip": { 312 | "mode": "single", 313 | "sort": "none" 314 | }, 315 | "xTickLabelRotation": 0, 316 | "xTickLabelSpacing": 200 317 | }, 318 | "pluginVersion": "10.2.0-60477", 319 | "targets": [ 320 | { 321 | "datasource": { 322 | "type": "postgres", 323 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 324 | }, 325 | "editorMode": "code", 326 | "format": "time_series", 327 | "rawQuery": true, 328 | "rawSql": "SELECT\n COALESCE(SUM((data ->> 'qty') :: float), 0) AS step,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'step_count'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 329 | "refId": "A", 330 | "sql": { 331 | "columns": [ 332 | { 333 | "parameters": [], 334 | "type": "function" 335 | } 336 | ], 337 | "groupBy": [ 338 | { 339 | "property": { 340 | "type": "string" 341 | }, 342 | "type": "groupBy" 343 | } 344 | ], 345 | "limit": 50 346 | } 347 | } 348 | ], 349 | "title": "步数", 350 | "type": "barchart" 351 | }, 352 | { 353 | "datasource": {}, 354 | "fieldConfig": { 355 | "defaults": { 356 | "color": { 357 | "mode": "palette-classic" 358 | }, 359 | "custom": { 360 | "axisCenteredZero": false, 361 | "axisColorMode": "text", 362 | "axisLabel": "", 363 | "axisPlacement": "auto", 364 | "fillOpacity": 70, 365 | "gradientMode": "none", 366 | "hideFrom": { 367 | "legend": false, 368 | "tooltip": false, 369 | "viz": false 370 | }, 371 | "lineWidth": 1, 372 | "scaleDistribution": { 373 | "type": "linear" 374 | }, 375 | "thresholdsStyle": { 376 | "mode": "off" 377 | } 378 | }, 379 | "mappings": [], 380 | "thresholds": { 381 | "mode": "absolute", 382 | "steps": [ 383 | { 384 | "color": "green", 385 | "value": null 386 | }, 387 | { 388 | "color": "red", 389 | "value": 80 390 | } 391 | ] 392 | } 393 | }, 394 | "overrides": [] 395 | }, 396 | "gridPos": { 397 | "h": 12, 398 | "w": 7, 399 | "x": 0, 400 | "y": 12 401 | }, 402 | "id": 10, 403 | "options": { 404 | "barRadius": 0, 405 | "barWidth": 0.97, 406 | "fullHighlight": false, 407 | "groupWidth": 0.7, 408 | "legend": { 409 | "calcs": [], 410 | "displayMode": "list", 411 | "placement": "bottom", 412 | "showLegend": true 413 | }, 414 | "orientation": "auto", 415 | "showValue": "auto", 416 | "stacking": "normal", 417 | "tooltip": { 418 | "mode": "single", 419 | "sort": "none" 420 | }, 421 | "xField": "Time", 422 | "xTickLabelRotation": 0, 423 | "xTickLabelSpacing": 200 424 | }, 425 | "pluginVersion": "10.2.0-60477", 426 | "targets": [ 427 | { 428 | "datasource": { 429 | "type": "postgres", 430 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 431 | }, 432 | "editorMode": "code", 433 | "format": "time_series", 434 | "rawQuery": true, 435 | "rawSql": "SELECT\n\tCOALESCE(SUM((data ->> 'deep')::float), 0) AS deep,\n\tCOALESCE(SUM((data ->> 'core')::float), 0) AS core,\n\tCOALESCE(SUM((data ->> 'awake')::float), 0) AS awake,\n\tCOALESCE(SUM((data ->> 'rem')::float), 0) AS rem,\n\tgenerate_series(date_trunc('day', min(timestamp)), date_trunc('day', max(timestamp)), '1 day'::interval) AS time\nFROM\n\tmetrics\nWHERE\n\tname = 'sleep_analysis'\n\tAND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n\tdate_trunc('day', timestamp)\nORDER BY\n\ttime;", 436 | "refId": "Deep", 437 | "sql": { 438 | "columns": [ 439 | { 440 | "parameters": [], 441 | "type": "function" 442 | } 443 | ], 444 | "groupBy": [ 445 | { 446 | "property": { 447 | "type": "string" 448 | }, 449 | "type": "groupBy" 450 | } 451 | ], 452 | "limit": 50 453 | } 454 | } 455 | ], 456 | "title": "睡眠分析", 457 | "type": "barchart" 458 | }, 459 | { 460 | "datasource": {}, 461 | "fieldConfig": { 462 | "defaults": { 463 | "color": { 464 | "mode": "thresholds" 465 | }, 466 | "custom": { 467 | "axisCenteredZero": false, 468 | "axisColorMode": "text", 469 | "axisLabel": "", 470 | "axisPlacement": "left", 471 | "fillOpacity": 80, 472 | "gradientMode": "none", 473 | "hideFrom": { 474 | "legend": false, 475 | "tooltip": false, 476 | "viz": false 477 | }, 478 | "lineWidth": 1, 479 | "scaleDistribution": { 480 | "type": "linear" 481 | }, 482 | "thresholdsStyle": { 483 | "mode": "off" 484 | } 485 | }, 486 | "mappings": [], 487 | "thresholds": { 488 | "mode": "absolute", 489 | "steps": [ 490 | { 491 | "color": "green", 492 | "value": null 493 | }, 494 | { 495 | "color": "green", 496 | "value": 80 497 | } 498 | ] 499 | } 500 | }, 501 | "overrides": [] 502 | }, 503 | "gridPos": { 504 | "h": 12, 505 | "w": 7, 506 | "x": 7, 507 | "y": 12 508 | }, 509 | "id": 26, 510 | "options": { 511 | "barRadius": 0, 512 | "barWidth": 0.8, 513 | "fullHighlight": false, 514 | "groupWidth": 0.7, 515 | "legend": { 516 | "calcs": [], 517 | "displayMode": "list", 518 | "placement": "right", 519 | "showLegend": true 520 | }, 521 | "orientation": "auto", 522 | "showValue": "auto", 523 | "stacking": "none", 524 | "tooltip": { 525 | "mode": "single", 526 | "sort": "none" 527 | }, 528 | "xField": "Time", 529 | "xTickLabelRotation": 0, 530 | "xTickLabelSpacing": -200 531 | }, 532 | "pluginVersion": "10.2.0-60477", 533 | "targets": [ 534 | { 535 | "datasource": { 536 | "type": "postgres", 537 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 538 | }, 539 | "editorMode": "code", 540 | "format": "time_series", 541 | "rawQuery": true, 542 | "rawSql": "SELECT\n COALESCE(SUM((data ->> 'qty') :: float), 0) AS calorie,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'active_energy'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 543 | "refId": "A", 544 | "sql": { 545 | "columns": [ 546 | { 547 | "parameters": [], 548 | "type": "function" 549 | } 550 | ], 551 | "groupBy": [ 552 | { 553 | "property": { 554 | "type": "string" 555 | }, 556 | "type": "groupBy" 557 | } 558 | ], 559 | "limit": 50 560 | } 561 | } 562 | ], 563 | "title": "消耗能量", 564 | "type": "barchart" 565 | }, 566 | { 567 | "datasource": {}, 568 | "fieldConfig": { 569 | "defaults": { 570 | "color": { 571 | "mode": "thresholds" 572 | }, 573 | "custom": { 574 | "axisCenteredZero": false, 575 | "axisColorMode": "text", 576 | "axisLabel": "", 577 | "axisPlacement": "auto", 578 | "fillOpacity": 80, 579 | "gradientMode": "none", 580 | "hideFrom": { 581 | "legend": false, 582 | "tooltip": false, 583 | "viz": false 584 | }, 585 | "lineWidth": 1, 586 | "scaleDistribution": { 587 | "type": "linear" 588 | }, 589 | "thresholdsStyle": { 590 | "mode": "off" 591 | } 592 | }, 593 | "mappings": [], 594 | "thresholds": { 595 | "mode": "absolute", 596 | "steps": [ 597 | { 598 | "color": "green", 599 | "value": null 600 | }, 601 | { 602 | "color": "red", 603 | "value": 80 604 | } 605 | ] 606 | } 607 | }, 608 | "overrides": [] 609 | }, 610 | "gridPos": { 611 | "h": 12, 612 | "w": 8, 613 | "x": 14, 614 | "y": 12 615 | }, 616 | "id": 25, 617 | "options": { 618 | "barRadius": 0, 619 | "barWidth": 0.97, 620 | "fullHighlight": false, 621 | "groupWidth": 0.7, 622 | "legend": { 623 | "calcs": [], 624 | "displayMode": "list", 625 | "placement": "bottom", 626 | "showLegend": true 627 | }, 628 | "orientation": "auto", 629 | "showValue": "auto", 630 | "stacking": "none", 631 | "tooltip": { 632 | "mode": "single", 633 | "sort": "none" 634 | }, 635 | "xTickLabelRotation": 0, 636 | "xTickLabelSpacing": 0 637 | }, 638 | "pluginVersion": "10.2.0-60477", 639 | "targets": [ 640 | { 641 | "datasource": { 642 | "type": "postgres", 643 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 644 | }, 645 | "editorMode": "code", 646 | "format": "table", 647 | "rawQuery": true, 648 | "rawSql": "SELECT\n COALESCE(SUM((data ->> 'qty') :: float), 0) AS minute,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'apple_exercise_time'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 649 | "refId": "A", 650 | "sql": { 651 | "columns": [ 652 | { 653 | "parameters": [ 654 | { 655 | "name": "data", 656 | "type": "functionParameter" 657 | } 658 | ], 659 | "type": "function" 660 | } 661 | ], 662 | "groupBy": [], 663 | "limit": 50, 664 | "whereJsonTree": { 665 | "children1": [ 666 | { 667 | "id": "889a8b98-89ab-4cde-b012-318a9a3b5a96", 668 | "properties": { 669 | "field": "name", 670 | "operator": "equal", 671 | "value": [ 672 | "apple_exercise_time" 673 | ], 674 | "valueSrc": [ 675 | "value" 676 | ], 677 | "valueType": [ 678 | "text" 679 | ] 680 | }, 681 | "type": "rule" 682 | } 683 | ], 684 | "id": "a9a8b8b8-89ab-4cde-b012-318a9a2b2ccf", 685 | "type": "group" 686 | }, 687 | "whereString": "name = 'apple_exercise_time'" 688 | }, 689 | "table": "metrics" 690 | } 691 | ], 692 | "title": "锻炼时间", 693 | "type": "barchart" 694 | }, 695 | { 696 | "datasource": {}, 697 | "fieldConfig": { 698 | "defaults": { 699 | "color": { 700 | "mode": "palette-classic" 701 | }, 702 | "custom": { 703 | "axisCenteredZero": false, 704 | "axisColorMode": "text", 705 | "axisLabel": "", 706 | "axisPlacement": "auto", 707 | "axisShow": false, 708 | "fillOpacity": 80, 709 | "gradientMode": "none", 710 | "hideFrom": { 711 | "legend": false, 712 | "tooltip": false, 713 | "viz": false 714 | }, 715 | "lineWidth": 1, 716 | "scaleDistribution": { 717 | "type": "linear" 718 | }, 719 | "thresholdsStyle": { 720 | "mode": "off" 721 | } 722 | }, 723 | "mappings": [], 724 | "thresholds": { 725 | "mode": "absolute", 726 | "steps": [ 727 | { 728 | "color": "green" 729 | }, 730 | { 731 | "color": "red", 732 | "value": 80 733 | } 734 | ] 735 | } 736 | }, 737 | "overrides": [] 738 | }, 739 | "gridPos": { 740 | "h": 12, 741 | "w": 7, 742 | "x": 0, 743 | "y": 24 744 | }, 745 | "id": 16, 746 | "options": { 747 | "barRadius": 0, 748 | "barWidth": 0.97, 749 | "fullHighlight": false, 750 | "groupWidth": 0.7, 751 | "legend": { 752 | "calcs": [], 753 | "displayMode": "list", 754 | "placement": "bottom", 755 | "showLegend": true 756 | }, 757 | "orientation": "auto", 758 | "showValue": "auto", 759 | "stacking": "none", 760 | "tooltip": { 761 | "mode": "single", 762 | "sort": "none" 763 | }, 764 | "xTickLabelRotation": 0, 765 | "xTickLabelSpacing": 0 766 | }, 767 | "pluginVersion": "10.2.0-60477", 768 | "targets": [ 769 | { 770 | "datasource": { 771 | "type": "postgres", 772 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 773 | }, 774 | "editorMode": "code", 775 | "format": "time_series", 776 | "rawQuery": true, 777 | "rawSql": "SELECT\n COALESCE(SUM((data ->> 'qty') :: float), 0) AS floor,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'flights_climbed'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 778 | "refId": "A", 779 | "sql": { 780 | "columns": [ 781 | { 782 | "parameters": [], 783 | "type": "function" 784 | } 785 | ], 786 | "groupBy": [ 787 | { 788 | "property": { 789 | "type": "string" 790 | }, 791 | "type": "groupBy" 792 | } 793 | ], 794 | "limit": 50 795 | } 796 | } 797 | ], 798 | "title": "攀登楼层数", 799 | "type": "barchart" 800 | }, 801 | { 802 | "datasource": {}, 803 | "fieldConfig": { 804 | "defaults": { 805 | "color": { 806 | "mode": "palette-classic" 807 | }, 808 | "custom": { 809 | "axisCenteredZero": false, 810 | "axisColorMode": "text", 811 | "axisLabel": "", 812 | "axisPlacement": "auto", 813 | "axisShow": false, 814 | "barAlignment": 0, 815 | "drawStyle": "line", 816 | "fillOpacity": 0, 817 | "gradientMode": "none", 818 | "hideFrom": { 819 | "legend": false, 820 | "tooltip": false, 821 | "viz": false 822 | }, 823 | "insertNulls": false, 824 | "lineInterpolation": "linear", 825 | "lineWidth": 1, 826 | "pointSize": 5, 827 | "scaleDistribution": { 828 | "type": "linear" 829 | }, 830 | "showPoints": "auto", 831 | "spanNulls": false, 832 | "stacking": { 833 | "group": "A", 834 | "mode": "none" 835 | }, 836 | "thresholdsStyle": { 837 | "mode": "off" 838 | } 839 | }, 840 | "mappings": [], 841 | "thresholds": { 842 | "mode": "absolute", 843 | "steps": [ 844 | { 845 | "color": "green" 846 | }, 847 | { 848 | "color": "red", 849 | "value": 80 850 | } 851 | ] 852 | } 853 | }, 854 | "overrides": [] 855 | }, 856 | "gridPos": { 857 | "h": 12, 858 | "w": 7, 859 | "x": 7, 860 | "y": 24 861 | }, 862 | "id": 21, 863 | "options": { 864 | "legend": { 865 | "calcs": [], 866 | "displayMode": "list", 867 | "placement": "bottom", 868 | "showLegend": true 869 | }, 870 | "tooltip": { 871 | "mode": "single", 872 | "sort": "none" 873 | } 874 | }, 875 | "pluginVersion": "10.2.0-60477", 876 | "targets": [ 877 | { 878 | "datasource": { 879 | "type": "postgres", 880 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 881 | }, 882 | "editorMode": "code", 883 | "format": "time_series", 884 | "rawQuery": true, 885 | "rawSql": "SELECT\n COALESCE(SUM((data ->> 'qty') :: float), 0) AS value,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'basal_energy_burned'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 886 | "refId": "A", 887 | "sql": { 888 | "columns": [ 889 | { 890 | "parameters": [], 891 | "type": "function" 892 | } 893 | ], 894 | "groupBy": [ 895 | { 896 | "property": { 897 | "type": "string" 898 | }, 899 | "type": "groupBy" 900 | } 901 | ], 902 | "limit": 50 903 | } 904 | } 905 | ], 906 | "title": "基础能量消耗", 907 | "type": "timeseries" 908 | }, 909 | { 910 | "datasource": {}, 911 | "fieldConfig": { 912 | "defaults": { 913 | "color": { 914 | "mode": "palette-classic" 915 | }, 916 | "custom": { 917 | "axisCenteredZero": false, 918 | "axisColorMode": "text", 919 | "axisLabel": "", 920 | "axisPlacement": "auto", 921 | "axisShow": false, 922 | "fillOpacity": 80, 923 | "gradientMode": "none", 924 | "hideFrom": { 925 | "legend": false, 926 | "tooltip": false, 927 | "viz": false 928 | }, 929 | "lineWidth": 1, 930 | "scaleDistribution": { 931 | "type": "linear" 932 | }, 933 | "thresholdsStyle": { 934 | "mode": "off" 935 | } 936 | }, 937 | "mappings": [], 938 | "thresholds": { 939 | "mode": "absolute", 940 | "steps": [ 941 | { 942 | "color": "green" 943 | }, 944 | { 945 | "color": "red", 946 | "value": 80 947 | } 948 | ] 949 | } 950 | }, 951 | "overrides": [] 952 | }, 953 | "gridPos": { 954 | "h": 12, 955 | "w": 8, 956 | "x": 14, 957 | "y": 24 958 | }, 959 | "id": 22, 960 | "options": { 961 | "barRadius": 0, 962 | "barWidth": 0.97, 963 | "fullHighlight": false, 964 | "groupWidth": 0.7, 965 | "legend": { 966 | "calcs": [], 967 | "displayMode": "list", 968 | "placement": "bottom", 969 | "showLegend": true 970 | }, 971 | "orientation": "auto", 972 | "showValue": "auto", 973 | "stacking": "none", 974 | "tooltip": { 975 | "mode": "single", 976 | "sort": "none" 977 | }, 978 | "xTickLabelRotation": 0, 979 | "xTickLabelSpacing": 0 980 | }, 981 | "pluginVersion": "10.2.0-60477", 982 | "targets": [ 983 | { 984 | "datasource": { 985 | "type": "postgres", 986 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 987 | }, 988 | "editorMode": "code", 989 | "format": "time_series", 990 | "rawQuery": true, 991 | "rawSql": "SELECT\n COALESCE(SUM((data ->> 'qty') :: float), 0) AS value,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'apple_stand_hour'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 992 | "refId": "A", 993 | "sql": { 994 | "columns": [ 995 | { 996 | "parameters": [], 997 | "type": "function" 998 | } 999 | ], 1000 | "groupBy": [ 1001 | { 1002 | "property": { 1003 | "type": "string" 1004 | }, 1005 | "type": "groupBy" 1006 | } 1007 | ], 1008 | "limit": 50 1009 | } 1010 | } 1011 | ], 1012 | "title": "站立时间", 1013 | "type": "barchart" 1014 | }, 1015 | { 1016 | "datasource": {}, 1017 | "fieldConfig": { 1018 | "defaults": { 1019 | "color": { 1020 | "mode": "palette-classic" 1021 | }, 1022 | "custom": { 1023 | "axisCenteredZero": false, 1024 | "axisColorMode": "text", 1025 | "axisLabel": "", 1026 | "axisPlacement": "auto", 1027 | "axisShow": false, 1028 | "barAlignment": 0, 1029 | "drawStyle": "line", 1030 | "fillOpacity": 0, 1031 | "gradientMode": "none", 1032 | "hideFrom": { 1033 | "legend": false, 1034 | "tooltip": false, 1035 | "viz": false 1036 | }, 1037 | "insertNulls": false, 1038 | "lineInterpolation": "linear", 1039 | "lineWidth": 1, 1040 | "pointSize": 5, 1041 | "scaleDistribution": { 1042 | "type": "linear" 1043 | }, 1044 | "showPoints": "auto", 1045 | "spanNulls": false, 1046 | "stacking": { 1047 | "group": "A", 1048 | "mode": "none" 1049 | }, 1050 | "thresholdsStyle": { 1051 | "mode": "off" 1052 | } 1053 | }, 1054 | "mappings": [], 1055 | "thresholds": { 1056 | "mode": "absolute", 1057 | "steps": [ 1058 | { 1059 | "color": "green" 1060 | }, 1061 | { 1062 | "color": "red", 1063 | "value": 80 1064 | } 1065 | ] 1066 | } 1067 | }, 1068 | "overrides": [] 1069 | }, 1070 | "gridPos": { 1071 | "h": 12, 1072 | "w": 7, 1073 | "x": 0, 1074 | "y": 36 1075 | }, 1076 | "id": 19, 1077 | "options": { 1078 | "legend": { 1079 | "calcs": [], 1080 | "displayMode": "list", 1081 | "placement": "bottom", 1082 | "showLegend": true 1083 | }, 1084 | "tooltip": { 1085 | "mode": "single", 1086 | "sort": "none" 1087 | } 1088 | }, 1089 | "pluginVersion": "10.2.0-60477", 1090 | "targets": [ 1091 | { 1092 | "datasource": { 1093 | "type": "postgres", 1094 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 1095 | }, 1096 | "editorMode": "code", 1097 | "format": "time_series", 1098 | "rawQuery": true, 1099 | "rawSql": "SELECT\n\t(data ->> 'qty')::float AS body_fat_percentage,\n\ttimestamp AS time\nFROM\n\tmetrics\nWHERE\n\tname = 'body_fat_percentage';", 1100 | "refId": "A", 1101 | "sql": { 1102 | "columns": [ 1103 | { 1104 | "parameters": [], 1105 | "type": "function" 1106 | } 1107 | ], 1108 | "groupBy": [ 1109 | { 1110 | "property": { 1111 | "type": "string" 1112 | }, 1113 | "type": "groupBy" 1114 | } 1115 | ], 1116 | "limit": 50 1117 | } 1118 | } 1119 | ], 1120 | "title": "体脂百分比", 1121 | "type": "timeseries" 1122 | }, 1123 | { 1124 | "datasource": {}, 1125 | "fieldConfig": { 1126 | "defaults": { 1127 | "color": { 1128 | "mode": "palette-classic" 1129 | }, 1130 | "custom": { 1131 | "axisCenteredZero": false, 1132 | "axisColorMode": "text", 1133 | "axisLabel": "", 1134 | "axisPlacement": "auto", 1135 | "axisShow": false, 1136 | "barAlignment": 0, 1137 | "drawStyle": "line", 1138 | "fillOpacity": 0, 1139 | "gradientMode": "none", 1140 | "hideFrom": { 1141 | "legend": false, 1142 | "tooltip": false, 1143 | "viz": false 1144 | }, 1145 | "insertNulls": false, 1146 | "lineInterpolation": "smooth", 1147 | "lineWidth": 1, 1148 | "pointSize": 5, 1149 | "scaleDistribution": { 1150 | "type": "linear" 1151 | }, 1152 | "showPoints": "auto", 1153 | "spanNulls": false, 1154 | "stacking": { 1155 | "group": "A", 1156 | "mode": "none" 1157 | }, 1158 | "thresholdsStyle": { 1159 | "mode": "off" 1160 | } 1161 | }, 1162 | "mappings": [], 1163 | "thresholds": { 1164 | "mode": "absolute", 1165 | "steps": [ 1166 | { 1167 | "color": "green" 1168 | }, 1169 | { 1170 | "color": "red", 1171 | "value": 80 1172 | } 1173 | ] 1174 | } 1175 | }, 1176 | "overrides": [] 1177 | }, 1178 | "gridPos": { 1179 | "h": 12, 1180 | "w": 7, 1181 | "x": 7, 1182 | "y": 36 1183 | }, 1184 | "id": 13, 1185 | "options": { 1186 | "legend": { 1187 | "calcs": [], 1188 | "displayMode": "list", 1189 | "placement": "bottom", 1190 | "showLegend": true 1191 | }, 1192 | "tooltip": { 1193 | "mode": "single", 1194 | "sort": "none" 1195 | } 1196 | }, 1197 | "pluginVersion": "10.2.0-60477", 1198 | "targets": [ 1199 | { 1200 | "datasource": { 1201 | "type": "postgres", 1202 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 1203 | }, 1204 | "editorMode": "code", 1205 | "format": "time_series", 1206 | "rawQuery": true, 1207 | "rawSql": "SELECT\n COALESCE(AVG((data ->> 'qty') :: float), 0) AS kg,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'lean_body_mass'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 1208 | "refId": "A", 1209 | "sql": { 1210 | "columns": [ 1211 | { 1212 | "parameters": [], 1213 | "type": "function" 1214 | } 1215 | ], 1216 | "groupBy": [ 1217 | { 1218 | "property": { 1219 | "type": "string" 1220 | }, 1221 | "type": "groupBy" 1222 | } 1223 | ], 1224 | "limit": 50 1225 | } 1226 | } 1227 | ], 1228 | "title": "去脂体重", 1229 | "type": "timeseries" 1230 | }, 1231 | { 1232 | "datasource": {}, 1233 | "fieldConfig": { 1234 | "defaults": { 1235 | "color": { 1236 | "mode": "palette-classic" 1237 | }, 1238 | "custom": { 1239 | "axisCenteredZero": false, 1240 | "axisColorMode": "text", 1241 | "axisLabel": "", 1242 | "axisPlacement": "auto", 1243 | "axisShow": false, 1244 | "barAlignment": 0, 1245 | "drawStyle": "line", 1246 | "fillOpacity": 0, 1247 | "gradientMode": "none", 1248 | "hideFrom": { 1249 | "legend": false, 1250 | "tooltip": false, 1251 | "viz": false 1252 | }, 1253 | "insertNulls": false, 1254 | "lineInterpolation": "linear", 1255 | "lineWidth": 1, 1256 | "pointSize": 5, 1257 | "scaleDistribution": { 1258 | "type": "linear" 1259 | }, 1260 | "showPoints": "auto", 1261 | "spanNulls": false, 1262 | "stacking": { 1263 | "group": "A", 1264 | "mode": "none" 1265 | }, 1266 | "thresholdsStyle": { 1267 | "mode": "off" 1268 | } 1269 | }, 1270 | "mappings": [], 1271 | "thresholds": { 1272 | "mode": "absolute", 1273 | "steps": [ 1274 | { 1275 | "color": "green" 1276 | }, 1277 | { 1278 | "color": "red", 1279 | "value": 80 1280 | } 1281 | ] 1282 | } 1283 | }, 1284 | "overrides": [] 1285 | }, 1286 | "gridPos": { 1287 | "h": 12, 1288 | "w": 8, 1289 | "x": 14, 1290 | "y": 36 1291 | }, 1292 | "id": 3, 1293 | "options": { 1294 | "legend": { 1295 | "calcs": [], 1296 | "displayMode": "list", 1297 | "placement": "bottom", 1298 | "showLegend": true 1299 | }, 1300 | "tooltip": { 1301 | "mode": "single", 1302 | "sort": "none" 1303 | } 1304 | }, 1305 | "pluginVersion": "10.2.0-60477", 1306 | "targets": [ 1307 | { 1308 | "datasource": { 1309 | "type": "postgres", 1310 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 1311 | }, 1312 | "editorMode": "code", 1313 | "format": "time_series", 1314 | "rawQuery": true, 1315 | "rawSql": "SELECT\n COALESCE(AVG((data ->> 'qty') :: float), 0) AS km,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'walking_speed'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 1316 | "refId": "A", 1317 | "sql": { 1318 | "columns": [ 1319 | { 1320 | "parameters": [], 1321 | "type": "function" 1322 | } 1323 | ], 1324 | "groupBy": [ 1325 | { 1326 | "property": { 1327 | "type": "string" 1328 | }, 1329 | "type": "groupBy" 1330 | } 1331 | ], 1332 | "limit": 50 1333 | } 1334 | } 1335 | ], 1336 | "title": "步行速度", 1337 | "type": "timeseries" 1338 | }, 1339 | { 1340 | "datasource": {}, 1341 | "fieldConfig": { 1342 | "defaults": { 1343 | "color": { 1344 | "mode": "palette-classic" 1345 | }, 1346 | "custom": { 1347 | "axisCenteredZero": false, 1348 | "axisColorMode": "text", 1349 | "axisLabel": "", 1350 | "axisPlacement": "auto", 1351 | "axisShow": false, 1352 | "barAlignment": 0, 1353 | "drawStyle": "line", 1354 | "fillOpacity": 0, 1355 | "gradientMode": "none", 1356 | "hideFrom": { 1357 | "legend": false, 1358 | "tooltip": false, 1359 | "viz": false 1360 | }, 1361 | "insertNulls": false, 1362 | "lineInterpolation": "linear", 1363 | "lineWidth": 1, 1364 | "pointSize": 5, 1365 | "scaleDistribution": { 1366 | "type": "linear" 1367 | }, 1368 | "showPoints": "auto", 1369 | "spanNulls": false, 1370 | "stacking": { 1371 | "group": "A", 1372 | "mode": "none" 1373 | }, 1374 | "thresholdsStyle": { 1375 | "mode": "off" 1376 | } 1377 | }, 1378 | "mappings": [], 1379 | "thresholds": { 1380 | "mode": "absolute", 1381 | "steps": [ 1382 | { 1383 | "color": "green" 1384 | }, 1385 | { 1386 | "color": "red", 1387 | "value": 80 1388 | } 1389 | ] 1390 | } 1391 | }, 1392 | "overrides": [] 1393 | }, 1394 | "gridPos": { 1395 | "h": 12, 1396 | "w": 7, 1397 | "x": 0, 1398 | "y": 48 1399 | }, 1400 | "id": 14, 1401 | "options": { 1402 | "legend": { 1403 | "calcs": [], 1404 | "displayMode": "list", 1405 | "placement": "bottom", 1406 | "showLegend": true 1407 | }, 1408 | "tooltip": { 1409 | "mode": "single", 1410 | "sort": "none" 1411 | } 1412 | }, 1413 | "pluginVersion": "10.2.0-60477", 1414 | "targets": [ 1415 | { 1416 | "datasource": { 1417 | "type": "postgres", 1418 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 1419 | }, 1420 | "editorMode": "code", 1421 | "format": "time_series", 1422 | "rawQuery": true, 1423 | "rawSql": "SELECT\n (data ->> 'qty') :: float AS HRV,\n timestamp AS time\nFROM\n metrics\nWHERE\n name = 'heart_rate_variability'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()", 1424 | "refId": "A", 1425 | "sql": { 1426 | "columns": [ 1427 | { 1428 | "parameters": [], 1429 | "type": "function" 1430 | } 1431 | ], 1432 | "groupBy": [ 1433 | { 1434 | "property": { 1435 | "type": "string" 1436 | }, 1437 | "type": "groupBy" 1438 | } 1439 | ], 1440 | "limit": 50 1441 | } 1442 | } 1443 | ], 1444 | "title": "HRV", 1445 | "type": "timeseries" 1446 | }, 1447 | { 1448 | "datasource": {}, 1449 | "fieldConfig": { 1450 | "defaults": { 1451 | "color": { 1452 | "mode": "palette-classic" 1453 | }, 1454 | "custom": { 1455 | "axisCenteredZero": false, 1456 | "axisColorMode": "text", 1457 | "axisLabel": "", 1458 | "axisPlacement": "auto", 1459 | "axisShow": false, 1460 | "barAlignment": 0, 1461 | "drawStyle": "line", 1462 | "fillOpacity": 0, 1463 | "gradientMode": "none", 1464 | "hideFrom": { 1465 | "legend": false, 1466 | "tooltip": false, 1467 | "viz": false 1468 | }, 1469 | "insertNulls": false, 1470 | "lineInterpolation": "linear", 1471 | "lineWidth": 1, 1472 | "pointSize": 5, 1473 | "scaleDistribution": { 1474 | "type": "linear" 1475 | }, 1476 | "showPoints": "auto", 1477 | "spanNulls": false, 1478 | "stacking": { 1479 | "group": "A", 1480 | "mode": "none" 1481 | }, 1482 | "thresholdsStyle": { 1483 | "mode": "off" 1484 | } 1485 | }, 1486 | "mappings": [], 1487 | "thresholds": { 1488 | "mode": "absolute", 1489 | "steps": [ 1490 | { 1491 | "color": "green" 1492 | }, 1493 | { 1494 | "color": "red", 1495 | "value": 80 1496 | } 1497 | ] 1498 | } 1499 | }, 1500 | "overrides": [] 1501 | }, 1502 | "gridPos": { 1503 | "h": 12, 1504 | "w": 7, 1505 | "x": 7, 1506 | "y": 48 1507 | }, 1508 | "id": 2, 1509 | "options": { 1510 | "legend": { 1511 | "calcs": [], 1512 | "displayMode": "list", 1513 | "placement": "bottom", 1514 | "showLegend": true 1515 | }, 1516 | "tooltip": { 1517 | "mode": "single", 1518 | "sort": "none" 1519 | } 1520 | }, 1521 | "pluginVersion": "10.2.0-60477", 1522 | "targets": [ 1523 | { 1524 | "datasource": { 1525 | "type": "postgres", 1526 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 1527 | }, 1528 | "editorMode": "code", 1529 | "format": "time_series", 1530 | "rawQuery": true, 1531 | "rawSql": "SELECT\n COALESCE(AVG((data ->> 'qty') :: float), 0) AS cm,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'walking_step_length'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 1532 | "refId": "A", 1533 | "sql": { 1534 | "columns": [ 1535 | { 1536 | "parameters": [], 1537 | "type": "function" 1538 | } 1539 | ], 1540 | "groupBy": [ 1541 | { 1542 | "property": { 1543 | "type": "string" 1544 | }, 1545 | "type": "groupBy" 1546 | } 1547 | ], 1548 | "limit": 50 1549 | } 1550 | } 1551 | ], 1552 | "title": "步行步幅", 1553 | "type": "timeseries" 1554 | }, 1555 | { 1556 | "datasource": {}, 1557 | "fieldConfig": { 1558 | "defaults": { 1559 | "color": { 1560 | "mode": "palette-classic" 1561 | }, 1562 | "custom": { 1563 | "axisCenteredZero": false, 1564 | "axisColorMode": "text", 1565 | "axisLabel": "", 1566 | "axisPlacement": "auto", 1567 | "axisShow": false, 1568 | "barAlignment": 0, 1569 | "drawStyle": "line", 1570 | "fillOpacity": 0, 1571 | "gradientMode": "none", 1572 | "hideFrom": { 1573 | "legend": false, 1574 | "tooltip": false, 1575 | "viz": false 1576 | }, 1577 | "insertNulls": false, 1578 | "lineInterpolation": "linear", 1579 | "lineWidth": 1, 1580 | "pointSize": 5, 1581 | "scaleDistribution": { 1582 | "type": "linear" 1583 | }, 1584 | "showPoints": "auto", 1585 | "spanNulls": false, 1586 | "stacking": { 1587 | "group": "A", 1588 | "mode": "none" 1589 | }, 1590 | "thresholdsStyle": { 1591 | "mode": "off" 1592 | } 1593 | }, 1594 | "mappings": [], 1595 | "thresholds": { 1596 | "mode": "absolute", 1597 | "steps": [ 1598 | { 1599 | "color": "green" 1600 | }, 1601 | { 1602 | "color": "red", 1603 | "value": 80 1604 | } 1605 | ] 1606 | } 1607 | }, 1608 | "overrides": [] 1609 | }, 1610 | "gridPos": { 1611 | "h": 12, 1612 | "w": 8, 1613 | "x": 14, 1614 | "y": 48 1615 | }, 1616 | "id": 9, 1617 | "options": { 1618 | "legend": { 1619 | "calcs": [], 1620 | "displayMode": "list", 1621 | "placement": "bottom", 1622 | "showLegend": true 1623 | }, 1624 | "tooltip": { 1625 | "mode": "single", 1626 | "sort": "none" 1627 | } 1628 | }, 1629 | "pluginVersion": "10.2.0-60477", 1630 | "targets": [ 1631 | { 1632 | "datasource": { 1633 | "type": "postgres", 1634 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 1635 | }, 1636 | "editorMode": "code", 1637 | "format": "time_series", 1638 | "rawQuery": true, 1639 | "rawSql": "SELECT\n COALESCE(AVG((data ->> 'qty') :: float), 0) AS cm,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'stair_speed_down'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 1640 | "refId": "A", 1641 | "sql": { 1642 | "columns": [ 1643 | { 1644 | "parameters": [], 1645 | "type": "function" 1646 | } 1647 | ], 1648 | "groupBy": [ 1649 | { 1650 | "property": { 1651 | "type": "string" 1652 | }, 1653 | "type": "groupBy" 1654 | } 1655 | ], 1656 | "limit": 50 1657 | } 1658 | } 1659 | ], 1660 | "title": "下楼速度", 1661 | "type": "timeseries" 1662 | }, 1663 | { 1664 | "datasource": {}, 1665 | "fieldConfig": { 1666 | "defaults": { 1667 | "color": { 1668 | "mode": "palette-classic" 1669 | }, 1670 | "custom": { 1671 | "axisCenteredZero": false, 1672 | "axisColorMode": "text", 1673 | "axisLabel": "", 1674 | "axisPlacement": "auto", 1675 | "axisShow": false, 1676 | "barAlignment": 0, 1677 | "drawStyle": "line", 1678 | "fillOpacity": 0, 1679 | "gradientMode": "none", 1680 | "hideFrom": { 1681 | "legend": false, 1682 | "tooltip": false, 1683 | "viz": false 1684 | }, 1685 | "insertNulls": false, 1686 | "lineInterpolation": "linear", 1687 | "lineWidth": 1, 1688 | "pointSize": 5, 1689 | "scaleDistribution": { 1690 | "type": "linear" 1691 | }, 1692 | "showPoints": "auto", 1693 | "spanNulls": false, 1694 | "stacking": { 1695 | "group": "A", 1696 | "mode": "none" 1697 | }, 1698 | "thresholdsStyle": { 1699 | "mode": "off" 1700 | } 1701 | }, 1702 | "mappings": [], 1703 | "thresholds": { 1704 | "mode": "absolute", 1705 | "steps": [ 1706 | { 1707 | "color": "green" 1708 | }, 1709 | { 1710 | "color": "red", 1711 | "value": 80 1712 | } 1713 | ] 1714 | } 1715 | }, 1716 | "overrides": [] 1717 | }, 1718 | "gridPos": { 1719 | "h": 12, 1720 | "w": 7, 1721 | "x": 0, 1722 | "y": 60 1723 | }, 1724 | "id": 20, 1725 | "options": { 1726 | "legend": { 1727 | "calcs": [], 1728 | "displayMode": "list", 1729 | "placement": "bottom", 1730 | "showLegend": true 1731 | }, 1732 | "tooltip": { 1733 | "mode": "single", 1734 | "sort": "none" 1735 | } 1736 | }, 1737 | "pluginVersion": "10.2.0-60477", 1738 | "targets": [ 1739 | { 1740 | "datasource": { 1741 | "type": "postgres", 1742 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 1743 | }, 1744 | "editorMode": "code", 1745 | "format": "time_series", 1746 | "rawQuery": true, 1747 | "rawSql": "SELECT\n (data ->> 'qty') :: float AS SPO,\n timestamp AS time\nFROM\n metrics\nWHERE\n name = 'blood_oxygen_saturation'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()", 1748 | "refId": "A", 1749 | "sql": { 1750 | "columns": [ 1751 | { 1752 | "parameters": [], 1753 | "type": "function" 1754 | } 1755 | ], 1756 | "groupBy": [ 1757 | { 1758 | "property": { 1759 | "type": "string" 1760 | }, 1761 | "type": "groupBy" 1762 | } 1763 | ], 1764 | "limit": 50 1765 | } 1766 | } 1767 | ], 1768 | "title": "SPO", 1769 | "type": "timeseries" 1770 | }, 1771 | { 1772 | "datasource": {}, 1773 | "fieldConfig": { 1774 | "defaults": { 1775 | "color": { 1776 | "mode": "palette-classic" 1777 | }, 1778 | "custom": { 1779 | "axisCenteredZero": false, 1780 | "axisColorMode": "text", 1781 | "axisLabel": "", 1782 | "axisPlacement": "auto", 1783 | "axisShow": false, 1784 | "barAlignment": 0, 1785 | "drawStyle": "line", 1786 | "fillOpacity": 0, 1787 | "gradientMode": "none", 1788 | "hideFrom": { 1789 | "legend": false, 1790 | "tooltip": false, 1791 | "viz": false 1792 | }, 1793 | "insertNulls": false, 1794 | "lineInterpolation": "linear", 1795 | "lineWidth": 1, 1796 | "pointSize": 5, 1797 | "scaleDistribution": { 1798 | "type": "linear" 1799 | }, 1800 | "showPoints": "auto", 1801 | "spanNulls": false, 1802 | "stacking": { 1803 | "group": "A", 1804 | "mode": "none" 1805 | }, 1806 | "thresholdsStyle": { 1807 | "mode": "off" 1808 | } 1809 | }, 1810 | "mappings": [], 1811 | "thresholds": { 1812 | "mode": "absolute", 1813 | "steps": [ 1814 | { 1815 | "color": "green" 1816 | }, 1817 | { 1818 | "color": "red", 1819 | "value": 80 1820 | } 1821 | ] 1822 | } 1823 | }, 1824 | "overrides": [] 1825 | }, 1826 | "gridPos": { 1827 | "h": 12, 1828 | "w": 7, 1829 | "x": 7, 1830 | "y": 60 1831 | }, 1832 | "id": 8, 1833 | "options": { 1834 | "legend": { 1835 | "calcs": [], 1836 | "displayMode": "list", 1837 | "placement": "bottom", 1838 | "showLegend": true 1839 | }, 1840 | "tooltip": { 1841 | "mode": "single", 1842 | "sort": "none" 1843 | } 1844 | }, 1845 | "pluginVersion": "10.2.0-60477", 1846 | "targets": [ 1847 | { 1848 | "datasource": { 1849 | "type": "postgres", 1850 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 1851 | }, 1852 | "editorMode": "code", 1853 | "format": "time_series", 1854 | "rawQuery": true, 1855 | "rawSql": "SELECT\n COALESCE(AVG((data ->> 'qty') :: float), 0) AS floor,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'stair_speed_up'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 1856 | "refId": "A", 1857 | "sql": { 1858 | "columns": [ 1859 | { 1860 | "parameters": [], 1861 | "type": "function" 1862 | } 1863 | ], 1864 | "groupBy": [ 1865 | { 1866 | "property": { 1867 | "type": "string" 1868 | }, 1869 | "type": "groupBy" 1870 | } 1871 | ], 1872 | "limit": 50 1873 | } 1874 | } 1875 | ], 1876 | "title": "上楼速度", 1877 | "type": "timeseries" 1878 | }, 1879 | { 1880 | "datasource": {}, 1881 | "fieldConfig": { 1882 | "defaults": { 1883 | "color": { 1884 | "mode": "palette-classic" 1885 | }, 1886 | "custom": { 1887 | "axisCenteredZero": false, 1888 | "axisColorMode": "text", 1889 | "axisLabel": "", 1890 | "axisPlacement": "auto", 1891 | "axisShow": false, 1892 | "barAlignment": 0, 1893 | "drawStyle": "line", 1894 | "fillOpacity": 0, 1895 | "gradientMode": "none", 1896 | "hideFrom": { 1897 | "legend": false, 1898 | "tooltip": false, 1899 | "viz": false 1900 | }, 1901 | "insertNulls": false, 1902 | "lineInterpolation": "smooth", 1903 | "lineWidth": 1, 1904 | "pointSize": 5, 1905 | "scaleDistribution": { 1906 | "type": "linear" 1907 | }, 1908 | "showPoints": "auto", 1909 | "spanNulls": false, 1910 | "stacking": { 1911 | "group": "A", 1912 | "mode": "none" 1913 | }, 1914 | "thresholdsStyle": { 1915 | "mode": "off" 1916 | } 1917 | }, 1918 | "mappings": [], 1919 | "thresholds": { 1920 | "mode": "absolute", 1921 | "steps": [ 1922 | { 1923 | "color": "green" 1924 | }, 1925 | { 1926 | "color": "red", 1927 | "value": 80 1928 | } 1929 | ] 1930 | } 1931 | }, 1932 | "overrides": [] 1933 | }, 1934 | "gridPos": { 1935 | "h": 12, 1936 | "w": 8, 1937 | "x": 14, 1938 | "y": 60 1939 | }, 1940 | "id": 12, 1941 | "options": { 1942 | "legend": { 1943 | "calcs": [], 1944 | "displayMode": "list", 1945 | "placement": "bottom", 1946 | "showLegend": true 1947 | }, 1948 | "tooltip": { 1949 | "mode": "single", 1950 | "sort": "none" 1951 | } 1952 | }, 1953 | "pluginVersion": "10.2.0-60477", 1954 | "targets": [ 1955 | { 1956 | "datasource": { 1957 | "type": "postgres", 1958 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 1959 | }, 1960 | "editorMode": "code", 1961 | "format": "time_series", 1962 | "rawQuery": true, 1963 | "rawSql": "SELECT\n COALESCE(AVG((data ->> 'qty') :: float), 0) AS QPM,\n generate_series(\n date_trunc('day', min(timestamp)),\n date_trunc('day', max(timestamp)),\n '1 day' :: interval\n ) AS time\nFROM\n metrics\nWHERE\n name = 'respiratory_rate'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()\nGROUP BY\n date_trunc('day', timestamp)\nORDER BY\n time;", 1964 | "refId": "A", 1965 | "sql": { 1966 | "columns": [ 1967 | { 1968 | "parameters": [], 1969 | "type": "function" 1970 | } 1971 | ], 1972 | "groupBy": [ 1973 | { 1974 | "property": { 1975 | "type": "string" 1976 | }, 1977 | "type": "groupBy" 1978 | } 1979 | ], 1980 | "limit": 50 1981 | } 1982 | } 1983 | ], 1984 | "title": "呼吸频率", 1985 | "type": "timeseries" 1986 | }, 1987 | { 1988 | "datasource": {}, 1989 | "fieldConfig": { 1990 | "defaults": { 1991 | "color": { 1992 | "mode": "palette-classic" 1993 | }, 1994 | "custom": { 1995 | "axisCenteredZero": false, 1996 | "axisColorMode": "text", 1997 | "axisLabel": "", 1998 | "axisPlacement": "auto", 1999 | "axisShow": false, 2000 | "barAlignment": 0, 2001 | "drawStyle": "line", 2002 | "fillOpacity": 0, 2003 | "gradientMode": "none", 2004 | "hideFrom": { 2005 | "legend": false, 2006 | "tooltip": false, 2007 | "viz": false 2008 | }, 2009 | "insertNulls": false, 2010 | "lineInterpolation": "smooth", 2011 | "lineWidth": 1, 2012 | "pointSize": 5, 2013 | "scaleDistribution": { 2014 | "log": 2, 2015 | "type": "log" 2016 | }, 2017 | "showPoints": "auto", 2018 | "spanNulls": false, 2019 | "stacking": { 2020 | "group": "A", 2021 | "mode": "none" 2022 | }, 2023 | "thresholdsStyle": { 2024 | "mode": "off" 2025 | } 2026 | }, 2027 | "mappings": [], 2028 | "thresholds": { 2029 | "mode": "absolute", 2030 | "steps": [ 2031 | { 2032 | "color": "green" 2033 | }, 2034 | { 2035 | "color": "red", 2036 | "value": 80 2037 | } 2038 | ] 2039 | } 2040 | }, 2041 | "overrides": [] 2042 | }, 2043 | "gridPos": { 2044 | "h": 12, 2045 | "w": 7, 2046 | "x": 0, 2047 | "y": 72 2048 | }, 2049 | "id": 17, 2050 | "options": { 2051 | "legend": { 2052 | "calcs": [], 2053 | "displayMode": "list", 2054 | "placement": "bottom", 2055 | "showLegend": true 2056 | }, 2057 | "tooltip": { 2058 | "mode": "single", 2059 | "sort": "none" 2060 | } 2061 | }, 2062 | "pluginVersion": "10.2.0-60477", 2063 | "targets": [ 2064 | { 2065 | "datasource": { 2066 | "type": "postgres", 2067 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 2068 | }, 2069 | "editorMode": "code", 2070 | "format": "time_series", 2071 | "rawQuery": true, 2072 | "rawSql": "SELECT\n\t(data ->> 'qty')::float AS db,\n\ttimestamp AS time\nFROM\n\tmetrics\nWHERE\n\tname = 'environmental_audio_exposure';", 2073 | "refId": "A", 2074 | "sql": { 2075 | "columns": [ 2076 | { 2077 | "parameters": [], 2078 | "type": "function" 2079 | } 2080 | ], 2081 | "groupBy": [ 2082 | { 2083 | "property": { 2084 | "type": "string" 2085 | }, 2086 | "type": "groupBy" 2087 | } 2088 | ], 2089 | "limit": 50 2090 | } 2091 | } 2092 | ], 2093 | "title": "环境音频暴露", 2094 | "type": "timeseries" 2095 | }, 2096 | { 2097 | "datasource": {}, 2098 | "fieldConfig": { 2099 | "defaults": { 2100 | "color": { 2101 | "mode": "palette-classic" 2102 | }, 2103 | "custom": { 2104 | "axisCenteredZero": false, 2105 | "axisColorMode": "text", 2106 | "axisLabel": "", 2107 | "axisPlacement": "auto", 2108 | "axisShow": false, 2109 | "barAlignment": 0, 2110 | "drawStyle": "line", 2111 | "fillOpacity": 0, 2112 | "gradientMode": "none", 2113 | "hideFrom": { 2114 | "legend": false, 2115 | "tooltip": false, 2116 | "viz": false 2117 | }, 2118 | "insertNulls": false, 2119 | "lineInterpolation": "linear", 2120 | "lineStyle": { 2121 | "fill": "solid" 2122 | }, 2123 | "lineWidth": 1, 2124 | "pointSize": 5, 2125 | "scaleDistribution": { 2126 | "log": 2, 2127 | "type": "log" 2128 | }, 2129 | "showPoints": "auto", 2130 | "spanNulls": false, 2131 | "stacking": { 2132 | "group": "A", 2133 | "mode": "none" 2134 | }, 2135 | "thresholdsStyle": { 2136 | "mode": "off" 2137 | } 2138 | }, 2139 | "mappings": [], 2140 | "thresholds": { 2141 | "mode": "absolute", 2142 | "steps": [ 2143 | { 2144 | "color": "green" 2145 | }, 2146 | { 2147 | "color": "red", 2148 | "value": 80 2149 | } 2150 | ] 2151 | } 2152 | }, 2153 | "overrides": [] 2154 | }, 2155 | "gridPos": { 2156 | "h": 12, 2157 | "w": 7, 2158 | "x": 7, 2159 | "y": 72 2160 | }, 2161 | "id": 11, 2162 | "options": { 2163 | "legend": { 2164 | "calcs": [], 2165 | "displayMode": "list", 2166 | "placement": "bottom", 2167 | "showLegend": true 2168 | }, 2169 | "timezone": [ 2170 | "browser" 2171 | ], 2172 | "tooltip": { 2173 | "mode": "single", 2174 | "sort": "none" 2175 | } 2176 | }, 2177 | "pluginVersion": "10.2.0-60477", 2178 | "targets": [ 2179 | { 2180 | "datasource": { 2181 | "type": "postgres", 2182 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 2183 | }, 2184 | "editorMode": "code", 2185 | "format": "time_series", 2186 | "rawQuery": true, 2187 | "rawSql": "SELECT\n (data ->> 'qty') :: float AS QPM,\n timestamp AS time\nFROM\n metrics\nWHERE\n name = 'resting_heart_rate'\n AND \"timestamp\" > $__timeFrom()\n AND \"timestamp\" < $__timeTo()", 2188 | "refId": "A", 2189 | "sql": { 2190 | "columns": [ 2191 | { 2192 | "parameters": [], 2193 | "type": "function" 2194 | } 2195 | ], 2196 | "groupBy": [ 2197 | { 2198 | "property": { 2199 | "type": "string" 2200 | }, 2201 | "type": "groupBy" 2202 | } 2203 | ], 2204 | "limit": 50 2205 | } 2206 | } 2207 | ], 2208 | "title": "休息心率", 2209 | "type": "timeseries" 2210 | }, 2211 | { 2212 | "datasource": {}, 2213 | "fieldConfig": { 2214 | "defaults": { 2215 | "color": { 2216 | "mode": "palette-classic" 2217 | }, 2218 | "custom": { 2219 | "axisCenteredZero": false, 2220 | "axisColorMode": "text", 2221 | "axisLabel": "", 2222 | "axisPlacement": "auto", 2223 | "axisShow": false, 2224 | "barAlignment": 0, 2225 | "drawStyle": "line", 2226 | "fillOpacity": 0, 2227 | "gradientMode": "none", 2228 | "hideFrom": { 2229 | "legend": false, 2230 | "tooltip": false, 2231 | "viz": false 2232 | }, 2233 | "insertNulls": false, 2234 | "lineInterpolation": "smooth", 2235 | "lineWidth": 1, 2236 | "pointSize": 5, 2237 | "scaleDistribution": { 2238 | "type": "linear" 2239 | }, 2240 | "showPoints": "auto", 2241 | "spanNulls": false, 2242 | "stacking": { 2243 | "group": "A", 2244 | "mode": "none" 2245 | }, 2246 | "thresholdsStyle": { 2247 | "mode": "off" 2248 | } 2249 | }, 2250 | "mappings": [], 2251 | "thresholds": { 2252 | "mode": "absolute", 2253 | "steps": [ 2254 | { 2255 | "color": "green" 2256 | }, 2257 | { 2258 | "color": "red", 2259 | "value": 80 2260 | } 2261 | ] 2262 | } 2263 | }, 2264 | "overrides": [] 2265 | }, 2266 | "gridPos": { 2267 | "h": 12, 2268 | "w": 8, 2269 | "x": 14, 2270 | "y": 72 2271 | }, 2272 | "id": 18, 2273 | "options": { 2274 | "legend": { 2275 | "calcs": [], 2276 | "displayMode": "list", 2277 | "placement": "bottom", 2278 | "showLegend": true 2279 | }, 2280 | "tooltip": { 2281 | "mode": "single", 2282 | "sort": "none" 2283 | } 2284 | }, 2285 | "pluginVersion": "10.2.0-60477", 2286 | "targets": [ 2287 | { 2288 | "datasource": { 2289 | "type": "postgres", 2290 | "uid": "ba8c639a-57c5-41cf-a0cf-1b2463323d66" 2291 | }, 2292 | "editorMode": "code", 2293 | "format": "time_series", 2294 | "rawQuery": true, 2295 | "rawSql": "SELECT\n\t(data ->> 'qty')::float AS BMI,\n\ttimestamp AS time\nFROM\n\tmetrics\nWHERE\n\tname = 'body_mass_index';", 2296 | "refId": "A", 2297 | "sql": { 2298 | "columns": [ 2299 | { 2300 | "parameters": [], 2301 | "type": "function" 2302 | } 2303 | ], 2304 | "groupBy": [ 2305 | { 2306 | "property": { 2307 | "type": "string" 2308 | }, 2309 | "type": "groupBy" 2310 | } 2311 | ], 2312 | "limit": 50 2313 | } 2314 | } 2315 | ], 2316 | "title": "BMI", 2317 | "type": "timeseries" 2318 | } 2319 | ], 2320 | "refresh": "", 2321 | "schemaVersion": 38, 2322 | "style": "dark", 2323 | "tags": [], 2324 | "templating": { 2325 | "list": [] 2326 | }, 2327 | "time": { 2328 | "from": "now-7d", 2329 | "to": "now" 2330 | }, 2331 | "timepicker": {}, 2332 | "timezone": "", 2333 | "title": "Holegots-Health", 2334 | "uid": "e8491a61-cdac-4398-bf19-68c1c300c98f", 2335 | "version": 2, 2336 | "weekStart": "" 2337 | } -------------------------------------------------------------------------------- /db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy_timescaledb.dialect import TimescaledbDDLCompiler 2 | from sqlalchemy import DDL 3 | @staticmethod 4 | def ddl_hypertable(table_name, hypertable): 5 | time_column_name = hypertable['time_column_name'] 6 | partitioning_column = hypertable.get("partitioning_column", None) 7 | number_partitions = hypertable.get("number_partitions") 8 | chunk_time_interval = hypertable.get('chunk_time_interval', '7 days') 9 | 10 | if isinstance(chunk_time_interval, str): 11 | if chunk_time_interval.isdigit(): 12 | chunk_time_interval = int(chunk_time_interval) 13 | else: 14 | chunk_time_interval = f"INTERVAL '{chunk_time_interval}'" 15 | 16 | return DDL( 17 | f"CREATE UNIQUE INDEX idx_{partitioning_column}_{time_column_name} ON {table_name}({partitioning_column}, {time_column_name});" if partitioning_column else "" + 18 | f""" 19 | SELECT create_hypertable( 20 | '{table_name}', 21 | '{time_column_name}', 22 | chunk_time_interval => {chunk_time_interval}, 23 | {f"partitioning_column => '{partitioning_column}'," if partitioning_column else ""} 24 | {f"number_partitions => {number_partitions}," if number_partitions else ""} 25 | if_not_exists => TRUE 26 | ); 27 | """ 28 | ) 29 | TimescaledbDDLCompiler.ddl_hypertable = ddl_hypertable -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | server: 4 | build: . 5 | ports: 6 | - "8000:8000" 7 | depends_on: 8 | - db 9 | env_file: 10 | - .env 11 | db: 12 | image: timescale/timescaledb-postgis:latest-pg13 13 | volumes: 14 | - ./data:/var/lib/postgresql/data 15 | environment: 16 | - POSTGRES_PASSWORD=postgres 17 | - POSTGRES_USER=postgres 18 | - POSTGRES_DB=postgres 19 | ports: 20 | - "5432:5432" 21 | grafana: 22 | image: grafana/grafana-enterprise:latest 23 | ports: 24 | - "3000:3000" 25 | depends_on: 26 | - server 27 | volumes: 28 | - ./grafana:/data 29 | env_file: 30 | - .env 31 | environment: 32 | - GF_DEFAULT_INSTANCE_NAME=health-export 33 | - GF_LOG_MODE=console 34 | - GF_PATHS_DATA=/data 35 | - PORT=3000 36 | -------------------------------------------------------------------------------- /docs/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuergaosi233/apple-health-exporter/fba2de10478f49347fdf1e99177b699731a17f37/docs/config.png -------------------------------------------------------------------------------- /docs/dashboard.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuergaosi233/apple-health-exporter/fba2de10478f49347fdf1e99177b699731a17f37/docs/dashboard.jpeg -------------------------------------------------------------------------------- /docs/import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuergaosi233/apple-health-exporter/fba2de10478f49347fdf1e99177b699731a17f37/docs/import.png -------------------------------------------------------------------------------- /docs/postgresql.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuergaosi233/apple-health-exporter/fba2de10478f49347fdf1e99177b699731a17f37/docs/postgresql.jpeg -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "apple-health" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Holegots "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.9" 10 | fastapi = {extras = ["all"], version = "^0.103.1"} 11 | influxdb = "^5.3.1" 12 | influxdb-client = {extras = ["async"], version = "^1.37.0"} 13 | influxdb3-python = "^0.2.1" 14 | sqlalchemy-timescaledb = "^0.4.1" 15 | sqlalchemy = "2.0.20" 16 | psycopg2-binary = "^2.9.7" 17 | python-dotenv = "^1.0.0" 18 | 19 | 20 | [build-system] 21 | requires = ["poetry-core"] 22 | build-backend = "poetry.core.masonry.api" 23 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import dotenv 4 | 5 | dotenv.load_dotenv() 6 | import uuid 7 | from typing import List, Optional 8 | 9 | from fastapi import FastAPI 10 | from pydantic import BaseModel 11 | from sqlalchemy import JSON, UUID, Column, DateTime, String, create_engine 12 | from sqlalchemy.dialects.postgresql import insert 13 | from sqlalchemy.orm import declarative_base, sessionmaker 14 | 15 | import db # type: ignore for timescale hook 16 | 17 | app = FastAPI() 18 | DATABASE_URL = os.environ.get( 19 | "DATABASE_URL", "timescaledb://postgres:postgres@localhost:5432/postgres" 20 | ) 21 | DATABASE_URL = DATABASE_URL.replace("postgresql://", "timescaledb://") 22 | 23 | engine = create_engine(DATABASE_URL) 24 | Base = declarative_base() 25 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 26 | 27 | 28 | class MetricTable(Base): 29 | __tablename__ = "metrics" 30 | id = Column(UUID, default=uuid.uuid4, primary_key=True) 31 | name = Column(String) 32 | data = Column(JSON) 33 | timestamp = Column(DateTime()) 34 | # Add index 35 | __table_args__ = { 36 | "timescaledb_hypertable": { 37 | "time_column_name": "timestamp", 38 | "partitioning_column": "name", 39 | "number_partitions": 10, 40 | } 41 | } 42 | 43 | 44 | # AUto migrate 45 | Base.metadata.create_all(engine) 46 | 47 | 48 | class Datum(BaseModel): 49 | date: str 50 | source: Optional[str] = None 51 | qty: Optional[float] = None 52 | avg: Optional[float] = None 53 | min: Optional[float] = None 54 | max: Optional[float] = None 55 | deep: Optional[float] = None 56 | core: Optional[float] = None 57 | awake: Optional[float] = None 58 | asleep: Optional[float] = None 59 | sleep_end: Optional[str] = None 60 | in_bed_start: Optional[str] = None 61 | in_bed_end: Optional[str] = None 62 | sleep_start: Optional[str] = None 63 | rem: Optional[float] = None 64 | in_bed: Optional[float] = None 65 | 66 | 67 | class Metric(BaseModel): 68 | units: str 69 | data: List[Datum] 70 | name: str 71 | 72 | 73 | class Data(BaseModel): 74 | metrics: List[Metric] 75 | 76 | 77 | class RequestData(BaseModel): 78 | data: Data 79 | 80 | 81 | @app.post("/upload") 82 | def upload_data(request_data: RequestData): 83 | ps = [] 84 | for metric in request_data.data.metrics: 85 | for datum in metric.data: 86 | data = datum.model_dump() 87 | date = data.pop("date", None) 88 | ps.append(dict(name=metric.name, data=data, timestamp=date)) 89 | with SessionLocal() as session: 90 | insert_ps = ( 91 | insert(MetricTable) 92 | .values(ps) 93 | .on_conflict_do_nothing(index_elements=["name", "timestamp"]) 94 | ) 95 | session.execute(insert_ps) 96 | session.commit() 97 | return {"status": "Data uploaded successfully!"} 98 | 99 | 100 | if __name__ == "__main__": 101 | import uvicorn 102 | 103 | uvicorn.run(app, host="0.0.0.0", port=8000) 104 | --------------------------------------------------------------------------------