refactor(monitoring): rearchitect taupi-fan dashboard & clean up docs
Dashboard changes: - Merge 3 separate correlation graphs into one full-width panel - Left Y axis: temperatures + dewpoints (°C) - Right Y axis: humidity + mold threshold (%) - Boolean signals (fan/mold/activation) as colored fill bands - graphTooltip: 2 for shared crosshair + tooltip - Add collapsed Deep Dive row with delta graphs - Collapse BLE diagnostics row Documentation changes: - DASHBOARD_CREATION.md: remove content now covered by Grafana skills (dashboarding, grafana-oss, promql), eliminate duplicated pitfalls section, add Prometheus dashboard guidance, add Grafana unit codes - AGENTS.md: add skills prerequisite table, update folder structure (add taupi-fan.yaml), add Prometheus data flow, remove duplicated InfluxDB/Flux/pitfalls sections (canonical in DASHBOARD_CREATION.md)
This commit is contained in:
parent
304ca52cca
commit
a623ec7b85
|
|
@ -4,22 +4,36 @@
|
|||
|
||||
This folder manages the **kube-prometheus-stack** deployment and **Grafana dashboards as code** for the `haumdaucher` Kubernetes cluster. The monitoring stack runs in the `monitoring` namespace.
|
||||
|
||||
## Prerequisites — Grafana Skills
|
||||
|
||||
Before creating or modifying dashboards, **read these skills first** — they cover general Grafana topics (JSON schema, panel types, PromQL, provisioning) that this document does not repeat:
|
||||
|
||||
| Skill | Covers |
|
||||
|---|---|
|
||||
| **`dashboarding`** | Dashboard JSON schema, panel types, units, template variables, transformations, annotations |
|
||||
| **`grafana-oss`** | Dashboard/datasource provisioning, RBAC, plugin config, health checks |
|
||||
| **`promql`** | PromQL query writing, rate/irate, histograms, recording rules, cardinality |
|
||||
|
||||
For project-specific dashboard creation workflows (ConfigMap template, InfluxDB Flux queries, deploy/verify steps), see **[DASHBOARD_CREATION.md](DASHBOARD_CREATION.md)**.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
```
|
||||
monitoring/
|
||||
├── AGENTS.md ← You are here
|
||||
├── README.md ← Human-oriented deployment guide
|
||||
├── DASHBOARD_CREATION.md ← How to create/update dashboards (read this!)
|
||||
├── DASHBOARD_CREATION.md ← Project-specific dashboard creation guide
|
||||
├── prometheus-operator.secret.yml ← Helm values for kube-prometheus-stack
|
||||
├── alertmanagerconfig.secret.yaml ← Alertmanager config (Telegram alerts)
|
||||
├── servicemonitor.secret.yml ← ServiceMonitor examples (commented out)
|
||||
├── taupi-fan.yml ← Cellar fan exporter (Service, Endpoints, ServiceMonitor, Alerts)
|
||||
├── tankerkoenig.yml ← Tankerkoenig fuel price exporter
|
||||
├── dashboards/ ← Grafana dashboards as ConfigMaps
|
||||
│ ├── grafana-dashboard-home-climate.yaml
|
||||
│ ├── grafana-dashboard-energy-monitor.yaml
|
||||
│ ├── grafana-dashboard-heating-gas.yaml
|
||||
│ └── grafana-dashboard-outdoor-weather.yaml
|
||||
│ ├── grafana-dashboard-outdoor-weather.yaml
|
||||
│ └── grafana-dashboard-taupi-fan.yaml
|
||||
└── scripts/ ← Discovery & helper scripts
|
||||
├── discover-influxdb.sh ← Query InfluxDB schema
|
||||
└── discover-home-assistant.sh ← Query Home Assistant entities
|
||||
|
|
@ -41,6 +55,10 @@ monitoring/
|
|||
Home Assistant sensors → InfluxDB (bucket: default, org: influxdata)
|
||||
↓
|
||||
Grafana (Flux queries via datasource)
|
||||
|
||||
Custom exporters (taupi-fan, etc.) → Prometheus (scraped via ServiceMonitor)
|
||||
↓
|
||||
Grafana (PromQL queries)
|
||||
```
|
||||
|
||||
### Grafana Datasources
|
||||
|
|
@ -73,92 +91,21 @@ Home Assistant sensors → InfluxDB (bucket: default, org: influxdata)
|
|||
|
||||
## Dashboard Architecture
|
||||
|
||||
Dashboards are managed as **Kubernetes ConfigMaps** with the label `grafana_dashboard: "1"`. The kube-prometheus-stack's **Grafana Dashboard Sidecar** (`grafana-sc-dashboard` container) watches for these ConfigMaps and auto-loads them into Grafana.
|
||||
Dashboards are managed as **Kubernetes ConfigMaps** with the label `grafana_dashboard: "1"`. The kube-prometheus-stack's **Grafana Dashboard Sidecar** (`grafana-sc-dashboard` container, singular!) watches for these ConfigMaps and auto-loads them into Grafana.
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
1. **One file per dashboard** — Each ConfigMap YAML contains the dashboard JSON embedded directly in `data`. No separate JSON files (avoids duplication).
|
||||
2. **Strict GitOps** — Dashboards are `editable: false` in Grafana UI. All changes must be made to the YAML files and re-applied.
|
||||
3. **Namespace-scoped** — Sidecar only watches the `monitoring` namespace (`searchNamespace: monitoring`).
|
||||
4. **Folder annotation** — Use `grafana_dashboard_folder` annotation to organize dashboards in Grafana folders.
|
||||
1. **One file per dashboard** — Each ConfigMap YAML contains the dashboard JSON embedded directly in `data`.
|
||||
2. **Strict GitOps** — Dashboards are `editable: false` in Grafana UI. All changes go through YAML files.
|
||||
3. **Namespace-scoped** — Sidecar only watches `monitoring` namespace (`searchNamespace: monitoring`).
|
||||
4. **Folder annotation** — Use `grafana_dashboard_folder` annotation to organize dashboards.
|
||||
5. **No `"dashboard"` wrapper** — File provisioning requires top-level JSON properties. The `{"dashboard": {...}}` wrapper is only for the HTTP API.
|
||||
6. **Never add `uid` to helm datasource config** — Let Grafana auto-generate UIDs. Adding `uid` to `additionalDataSources` crashes provisioning.
|
||||
|
||||
### ConfigMap Template
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: grafana-dashboard-<name>
|
||||
namespace: monitoring
|
||||
labels:
|
||||
grafana_dashboard: "1"
|
||||
annotations:
|
||||
grafana_dashboard_folder: "🏠 Home"
|
||||
data:
|
||||
<name>.json: |
|
||||
{
|
||||
"uid": "<unique-uid>",
|
||||
"title": "<Dashboard Title>",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## InfluxDB Data Schema
|
||||
|
||||
Home Assistant writes sensor data to InfluxDB with this structure:
|
||||
|
||||
- **Measurement name** = unit of measurement (e.g., `°C`, `%`, `W`, `kWh`, `m³`, `hPa`, `km/h`, `mm/h`)
|
||||
- **Fields**: `value` (numeric), plus metadata fields (`friendly_name_str`, `device_class_str`, `id_str`, etc.)
|
||||
- **Tags**: `entity_id` (e.g., `home_wohnzimmer_temperature` — **NOTE: no `sensor.` prefix!**), `domain` (e.g., `sensor`)
|
||||
|
||||
### Flux Query Pattern (MUST follow this exactly)
|
||||
|
||||
**Exact match on entity_id:**
|
||||
```flux
|
||||
from(bucket: "default")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_measurement"] == "°C")
|
||||
|> filter(fn: (r) => r["_field"] == "value")
|
||||
|> filter(fn: (r) => r.entity_id == "home_wandthermostat_bad_temperature")
|
||||
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|
||||
|> yield(name: "mean")
|
||||
```
|
||||
|
||||
**Regex match on entity_id** (use `=~` operator with `/pattern/`):
|
||||
```flux
|
||||
from(bucket: "default")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_measurement"] == "°C")
|
||||
|> filter(fn: (r) => r["_field"] == "value")
|
||||
|> filter(fn: (r) => r.entity_id =~ /^home_/)
|
||||
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|
||||
|> yield(name: "mean")
|
||||
```
|
||||
|
||||
**Critical rules:**
|
||||
- Always use bracket notation: `r["_measurement"]`, `r["_field"]` (fields starting with `_`)
|
||||
- Always use dynamic time: `range(start: v.timeRangeStart, stop: v.timeRangeStop)` and `aggregateWindow(every: v.windowPeriod, ...)`
|
||||
- Always end with `|> yield(name: "mean")`
|
||||
- **entity_id in InfluxDB does NOT have the `sensor.` prefix** — use `home_wohnzimmer_temperature`, NOT `sensor.home_wohnzimmer_temperature`
|
||||
- For "current value" stat panels, use `range(start: -15m)` with `last()` instead of aggregateWindow
|
||||
- Regex operator is `=~` with `/pattern/` syntax (e.g., `=~ /^home_/` matches all entities starting with `home_`)
|
||||
|
||||
## Common Pitfalls (Lessons Learned)
|
||||
|
||||
1. **Entity IDs have no `sensor.` prefix in InfluxDB** — HA stores domain separately. Use `home_wohnzimmer_temperature`, not `sensor.home_wohnzimmer_temperature`. This is the #1 cause of "no data".
|
||||
2. **Dashboard JSON must NOT be wrapped in `"dashboard"` key** — Grafana file provisioning expects top-level properties (`uid`, `title`, `panels`, etc.). The `"dashboard"` wrapper is only used in the HTTP API.
|
||||
3. **Datasource reference must use actual UID** — Discover via `kubectl exec` + Grafana API. Current UID: `P2AB959DC95E5519F`.
|
||||
4. **Don't add `uid` to helm datasource config** — Adding `uid` to existing datasources in `additionalDataSources` causes Grafana provisioning to fail with "data source not found". Let UIDs be auto-generated.
|
||||
5. **Always use bracket notation for `_measurement` and `_field`** — Use `r["_measurement"]` and `r["_field"]`, not dot notation. Fields starting with underscore may not resolve with dot notation in Flux.
|
||||
6. **Always use Grafana time variables** — Never hardcode `range(start: -24h)` or `aggregateWindow(every: 5m)`. Always use `v.timeRangeStart`, `v.timeRangeStop`, `v.windowPeriod`.
|
||||
7. **Always add `yield(name: "mean")`** — Required for Grafana to properly parse Flux results.
|
||||
8. **InfluxDB has multiple fields per measurement** — Always filter `r["_field"] == "value"` to get numeric data.
|
||||
9. **Sidecar container name is `grafana-sc-dashboard`** (singular), not `grafana-sc-dashboards`.
|
||||
10. **Use Wandthermostat entities for temperature/humidity** — Query HA to filter by device type. Wandthermostat entities have `wandthermostat` or `thermostat` in their entity_id.
|
||||
For the full creation workflow, Flux query templates, and InfluxDB conventions, see **[DASHBOARD_CREATION.md](DASHBOARD_CREATION.md)**.
|
||||
|
||||
## Related Files Outside This Folder
|
||||
|
||||
- `../home-assistant/home-assistant.secret.yaml` — HA helm values (InfluxDB integration config)
|
||||
- `../home-assistant/tmp_long_lived_token.secret.yml` — HA API token for discovery
|
||||
- `../influxdb/influxdb2.secret.yml` — InfluxDB helm values
|
||||
- `../../kochbuch/plans/grafana-dashboards-as-code.md` — Original architecture plan
|
||||
- `../influxdb/influxdb2.secret.yml` — InfluxDB helm values
|
||||
|
|
@ -2,102 +2,79 @@
|
|||
|
||||
> **Linked from**: [AGENTS.md](AGENTS.md)
|
||||
|
||||
This document captures the exact process used to create the Grafana dashboards in this folder, including all pitfalls encountered and lessons learned. Use this as a reference when creating new dashboards or updating existing ones.
|
||||
This document captures the project-specific process for creating Grafana dashboards in this folder. For general Grafana dashboard JSON schema, panel types, units, transformations, and API workflows, use the **Grafana skills** (`dashboarding`, `grafana-oss`, `promql`) — they are the authoritative reference for Grafana-native topics.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Always read the skills first** before creating a dashboard:
|
||||
> - **`dashboarding`** — JSON schema, panel types, units, template variables, transformations, links, annotations
|
||||
> - **`grafana-oss`** — Dashboard provisioning, datasource config, RBAC, plugin provisioning
|
||||
> - **`promql`** — PromQL query patterns, rate/irate, histograms, recording rules
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Dashboard Creation Process
|
||||
## Step 1: Identify the Data Source
|
||||
|
||||
### Step 1: Discovery — Find Available Metrics
|
||||
This project has **two** types of dashboard data sources:
|
||||
|
||||
Before creating any dashboard, discover what data actually exists:
|
||||
| Data Source | Type | When to Use | Query Language |
|
||||
|---|---|---|---|
|
||||
| Prometheus | `prometheus` | Kubernetes metrics, custom exporters (taupi-fan, tankerkoenig) | PromQL |
|
||||
| InfluxDB Home Assistant | `influxdb` (Flux) | Home Assistant sensor data (temperature, humidity, energy, etc.) | Flux |
|
||||
|
||||
#### 1a. Query Home Assistant for Entity Metadata
|
||||
**Prometheus dashboards** are simpler — just write PromQL expressions in `targets[].expr`. See the `promql` skill and existing examples like [grafana-dashboard-taupi-fan.yaml](dashboards/grafana-dashboard-taupi-fan.yaml).
|
||||
|
||||
```bash
|
||||
# Get all entities with their friendly names, units, and device classes
|
||||
curl -s -H "Authorization: Bearer <HA_TOKEN>" \
|
||||
https://hass.moritzgraf.de/api/states | \
|
||||
jq '[.[] | select(.attributes.unit_of_measurement != null) | {
|
||||
entity_id,
|
||||
unit: .attributes.unit_of_measurement,
|
||||
friendly_name: (.attributes.friendly_name // ""),
|
||||
device_class: (.attributes.device_class // "")
|
||||
}] | group_by(.unit)'
|
||||
```
|
||||
**InfluxDB dashboards** require Flux queries with project-specific conventions (see [§ InfluxDB Flux Queries](#influxdb-flux-queries) below).
|
||||
|
||||
Or use the helper script: `./scripts/discover-home-assistant.sh`
|
||||
### Datasource UIDs
|
||||
|
||||
#### 1b. Query InfluxDB for Measurements and Schema
|
||||
|
||||
```bash
|
||||
# Port-forward to InfluxDB (no ingress configured)
|
||||
kubectl -n influxdb port-forward svc/influxdb-influxdb2 8086:80
|
||||
|
||||
# List measurements with data in last 6 months
|
||||
curl -s "http://localhost:8086/api/v2/query?org=influxdata" \
|
||||
-H "Authorization: Token <INFLUXDB_TOKEN>" \
|
||||
-H "Content-Type: application/vnd.flux" \
|
||||
-d 'from(bucket: "default")
|
||||
|> range(start: -6mo)
|
||||
|> keep(columns: ["_measurement"])
|
||||
|> distinct()
|
||||
|> sort()'
|
||||
|
||||
# For a specific measurement, get fields and tags
|
||||
curl -s "http://localhost:8086/api/v2/query?org=influxdata" \
|
||||
-H "Authorization: Token <INFLUXDB_TOKEN>" \
|
||||
-H "Content-Type: application/vnd.flux" \
|
||||
-d 'import "influxdata/influxdb/schema"
|
||||
schema.measurementFieldKeys(bucket: "default", measurement: "°C")'
|
||||
```
|
||||
|
||||
Or use: `./scripts/discover-influxdb.sh`
|
||||
|
||||
#### 1c. Verify Data Exists
|
||||
|
||||
Always test a sample query before building dashboards:
|
||||
|
||||
```bash
|
||||
curl -s "http://localhost:8086/api/v2/query?org=influxdata" \
|
||||
-H "Authorization: Token <INFLUXDB_TOKEN>" \
|
||||
-H "Content-Type: application/vnd.flux" \
|
||||
-d 'from(bucket: "default")
|
||||
|> range(start: -1h)
|
||||
|> filter(fn: (r) => r._measurement == "°C" and r._field == "value")
|
||||
|> limit(n: 5)'
|
||||
```
|
||||
|
||||
### Step 2: Discover Grafana Datasource UIDs
|
||||
|
||||
Datasource UIDs change when Grafana is redeployed. Always discover the current UIDs:
|
||||
Datasource references in panels require the **actual UID**, not the display name. UIDs can change when Grafana is redeployed. Current known UIDs are documented in [AGENTS.md](AGENTS.md) — always verify before creating a new dashboard:
|
||||
|
||||
```bash
|
||||
kubectl -n monitoring exec deploy/prometheus-operator-grafana -c grafana -- \
|
||||
curl -s -u "admin:<GRAFANA_PASSWORD>" \
|
||||
http://localhost:3000/api/datasources | \
|
||||
jq '.[] | {name, uid, type}'
|
||||
http://localhost:3000/api/datasources | jq '.[] | {name, uid, type}'
|
||||
```
|
||||
|
||||
The Grafana admin password is in [`prometheus-operator.secret.yml`](prometheus-operator.secret.yml) under `grafana.adminPassword`.
|
||||
|
||||
### Step 3: Select the Right Entities
|
||||
---
|
||||
|
||||
**Important**: Not all sensors of the same type are equal. For example:
|
||||
- Use **Wandthermostat** (wall thermostat) entities for temperature/humidity, not raw sensor entities. Wandthermostats provide more reliable, calibrated readings.
|
||||
- Query Home Assistant to filter by entity name pattern or device type.
|
||||
## Step 2: Discover Available Metrics
|
||||
|
||||
### For Prometheus Dashboards
|
||||
|
||||
Query Prometheus directly or use the Grafana Explore UI:
|
||||
|
||||
```bash
|
||||
# Find all Wandthermostat temperature sensors
|
||||
curl -s -H "Authorization: Bearer <HA_TOKEN>" \
|
||||
https://hass.moritzgraf.de/api/states | \
|
||||
jq '[.[] | select(.attributes.unit_of_measurement == "°C" and
|
||||
(.entity_id | test("wandthermostat|thermostat"; "i")))]'
|
||||
# Port-forward to Prometheus
|
||||
kubectl -n monitoring port-forward svc/prometheus-operated 9090
|
||||
|
||||
# Browse available metrics
|
||||
curl -s http://localhost:9090/api/v1/label/__name__/values | jq '.data[]' | grep '<prefix>'
|
||||
```
|
||||
|
||||
### Step 4: Create the Dashboard ConfigMap YAML
|
||||
### For InfluxDB Dashboards (Home Assistant Data)
|
||||
|
||||
Create a new file in `dashboards/` following this template:
|
||||
Use the helper scripts to discover what data exists:
|
||||
|
||||
```bash
|
||||
# Query Home Assistant for entity metadata
|
||||
./scripts/discover-home-assistant.sh
|
||||
|
||||
# Query InfluxDB for measurements and schema
|
||||
# (requires port-forward first: kubectl -n influxdb port-forward svc/influxdb-influxdb2 8086:80)
|
||||
./scripts/discover-influxdb.sh
|
||||
```
|
||||
|
||||
**Sensor selection tip**: Use **Wandthermostat** entities for temperature/humidity (they provide calibrated readings). Filter by `wandthermostat` or `thermostat` in the entity_id.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Create the Dashboard ConfigMap
|
||||
|
||||
Dashboards are deployed as **Kubernetes ConfigMaps** with the label `grafana_dashboard: "1"`. The Grafana sidecar (`grafana-sc-dashboard` container) watches for these and auto-loads them.
|
||||
|
||||
### ConfigMap Template
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
|
|
@ -114,57 +91,94 @@ data:
|
|||
{
|
||||
"uid": "<unique-uid>",
|
||||
"title": "<Dashboard Title>",
|
||||
"tags": ["home", "<category>", "auto-generated"],
|
||||
"tags": ["<category>"],
|
||||
"timezone": "browser",
|
||||
"schemaVersion": 39,
|
||||
"version": 1,
|
||||
"refresh": "5m",
|
||||
"refresh": "30s",
|
||||
"editable": false,
|
||||
"graphTooltip": 1,
|
||||
"graphTooltip": 2,
|
||||
"time": { "from": "now-24h", "to": "now" },
|
||||
"panels": [ ... ]
|
||||
}
|
||||
```
|
||||
|
||||
#### Critical JSON Structure Rules
|
||||
> [!CAUTION]
|
||||
> **No `"dashboard"` wrapper.** The JSON must have `uid`, `title`, `panels` at the top level. The `{"dashboard": {...}}` wrapper is only for the Grafana HTTP API — file provisioning via the sidecar will fail with *"Dashboard title cannot be empty"* if you use it.
|
||||
|
||||
**✅ CORRECT** (top-level properties, no wrapper):
|
||||
```json
|
||||
{
|
||||
"uid": "my-dashboard",
|
||||
"title": "My Dashboard",
|
||||
"panels": [...]
|
||||
}
|
||||
For the full panel JSON schema, units, thresholds, overrides, and transformations, see the **`dashboarding`** skill's `references/json-schema.md`.
|
||||
|
||||
### Project-Specific Dashboard Conventions
|
||||
|
||||
- `"editable": false` — strict GitOps; never edit in the Grafana UI
|
||||
- `"graphTooltip": 2` — shared crosshair + tooltip across all panels
|
||||
- Use emoji in titles for visual appeal (🏠 ⚡ 🔥 🌡️ 💧 💨)
|
||||
- Include `calcs` in legend: `["lastNotNull", "min", "max", "mean"]`
|
||||
- Use `"spanNulls": true` to handle gaps in sensor data
|
||||
- Use semantic threshold colors: blue=cold, green=comfort, orange=warm, red=hot
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Deploy & Verify
|
||||
|
||||
```bash
|
||||
# Apply
|
||||
kubectl apply -f dashboards/grafana-dashboard-<name>.yaml
|
||||
|
||||
# Verify ConfigMap labeling
|
||||
kubectl get configmaps -n monitoring -l grafana_dashboard=1
|
||||
|
||||
# Check sidecar pickup (container name is singular: grafana-sc-dashboard)
|
||||
kubectl -n monitoring logs -l "app.kubernetes.io/name=grafana" \
|
||||
-c grafana-sc-dashboard --tail=20
|
||||
|
||||
# Check for Grafana errors
|
||||
kubectl -n monitoring logs -l "app.kubernetes.io/name=grafana" \
|
||||
-c grafana --tail=30 | grep -i "error\|failed"
|
||||
```
|
||||
|
||||
**❌ WRONG** (wrapped in "dashboard" key — causes "Dashboard title cannot be empty"):
|
||||
```json
|
||||
{
|
||||
"dashboard": {
|
||||
"uid": "my-dashboard",
|
||||
"title": "My Dashboard",
|
||||
"panels": [...]
|
||||
}
|
||||
}
|
||||
### Updating an Existing Dashboard
|
||||
|
||||
1. Edit the YAML file in `dashboards/`
|
||||
2. Increment the `version` field in the dashboard JSON
|
||||
3. `kubectl apply -f dashboards/grafana-dashboard-<name>.yaml`
|
||||
4. Sidecar detects the change within ~60 seconds
|
||||
|
||||
### Removing a Dashboard
|
||||
|
||||
```bash
|
||||
kubectl delete configmap -n monitoring grafana-dashboard-<name>
|
||||
```
|
||||
|
||||
The `"dashboard"` wrapper is **only** used in the Grafana HTTP API (`/api/dashboards/db`). For file-based provisioning via the sidecar, properties must be at the top level.
|
||||
---
|
||||
|
||||
#### Datasource Reference
|
||||
## InfluxDB Flux Queries
|
||||
|
||||
**✅ CORRECT** (by actual UID):
|
||||
```json
|
||||
"datasource": { "type": "influxdb", "uid": "P2AB959DC95E5519F" }
|
||||
```
|
||||
This section covers the **project-specific Flux conventions** for querying Home Assistant data in InfluxDB. These patterns are unique to this project and not covered by the Grafana skills.
|
||||
|
||||
**❌ WRONG** (by name — won't resolve):
|
||||
```json
|
||||
"datasource": { "type": "influxdb", "uid": "InfluxDB Home Assistant" }
|
||||
```
|
||||
### Data Schema
|
||||
|
||||
#### Flux Query Template
|
||||
Home Assistant writes to InfluxDB with this structure:
|
||||
|
||||
**Exact match on entity_id:**
|
||||
- **Measurement name** = unit of measurement (e.g., `°C`, `%`, `W`, `kWh`)
|
||||
- **Tags**: `entity_id` (e.g., `home_wohnzimmer_temperature`), `domain` (e.g., `sensor`)
|
||||
- **Fields**: `value` (numeric), plus metadata strings (`friendly_name_str`, etc.)
|
||||
|
||||
### Critical Flux Query Rules
|
||||
|
||||
> [!WARNING]
|
||||
> These rules come from hard-won debugging. Violating any of them will result in "no data" panels.
|
||||
|
||||
1. **`entity_id` has NO `sensor.` prefix** — HA stores the domain in a separate `domain` tag. Use `home_wohnzimmer_temperature`, NOT `sensor.home_wohnzimmer_temperature`.
|
||||
2. **Always use bracket notation** for underscore-prefixed fields: `r["_measurement"]`, `r["_field"]` — dot notation (`r._measurement`) may silently fail.
|
||||
3. **Always use Grafana time variables** — `v.timeRangeStart`, `v.timeRangeStop`, `v.windowPeriod`. Never hardcode `range(start: -24h)`.
|
||||
4. **Always end with `|> yield(name: "mean")`** — required for Grafana to parse Flux results.
|
||||
5. **Always filter `r["_field"] == "value"`** — InfluxDB stores metadata strings in other fields.
|
||||
6. **Use regex for multi-entity panels** — `r.entity_id =~ /^home_/` instead of `or` chains (Flux `or` inside a single filter is unreliable).
|
||||
|
||||
### Flux Query Templates
|
||||
|
||||
**Time series panel (aggregated):**
|
||||
```flux
|
||||
from(bucket: "default")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|
|
@ -174,17 +188,7 @@ from(bucket: "default")
|
|||
|> yield(name: "mean")
|
||||
```
|
||||
|
||||
**Regex match on entity_id** (for panels showing multiple entities matching a pattern):
|
||||
```flux
|
||||
from(bucket: "default")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_measurement"] == "<UNIT>" and r["_field"] == "value")
|
||||
|> filter(fn: (r) => r.entity_id =~ /^home_/)
|
||||
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|
||||
|> yield(name: "mean")
|
||||
```
|
||||
|
||||
**Current value** (for stat panels showing last reading):
|
||||
**Stat panel (current value):**
|
||||
```flux
|
||||
from(bucket: "default")
|
||||
|> range(start: -15m)
|
||||
|
|
@ -194,133 +198,29 @@ from(bucket: "default")
|
|||
|> yield(name: "mean")
|
||||
```
|
||||
|
||||
**Key points**:
|
||||
- Use bracket notation `r["_measurement"]`, `r["_field"]` for fields starting with underscore.
|
||||
- **entity_id in InfluxDB has NO `sensor.` prefix** — HA stores domain in a separate `domain` tag. Use `home_wohnzimmer_temperature`, not `sensor.home_wohnzimmer_temperature`.
|
||||
- Always use `v.timeRangeStart`, `v.timeRangeStop`, `v.windowPeriod` — never hardcode time values.
|
||||
- Always end with `|> yield(name: "mean")` — required for Grafana to parse Flux results.
|
||||
- Use regex `=~ /pattern/` for multi-entity panels (e.g., `=~ /^home_/` matches all home entities).
|
||||
- Always filter `r["_field"] == "value"` — InfluxDB stores metadata strings in other fields.
|
||||
|
||||
### Step 5: Deploy
|
||||
|
||||
```bash
|
||||
# Apply all dashboards
|
||||
kubectl apply -f dashboards/
|
||||
|
||||
# Apply a single dashboard
|
||||
kubectl apply -f dashboards/grafana-dashboard-<name>.yaml
|
||||
**Multi-entity panel (regex):**
|
||||
```flux
|
||||
from(bucket: "default")
|
||||
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|
||||
|> filter(fn: (r) => r["_measurement"] == "<UNIT>" and r["_field"] == "value")
|
||||
|> filter(fn: (r) => r.entity_id =~ /^home_/)
|
||||
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|
||||
|> yield(name: "mean")
|
||||
```
|
||||
|
||||
### Step 6: Verify
|
||||
|
||||
```bash
|
||||
# Check ConfigMaps are labeled correctly
|
||||
kubectl get configmaps -n monitoring -l grafana_dashboard=1
|
||||
|
||||
# Check sidecar logs (container name is grafana-sc-dashboard, singular!)
|
||||
kubectl -n monitoring logs -l "app.kubernetes.io/name=grafana" \
|
||||
-c grafana-sc-dashboard --tail=20
|
||||
|
||||
# Check Grafana main logs for errors
|
||||
kubectl -n monitoring logs -l "app.kubernetes.io/name=grafana" \
|
||||
-c grafana --tail=30 | grep -i "error\|failed"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update Process (Modifying an Existing Dashboard)
|
||||
|
||||
1. Edit the YAML file in `dashboards/`
|
||||
2. Increment the `version` field in the dashboard JSON
|
||||
3. Run `kubectl apply -f dashboards/grafana-dashboard-<name>.yaml`
|
||||
4. The sidecar will detect the change and reload within ~60 seconds
|
||||
5. Verify in Grafana UI
|
||||
|
||||
**Never edit dashboards in the Grafana UI** — they are `editable: false`. All changes must go through the YAML files.
|
||||
|
||||
---
|
||||
|
||||
## Removing a Dashboard
|
||||
|
||||
```bash
|
||||
kubectl delete configmap -n monitoring grafana-dashboard-<name>
|
||||
```
|
||||
|
||||
The sidecar will remove it from Grafana within ~60 seconds.
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls Encountered in This Conversation
|
||||
|
||||
### Pitfall 1: Entity IDs lack `sensor.` prefix in InfluxDB
|
||||
**Problem**: Used `sensor.home_wohnzimmer_temperature` in queries — no data appeared. Home Assistant stores the domain (`sensor`) in a separate `domain` tag, not in `entity_id`.
|
||||
**Fix**: Use `home_wohnzimmer_temperature` (without prefix). Always query InfluxDB directly to verify entity_id format before building queries.
|
||||
|
||||
### Pitfall 2: Hardcoded time ranges
|
||||
**Problem**: Used `range(start: -24h)` and `aggregateWindow(every: 5m)` — dashboard time picker had no effect, data not loaded properly.
|
||||
**Fix**: Always use `v.timeRangeStart`, `v.timeRangeStop`, `v.windowPeriod`. These Grafana variables adapt to the dashboard time picker.
|
||||
|
||||
### Pitfall 3: Missing `yield()`
|
||||
**Problem**: Flux queries missing `yield(name: "mean")` — Grafana couldn't parse results.
|
||||
**Fix**: Always end Flux queries with `|> yield(name: "mean")`.
|
||||
|
||||
### Pitfall 4: Dot notation for underscore fields
|
||||
**Problem**: Used `r._measurement` and `r._field` — Flux may not resolve fields starting with `_` via dot notation.
|
||||
**Fix**: Use bracket notation: `r["_measurement"]`, `r["_field"]`.
|
||||
|
||||
### Pitfall 5: Datasource UID vs Name
|
||||
**Problem**: Used datasource name as UID (`"uid": "InfluxDB Home Assistant"`). Dashboards loaded but showed "no data".
|
||||
**Fix**: Discover actual UID via `kubectl exec` + Grafana API. Current: `P2AB959DC95E5519F`.
|
||||
|
||||
### Pitfall 6: `"dashboard"` JSON Wrapper
|
||||
**Problem**: Dashboard JSON wrapped in `{"dashboard": {...}}`. Grafana showed "Dashboard title cannot be empty".
|
||||
**Fix**: Top-level properties only. The `"dashboard"` wrapper is for HTTP API, not file provisioning.
|
||||
|
||||
### Pitfall 7: Adding `uid` to Helm Datasource Config
|
||||
**Problem**: Added `uid: influxdb-ha-flux` to helm values — Grafana pod crashed.
|
||||
**Fix**: Let Grafana auto-generate UIDs. Never add `uid` to existing datasources.
|
||||
|
||||
### Pitfall 8: Multi-entity panels using `or` in filter
|
||||
**Problem**: Used `filter(fn: (r) => r.entity_id == "A" or r.entity_id == "B")` — Flux `or` inside a single filter call doesn't reliably work.
|
||||
**Fix**: Use regex: `filter(fn: (r) => r.entity_id =~ /^home_/)` for matching multiple entities.
|
||||
|
||||
### Pitfall 9: Wrong Sensor Selection
|
||||
**Problem**: Used raw temperature sensors instead of Wandthermostat sensors.
|
||||
**Fix**: Query HA to filter entities with `wandthermostat` or `thermostat` in entity_id.
|
||||
|
||||
### Pitfall 10: Sidecar Container Name
|
||||
**Problem**: `kubectl logs -c grafana-sc-dashboards` (plural).
|
||||
**Fix**: Container name is `grafana-sc-dashboard` (singular).
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Design Guidelines
|
||||
|
||||
1. **Use semantic thresholds with colors**: Blue=cold, green=comfort, orange=warm, red=hot for temperature; reverse for grid import (green=low, red=high).
|
||||
2. **Use Stat panels with sparklines** for current values — they show the 24h mini-trend.
|
||||
3. **Use BarGauge for day comparisons** (today vs yesterday vs avg).
|
||||
4. **Use Gauge for ranged values** (battery SoC 0-100%, cloud cover 0-100%).
|
||||
5. **Color-code panel backgrounds** with `"colorMode": "background"` for at-a-glance status.
|
||||
6. **Set `"editable": false`** for strict GitOps.
|
||||
7. **Use emoji in dashboard and panel titles** for visual appeal (🏠 ⚡ 🔥 🌡️ 💧).
|
||||
8. **Add `"auto-generated"` tag** to distinguish from manually created dashboards.
|
||||
9. **Include calcs in legend** (`mean`, `max`, `min`, `lastNotNull`) for quick statistics.
|
||||
10. **Use `"spanNulls": true`** in timeseries to handle gaps in sensor data.
|
||||
|
||||
---
|
||||
|
||||
## InfluxDB Measurement Reference
|
||||
|
||||
| Measurement | Unit | Typical Entities |
|
||||
|-------------|------|------------------|
|
||||
| `°C` | celsius | Wandthermostat temperatures, outdoor temp, battery temp |
|
||||
| `%` | percent | Humidity, battery levels, heating valve position, cloud cover |
|
||||
| `W` | watt | Solar production, self-consumption, grid import/export, house consumption |
|
||||
| `kWh` | kilowatt-hour | Daily energy totals (production, consumption, import, export) |
|
||||
| `m³` | cubic meter | Gas volume counter |
|
||||
| `m³/h` | cubic meter/hour | Current gas flow rate |
|
||||
| `hPa` | hectopascal | Barometric pressure |
|
||||
| `km/h` | kilometer/hour | Wind speed |
|
||||
| `mm/h` | millimeter/hour | Precipitation rate |
|
||||
| `EUR` | euro | Gas cost, electricity cost |
|
||||
| Measurement | Grafana Unit | Typical Entities |
|
||||
|---|---|---|
|
||||
| `°C` | `celsius` | Wandthermostat temperatures, outdoor temp, battery temp |
|
||||
| `%` | `percent` | Humidity, battery levels, heating valve position, cloud cover |
|
||||
| `W` | `watt` | Solar production, self-consumption, grid import/export |
|
||||
| `kWh` | `kwatth` | Daily energy totals (production, consumption, import, export) |
|
||||
| `m³` | `m3` | Gas volume counter |
|
||||
| `m³/h` | `m3/h` | Current gas flow rate |
|
||||
| `hPa` | `pressurehpa` | Barometric pressure |
|
||||
| `km/h` | `velocitykmh` | Wind speed |
|
||||
| `mm/h` | `lengthmm` | Precipitation rate |
|
||||
| `EUR` | `currencyEUR` | Gas cost, electricity cost |
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ data:
|
|||
"tags": ["climate", "fan", "cellar", "prometheus"],
|
||||
"timezone": "browser",
|
||||
"schemaVersion": 39,
|
||||
"version": 1,
|
||||
"version": 3,
|
||||
"refresh": "30s",
|
||||
"editable": false,
|
||||
"graphTooltip": 1,
|
||||
"graphTooltip": 2,
|
||||
"time": {
|
||||
"from": "now-24h",
|
||||
"to": "now"
|
||||
|
|
@ -50,7 +50,7 @@ data:
|
|||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_relay_status", "legendFormat": "Fan Status"}
|
||||
{"expr": "taupi_relay_status", "legendFormat": "Fan Status", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -72,7 +72,7 @@ data:
|
|||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_is_critical", "legendFormat": "Mold Danger"}
|
||||
{"expr": "taupi_is_critical", "legendFormat": "Mold Danger", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -94,7 +94,7 @@ data:
|
|||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_would_fan_activate", "legendFormat": "Dewpoint Condition"}
|
||||
{"expr": "taupi_would_fan_activate", "legendFormat": "Dewpoint Condition", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -116,150 +116,389 @@ data:
|
|||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "up{job=\"taupi-fan\"}", "legendFormat": "Connectivity"}
|
||||
{"expr": "up{job=\"taupi-fan\"}", "legendFormat": "Connectivity", "refId": "A"}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"title": "🌡️ Climate Details",
|
||||
"title": "📊 Climate Correlation",
|
||||
"type": "row",
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 5},
|
||||
"collapsed": false
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Temperatures",
|
||||
"title": "📊 Full Climate & Fan Correlation",
|
||||
"description": "All metrics in one view. Left axis: temperatures & dewpoints (°C). Right axis: humidity & mold threshold (%). Colored background bands: orange = fan running, red = mold critical, green = activation criteria met. Hover anywhere to read all values at that timestamp.",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 6},
|
||||
"gridPos": {"h": 14, "w": 24, "x": 0, "y": 6},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "celsius",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 5,
|
||||
"fillOpacity": 0,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_temperature_celsius_innen", "legendFormat": "Indoor Temperature"},
|
||||
{"expr": "taupi_temperature_celsius_aussen", "legendFormat": "Outdoor Temperature"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Dew Points",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 6},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "celsius",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 5,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Fan Active"},
|
||||
"properties": [
|
||||
{"id": "color", "value": {"fixedColor": "#FF9800", "mode": "fixed"}},
|
||||
{"id": "custom.axisPlacement", "value": "hidden"},
|
||||
{"id": "custom.drawStyle", "value": "line"},
|
||||
{"id": "custom.lineInterpolation", "value": "stepAfter"},
|
||||
{"id": "custom.lineWidth", "value": 0},
|
||||
{"id": "custom.fillOpacity", "value": 15},
|
||||
{"id": "min", "value": 0},
|
||||
{"id": "max", "value": 1},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "mappings", "value": [{"type": "value", "options": {"0": {"text": "OFF"}, "1": {"text": "ON"}}}]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Mold Critical"},
|
||||
"properties": [
|
||||
{"id": "color", "value": {"fixedColor": "#F44336", "mode": "fixed"}},
|
||||
{"id": "custom.axisPlacement", "value": "hidden"},
|
||||
{"id": "custom.drawStyle", "value": "line"},
|
||||
{"id": "custom.lineInterpolation", "value": "stepAfter"},
|
||||
{"id": "custom.lineWidth", "value": 0},
|
||||
{"id": "custom.fillOpacity", "value": 10},
|
||||
{"id": "min", "value": 0},
|
||||
{"id": "max", "value": 1},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "mappings", "value": [{"type": "value", "options": {"0": {"text": "SAFE"}, "1": {"text": "CRITICAL"}}}]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Activation Criteria"},
|
||||
"properties": [
|
||||
{"id": "color", "value": {"fixedColor": "#4CAF50", "mode": "fixed"}},
|
||||
{"id": "custom.axisPlacement", "value": "hidden"},
|
||||
{"id": "custom.drawStyle", "value": "line"},
|
||||
{"id": "custom.lineInterpolation", "value": "stepAfter"},
|
||||
{"id": "custom.lineWidth", "value": 0},
|
||||
{"id": "custom.fillOpacity", "value": 8},
|
||||
{"id": "min", "value": 0},
|
||||
{"id": "max", "value": 1},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "mappings", "value": [{"type": "value", "options": {"0": {"text": "NO"}, "1": {"text": "YES"}}}]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Indoor Temp"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "left"},
|
||||
{"id": "unit", "value": "celsius"},
|
||||
{"id": "color", "value": {"fixedColor": "#FF6D00", "mode": "fixed"}},
|
||||
{"id": "custom.lineWidth", "value": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Outdoor Temp"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "left"},
|
||||
{"id": "unit", "value": "celsius"},
|
||||
{"id": "color", "value": {"fixedColor": "#2979FF", "mode": "fixed"}},
|
||||
{"id": "custom.lineWidth", "value": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Indoor Dewpoint"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "left"},
|
||||
{"id": "unit", "value": "celsius"},
|
||||
{"id": "color", "value": {"fixedColor": "#FF6D00", "mode": "fixed"}},
|
||||
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [10, 5]}},
|
||||
{"id": "custom.lineWidth", "value": 1}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Outdoor Dewpoint"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "left"},
|
||||
{"id": "unit", "value": "celsius"},
|
||||
{"id": "color", "value": {"fixedColor": "#2979FF", "mode": "fixed"}},
|
||||
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [10, 5]}},
|
||||
{"id": "custom.lineWidth", "value": 1}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Indoor Humidity"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "right"},
|
||||
{"id": "unit", "value": "percent"},
|
||||
{"id": "color", "value": {"fixedColor": "#00BFA5", "mode": "fixed"}},
|
||||
{"id": "custom.lineWidth", "value": 2},
|
||||
{"id": "custom.fillOpacity", "value": 5}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Outdoor Humidity"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "right"},
|
||||
{"id": "unit", "value": "percent"},
|
||||
{"id": "color", "value": {"fixedColor": "#448AFF", "mode": "fixed"}},
|
||||
{"id": "custom.lineWidth", "value": 1}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Mold Threshold"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "right"},
|
||||
{"id": "unit", "value": "percent"},
|
||||
{"id": "color", "value": {"fixedColor": "#F44336", "mode": "fixed"}},
|
||||
{"id": "custom.lineStyle", "value": {"fill": "dash", "dash": [10, 5]}},
|
||||
{"id": "custom.lineWidth", "value": 1}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean"]},
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "min", "max"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_dewpoint_celsius_innen", "legendFormat": "Indoor Dewpoint"},
|
||||
{"expr": "taupi_dewpoint_celsius_aussen", "legendFormat": "Outdoor Dewpoint"}
|
||||
{"expr": "taupi_relay_status", "legendFormat": "Fan Active", "refId": "A"},
|
||||
{"expr": "taupi_is_critical", "legendFormat": "Mold Critical", "refId": "B"},
|
||||
{"expr": "taupi_would_fan_activate", "legendFormat": "Activation Criteria", "refId": "C"},
|
||||
{"expr": "taupi_temperature_celsius_innen", "legendFormat": "Indoor Temp", "refId": "D"},
|
||||
{"expr": "taupi_temperature_celsius_aussen", "legendFormat": "Outdoor Temp", "refId": "E"},
|
||||
{"expr": "taupi_dewpoint_celsius_innen", "legendFormat": "Indoor Dewpoint", "refId": "F"},
|
||||
{"expr": "taupi_dewpoint_celsius_aussen", "legendFormat": "Outdoor Dewpoint", "refId": "G"},
|
||||
{"expr": "taupi_humidity_percent_innen", "legendFormat": "Indoor Humidity", "refId": "H"},
|
||||
{"expr": "taupi_humidity_percent_aussen", "legendFormat": "Outdoor Humidity", "refId": "I"},
|
||||
{"expr": "taupi_critical_humidity_threshold_percent", "legendFormat": "Mold Threshold", "refId": "J"}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"title": "Relative Humidity vs. Mold Threshold",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 8, "w": 24, "x": 0, "y": 14},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 5,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never"
|
||||
}
|
||||
"title": "📈 Deep Dive",
|
||||
"type": "row",
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 16},
|
||||
"collapsed": true,
|
||||
"panels": [
|
||||
{
|
||||
"title": "🌡️ Temperature Differential (Indoor − Outdoor)",
|
||||
"description": "Positive = cellar warmer than outside (normal). Large delta means the fan can effectively dry the cellar air by exchanging it with drier outside air.",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 17},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "celsius",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 10,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never",
|
||||
"gradientMode": "scheme",
|
||||
"thresholdsStyle": {"mode": "area"}
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "blue", "value": null},
|
||||
{"color": "green", "value": 0},
|
||||
{"color": "orange", "value": 5}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean", "lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_temperature_celsius_innen - taupi_temperature_celsius_aussen", "legendFormat": "Temp Delta (In − Out)", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "💧 Dewpoint Spread (Indoor − Outdoor)",
|
||||
"description": "When indoor dewpoint exceeds outdoor dewpoint, running the fan helps: outside air can absorb moisture. Negative = fan would make things worse.",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 17},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "celsius",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 10,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never",
|
||||
"gradientMode": "scheme",
|
||||
"thresholdsStyle": {"mode": "area"}
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "red", "value": null},
|
||||
{"color": "green", "value": 0},
|
||||
{"color": "super-light-green", "value": 3}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean", "lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_dewpoint_celsius_innen - taupi_dewpoint_celsius_aussen", "legendFormat": "Dewpoint Delta (In − Out)", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "💧 Humidity Delta (Indoor − Outdoor)",
|
||||
"description": "How much more humid the cellar is compared to outside. High positive values indicate the fan can effectively remove moisture.",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 0, "y": 25},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 10,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never",
|
||||
"gradientMode": "scheme",
|
||||
"thresholdsStyle": {"mode": "area"}
|
||||
},
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "blue", "value": null},
|
||||
{"color": "green", "value": 0},
|
||||
{"color": "orange", "value": 15}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean", "lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_humidity_percent_innen - taupi_humidity_percent_aussen", "legendFormat": "Humidity Delta (In − Out)", "refId": "A"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "📉 Indoor Humidity vs Fan Runtime",
|
||||
"description": "Overlay of indoor humidity (left axis) and fan state (right axis). Shows the direct effect: when the orange fan-on band appears, does the teal humidity line drop?",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 8, "w": 12, "x": 12, "y": 25},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Indoor Humidity"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "left"},
|
||||
{"id": "unit", "value": "percent"},
|
||||
{"id": "color", "value": {"fixedColor": "#00BFA5", "mode": "fixed"}},
|
||||
{"id": "custom.fillOpacity", "value": 10}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {"id": "byName", "options": "Fan Active"},
|
||||
"properties": [
|
||||
{"id": "custom.axisPlacement", "value": "right"},
|
||||
{"id": "custom.drawStyle", "value": "line"},
|
||||
{"id": "custom.lineInterpolation", "value": "stepAfter"},
|
||||
{"id": "custom.fillOpacity", "value": 30},
|
||||
{"id": "color", "value": {"fixedColor": "orange", "mode": "fixed"}},
|
||||
{"id": "min", "value": 0},
|
||||
{"id": "max", "value": 1},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "mappings", "value": [{"type": "value", "options": {"0": {"text": "OFF"}, "1": {"text": "ON"}}}]}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean", "lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_humidity_percent_innen", "legendFormat": "Indoor Humidity", "refId": "A"},
|
||||
{"expr": "taupi_relay_status", "legendFormat": "Fan Active", "refId": "B"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_humidity_percent_innen", "legendFormat": "Indoor Humidity"},
|
||||
{"expr": "taupi_humidity_percent_aussen", "legendFormat": "Outdoor Humidity"},
|
||||
{"expr": "taupi_critical_humidity_threshold_percent", "legendFormat": "Mold Danger Threshold"}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"title": "🔋 BLE Sensors Diagnostic",
|
||||
"type": "row",
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 22},
|
||||
"collapsed": false
|
||||
},
|
||||
{
|
||||
"title": "Sensor Batteries",
|
||||
"type": "gauge",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 6, "w": 12, "x": 0, "y": 23},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "red", "value": null},
|
||||
{"color": "orange", "value": 20},
|
||||
{"color": "green", "value": 50}
|
||||
]
|
||||
}
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": 17},
|
||||
"collapsed": true,
|
||||
"panels": [
|
||||
{
|
||||
"title": "Sensor Batteries",
|
||||
"type": "gauge",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 6, "w": 12, "x": 0, "y": 18},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{"color": "red", "value": null},
|
||||
{"color": "orange", "value": 20},
|
||||
{"color": "green", "value": 50}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]},
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_battery_percent_innen", "legendFormat": "Indoor Sensor Battery", "refId": "A"},
|
||||
{"expr": "taupi_battery_percent_aussen", "legendFormat": "Outdoor Sensor Battery", "refId": "B"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Sensor Connection Age (Staleness)",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 6, "w": 12, "x": 12, "y": 18},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 5,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_lost_connection_seconds_innen", "legendFormat": "Indoor Connection Stale Time", "refId": "A"},
|
||||
{"expr": "taupi_lost_connection_seconds_aussen", "legendFormat": "Outdoor Connection Stale Time", "refId": "B"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"reduceOptions": {"values": false, "calcs": ["lastNotNull"]},
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_battery_percent_innen", "legendFormat": "Indoor Sensor Battery"},
|
||||
{"expr": "taupi_battery_percent_aussen", "legendFormat": "Outdoor Sensor Battery"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Sensor Connection Age (Staleness)",
|
||||
"type": "timeseries",
|
||||
"datasource": {"type": "prometheus", "uid": "prometheus"},
|
||||
"gridPos": {"h": 6, "w": 12, "x": 12, "y": 23},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "s",
|
||||
"custom": {
|
||||
"lineWidth": 2,
|
||||
"fillOpacity": 5,
|
||||
"spanNulls": true,
|
||||
"showPoints": "never"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}
|
||||
},
|
||||
"targets": [
|
||||
{"expr": "taupi_lost_connection_seconds_innen", "legendFormat": "Indoor Connection Stale Time"},
|
||||
{"expr": "taupi_lost_connection_seconds_aussen", "legendFormat": "Outdoor Connection Stale Time"}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue