diff --git a/README.md b/README.md index dc5109b..fdaaba9 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,55 @@ docker compose -f docker-compose.yml up -d docker compose logs -f app ``` +## Grafana Provisioning + +当前仓库支持通过 Grafana provisioning 自动加载 SQLite datasource 和 repo 内的 dashboard 导出文件。 + +需要保留的文件路径如下: + +- `grafana/provisioning/datasources/locationrecorder.yaml` +- `grafana/provisioning/datasources/poorecorder.yaml` +- `grafana/provisioning/dashboards/provider.yaml` +- `grafana/dashboards/locationrecorder.json` +- `grafana/dashboards/poorecorder.json` + +这些文件的职责分别是: + +- `grafana/provisioning/datasources/locationrecorder.yaml`:声明 `locationrecorder` SQLite datasource,并指向 `/data/home-automation/locationRecorder.db` +- `grafana/provisioning/datasources/poorecorder.yaml`:声明 `poorecorder` SQLite datasource,并指向 `/data/home-automation/pooRecorder.db` +- `grafana/provisioning/dashboards/provider.yaml`:告诉 Grafana 从 `/var/lib/grafana/dashboards` 扫描并加载 dashboard JSON +- `grafana/dashboards/locationrecorder.json`:location recorder dashboard 导出文件,内容本身不需要在 compose 中改写 +- `grafana/dashboards/poorecorder.json`:poo recorder dashboard 导出文件,内容本身不需要在 compose 中改写 + +当前 `docker-compose.yml` 中,Grafana service 需要挂载以下目录: + +- `./grafana/provisioning -> /etc/grafana/provisioning:ro` +- `./grafana/dashboards -> /var/lib/grafana/dashboards:ro` + +同时保留现有 named volume `homeautomation_grafana_storage:/var/lib/grafana` 作为 Grafana 运行态数据存储。 + +一键启动前,至少需要以下文件已经存在: + +- `grafana/provisioning/datasources/locationrecorder.yaml` +- `grafana/provisioning/datasources/poorecorder.yaml` +- `grafana/provisioning/dashboards/provider.yaml` +- `grafana/dashboards/locationrecorder.json` +- `grafana/dashboards/poorecorder.json` + +启动方式: + +```bash +docker compose up -d +``` + +启动后会发生的事情: + +- Grafana 容器会安装 `frser-sqlite-datasource` 插件 +- Grafana 会读取 `/etc/grafana/provisioning/datasources/` 下的 datasource YAML +- Grafana 会读取 `/etc/grafana/provisioning/dashboards/provider.yaml` +- Grafana 会从 `/var/lib/grafana/dashboards/` 自动导入两个 dashboard JSON +- 现有 Grafana named volume 继续负责保存 Grafana 运行态数据,不会覆盖 repo 内的 dashboard 与 provisioning 文件 + ## Container Image CI 项目提供了一个 release image workflow: diff --git a/docker-compose.yml b/docker-compose.yml index bc79bcd..b49aa84 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,10 @@ services: GF_PLUGINS_PREINSTALL: frser-sqlite-datasource volumes: - ./data:/data/home-automation:ro + - ./grafana/provisioning:/etc/grafana/provisioning:ro + - ./grafana/dashboards:/var/lib/grafana/dashboards:ro - homeautomation_grafana_storage:/var/lib/grafana volumes: homeautomation_grafana_storage: + name: homeautomation_grafana_storage diff --git a/grafana/dashboards/locationrecorder.json b/grafana/dashboards/locationrecorder.json new file mode 100644 index 0000000..5d417bd --- /dev/null +++ b/grafana/dashboards/locationrecorder.json @@ -0,0 +1,288 @@ +{ + "apiVersion": "dashboard.grafana.app/v2", + "kind": "Dashboard", + "metadata": { + "name": "adzr6rv", + "namespace": "default", + "uid": "c5fc57e5-7fb5-4104-9861-023710ada568", + "resourceVersion": "1776634346371016", + "generation": 19, + "creationTimestamp": "2026-04-18T19:05:57Z", + "labels": { + "grafana.app/deprecatedInternalID": "945374452785152" + }, + "annotations": { + "grafana.app/createdBy": "user:ffjhknvgkvhtsc", + "grafana.app/folder": "", + "grafana.app/saved-from-ui": "Grafana v13.0.1 (a100054f)", + "grafana.app/updatedBy": "user:ffjhknvgkvhtsc", + "grafana.app/updatedTimestamp": "2026-04-19T21:32:26Z" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "builtIn": true + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "轨迹", + "description": "", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "frser-sqlite-datasource", + "version": "v0", + "datasource": { + "name": "ffjhr941d5iwwf" + }, + "spec": { + "queryText": "SELECT\n datetime AS time,\n latitude,\n longitude,\n altitude\nFROM location\nWHERE person = 'Jiangxue'\n AND datetime >= '2021-04-19T21:29:57.036Z'\n AND datetime <= '2026-04-19T21:29:57.036Z'\n AND latitude != 0\n AND longitude != 0\nORDER BY datetime;\n", + "queryType": "table", + "rawQueryText": "SELECT\n datetime AS time,\n latitude,\n longitude,\n altitude\nFROM location\nWHERE person = '$person'\n AND datetime >= '${__from:date:iso}'\n AND datetime <= '${__to:date:iso}'\n AND latitude != 0\n AND longitude != 0\nORDER BY datetime;\n", + "timeColumns": [ + "time", + "ts" + ] + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "geomap", + "version": "13.0.1", + "spec": { + "options": { + "basemap": { + "config": { + "server": "streets" + }, + "name": "Layer 0", + "noRepeat": false, + "type": "default" + }, + "controls": { + "mouseWheelZoom": true, + "showAttribution": true, + "showDebug": false, + "showMeasure": false, + "showScale": false, + "showZoom": true + }, + "layers": [ + { + "config": { + "showLegend": false, + "style": { + "color": { + "fixed": "blue" + }, + "opacity": 0.7, + "rotation": { + "fixed": 0, + "max": 360, + "min": -360, + "mode": "mod" + }, + "size": { + "fixed": 3, + "max": 15, + "min": 2 + }, + "symbol": { + "fixed": "img/icons/marker/circle.svg", + "mode": "fixed" + }, + "symbolAlign": { + "horizontal": "center", + "vertical": "center" + }, + "textConfig": { + "fontSize": 12, + "offsetX": 0, + "offsetY": 0, + "textAlign": "center", + "textBaseline": "middle" + } + } + }, + "layer-tooltip": true, + "name": "path", + "tooltip": true, + "type": "markers" + } + ], + "tooltip": { + "mode": "details" + }, + "view": { + "allLayers": true, + "dashboardVariable": false, + "id": "fit", + "lat": 0, + "lon": 0, + "noRepeat": false, + "shared": false, + "zoom": 15 + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + } + ] + }, + "color": { + "mode": "thresholds" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 18, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-5y", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "轨迹", + "variables": [ + { + "kind": "QueryVariable", + "spec": { + "name": "person", + "current": { + "text": "Jiangxue", + "value": "Jiangxue" + }, + "label": "person", + "hide": "dontHide", + "refresh": "onDashboardLoad", + "skipUrlSync": false, + "description": "", + "query": { + "kind": "DataQuery", + "group": "frser-sqlite-datasource", + "version": "v0", + "datasource": { + "name": "ffjhr941d5iwwf" + }, + "spec": { + "__legacyStringValue": "SELECT DISTINCT person\nFROM location\nORDER BY person;\n" + } + }, + "regex": "", + "regexApplyTo": "value", + "sort": "disabled", + "definition": "SELECT DISTINCT person\nFROM location\nORDER BY person;\n", + "options": [], + "multi": false, + "includeAll": false, + "allowCustomValue": true + } + } + ], + "preferences": { + "layout": { + "kind": "AutoGridLayout", + "spec": { + "maxColumnCount": 3, + "columnWidthMode": "standard", + "rowHeightMode": "standard", + "items": [] + } + } + } + } +} \ No newline at end of file diff --git a/grafana/dashboards/poorecorder.json b/grafana/dashboards/poorecorder.json new file mode 100644 index 0000000..006d079 --- /dev/null +++ b/grafana/dashboards/poorecorder.json @@ -0,0 +1,231 @@ +{ + "apiVersion": "dashboard.grafana.app/v2", + "kind": "Dashboard", + "metadata": { + "name": "adl5sjt", + "namespace": "default", + "uid": "d4c72406-9fc5-4b85-844b-be1250f1fa8b", + "resourceVersion": "1776606363367013", + "generation": 6, + "creationTimestamp": "2026-04-18T20:07:34Z", + "labels": { + "grafana.app/deprecatedInternalID": "960882027798528" + }, + "annotations": { + "grafana.app/createdBy": "user:ffjhknvgkvhtsc", + "grafana.app/folder": "", + "grafana.app/saved-from-ui": "Grafana v13.0.1 (a100054f)", + "grafana.app/updatedBy": "user:ffjhknvgkvhtsc", + "grafana.app/updatedTimestamp": "2026-04-19T13:46:03Z" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "grafana", + "version": "v0", + "datasource": { + "name": "-- Grafana --" + }, + "spec": {} + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "builtIn": true + } + } + ], + "cursorSync": "Off", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "id": 1, + "title": "Mika Poo", + "description": "Mika's poo", + "links": [], + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "query": { + "kind": "DataQuery", + "group": "frser-sqlite-datasource", + "version": "v0", + "datasource": { + "name": "ffjhkuu4hc3y8e" + }, + "spec": { + "queryText": "SELECT\n latitude,\n longitude,\n timestamp\nFROM poo_records\nWHERE timestamp >= '${__from:date:iso}'\n AND timestamp <= '${__to:date:iso}'\n AND latitude != 0\n AND longitude != 0\nORDER BY timestamp;\n", + "queryType": "table", + "rawQueryText": "SELECT\n latitude,\n longitude,\n timestamp\nFROM poo_records\nWHERE timestamp >= '${__from:date:iso}'\n AND timestamp <= '${__to:date:iso}'\n AND latitude != 0\n AND longitude != 0\nORDER BY timestamp;\n", + "timeColumns": [ + "time", + "ts" + ] + } + }, + "refId": "A", + "hidden": false + } + } + ], + "transformations": [], + "queryOptions": {} + } + }, + "vizConfig": { + "kind": "VizConfig", + "group": "geomap", + "version": "13.0.1", + "spec": { + "options": { + "basemap": { + "config": {}, + "name": "Layer 0", + "noRepeat": false, + "type": "default" + }, + "controls": { + "mouseWheelZoom": true, + "showAttribution": true, + "showDebug": false, + "showMeasure": false, + "showScale": false, + "showZoom": true + }, + "layers": [ + { + "config": { + "blur": 15, + "radius": 5, + "weight": { + "fixed": 1, + "max": 1, + "min": 0 + } + }, + "filterData": { + "id": "byRefId", + "options": "A" + }, + "location": { + "mode": "auto" + }, + "name": "Poo", + "tooltip": true, + "type": "heatmap" + } + ], + "tooltip": { + "mode": "details" + }, + "view": { + "allLayers": true, + "dashboardVariable": false, + "id": "zero", + "lat": 0, + "lon": 0, + "noRepeat": false, + "zoom": 1 + } + }, + "fieldConfig": { + "defaults": { + "thresholds": { + "mode": "absolute", + "steps": [ + { + "value": 0, + "color": "green" + }, + { + "value": 80, + "color": "red" + } + ] + }, + "color": { + "mode": "thresholds" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + } + } + } + } + } + }, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "x": 0, + "y": 0, + "width": 24, + "height": 19, + "element": { + "kind": "ElementReference", + "name": "panel-1" + } + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "timezone": "browser", + "from": "now-5y", + "to": "now", + "autoRefresh": "", + "autoRefreshIntervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "hideTimepicker": false, + "fiscalYearStartMonth": 0 + }, + "title": "Mika Poo", + "variables": [], + "preferences": { + "layout": { + "kind": "GridLayout", + "spec": { + "items": [] + } + } + } + } +} \ No newline at end of file diff --git a/grafana/provisioning/dashboards/provider.yaml b/grafana/provisioning/dashboards/provider.yaml new file mode 100644 index 0000000..de78613 --- /dev/null +++ b/grafana/provisioning/dashboards/provider.yaml @@ -0,0 +1,13 @@ +apiVersion: 1 + +providers: + - name: home-automation-dashboards + orgId: 1 + folder: "" + type: file + disableDeletion: false + allowUiUpdates: false + updateIntervalSeconds: 30 + options: + path: /var/lib/grafana/dashboards + foldersFromFilesStructure: false \ No newline at end of file diff --git a/grafana/provisioning/datasources/locationrecorder.yaml b/grafana/provisioning/datasources/locationrecorder.yaml new file mode 100644 index 0000000..0c9749b --- /dev/null +++ b/grafana/provisioning/datasources/locationrecorder.yaml @@ -0,0 +1,11 @@ +apiVersion: 1 + +datasources: + - name: locationrecorder + uid: ffjhr941d5iwwf + type: frser-sqlite-datasource + access: proxy + isDefault: false + editable: false + jsonData: + path: /data/home-automation/locationRecorder.db \ No newline at end of file diff --git a/grafana/provisioning/datasources/poorecorder.yaml b/grafana/provisioning/datasources/poorecorder.yaml new file mode 100644 index 0000000..0fcd352 --- /dev/null +++ b/grafana/provisioning/datasources/poorecorder.yaml @@ -0,0 +1,11 @@ +apiVersion: 1 + +datasources: + - name: poorecorder + uid: ffjhkuu4hc3y8e + type: frser-sqlite-datasource + access: proxy + isDefault: false + editable: false + jsonData: + path: /data/home-automation/pooRecorder.db \ No newline at end of file