├── .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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | > Explore your apple health with Grafana
13 |
14 | 
15 | ### 🏠 [Homepage](https://github.com/fuergaosi233/apple-health-exporter) | ✨ [Demo](https://grafana-health.y1s1.host/goto/egkRFfmIR?orgId=1)
16 |
17 | [](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 | 
32 | 1. Import `dashboard.json` to your dashboard
33 | 
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 | 
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 |
--------------------------------------------------------------------------------