From 15e802bebc9b56bd30d5dd74e1088a97463b5f8b Mon Sep 17 00:00:00 2001 From: Moritz Graf Date: Wed, 1 Jul 2026 11:01:01 +0200 Subject: [PATCH] I have added a lot of changes from various ai sessions --- .../group_vars/k8s_cluster/k8s-cluster.yml | 3 + k8s/README.md | 17 +- k8s/cert-manager/production-issuer.yaml | 2 +- k8s/cert-manager/staging-issuer.yaml | 2 +- k8s/home-assistant/home-assistant.secret.yaml | Bin 8122 -> 7352 bytes .../tmp_long_lived_token.secret.yml | 2 + k8s/mailu/backup.secret.yml | 45 - k8s/mailu/mailu.secret.yml | 82 - k8s/mailu/restore.yaml | 25 - k8s/minio/minio.secret.yaml | Bin 622 -> 600 bytes k8s/monitoring/AGENTS.md | 164 ++ k8s/monitoring/DASHBOARD_CREATION.md | 326 +++ k8s/monitoring/README.md | 90 + k8s/monitoring/alertmanagerconfig.secret.yaml | Bin 1565 -> 1779 bytes .../grafana-dashboard-energy-monitor.yaml | 92 + .../grafana-dashboard-heating-gas.yaml | 62 + .../grafana-dashboard-home-climate.yaml | 98 + .../grafana-dashboard-outdoor-weather.yaml | 75 + k8s/monitoring/prometheus-operator.secret.yml | 52 +- .../scripts/discover-home-assistant.sh | 63 + k8s/monitoring/scripts/discover-influxdb.sh | 98 + k8s/tt-rss/tt-rss-mariadb.secret.yml | 10 - k8s/tt-rss/tt-rss-phpmyadmin.yml | 13 - k8s/tt-rss/tt-rss.helm.secret.yml | 32 - k8s/tt-rss/tt-rss.secret.yaml | Bin 9653 -> 0 bytes k8s/velero/dropbox_sync.yml | 29 - k8s/velero/gdrive_sa.json.secret | Bin 0 -> 2414 bytes k8s/velero/gdrive_sync.yml | 39 + k8s/velero/gdrive_sync_manual_job.yml | 36 + k8s/velero/rclone.secret | Bin 427 -> 396 bytes k8s/velero/values_12.1.0_default.yaml | 828 ++++++++ k8s/velero/velero.secret.yaml | Bin 1329 -> 1330 bytes terraform/.terraform.lock.hcl | 83 +- terraform/gcp_backups.tf | 56 + terraform/terraform.tfstate | 1808 ++++++++++++++++- terraform/terraform.tfstate.backup | 1716 +++++++++++++++- terraform/versions.tf | 8 + 37 files changed, 5704 insertions(+), 252 deletions(-) create mode 100644 k8s/home-assistant/tmp_long_lived_token.secret.yml delete mode 100644 k8s/mailu/backup.secret.yml delete mode 100644 k8s/mailu/mailu.secret.yml delete mode 100644 k8s/mailu/restore.yaml create mode 100644 k8s/monitoring/AGENTS.md create mode 100644 k8s/monitoring/DASHBOARD_CREATION.md create mode 100644 k8s/monitoring/README.md create mode 100644 k8s/monitoring/dashboards/grafana-dashboard-energy-monitor.yaml create mode 100644 k8s/monitoring/dashboards/grafana-dashboard-heating-gas.yaml create mode 100644 k8s/monitoring/dashboards/grafana-dashboard-home-climate.yaml create mode 100644 k8s/monitoring/dashboards/grafana-dashboard-outdoor-weather.yaml create mode 100755 k8s/monitoring/scripts/discover-home-assistant.sh create mode 100755 k8s/monitoring/scripts/discover-influxdb.sh delete mode 100644 k8s/tt-rss/tt-rss-mariadb.secret.yml delete mode 100644 k8s/tt-rss/tt-rss-phpmyadmin.yml delete mode 100644 k8s/tt-rss/tt-rss.helm.secret.yml delete mode 100644 k8s/tt-rss/tt-rss.secret.yaml delete mode 100644 k8s/velero/dropbox_sync.yml create mode 100755 k8s/velero/gdrive_sa.json.secret create mode 100644 k8s/velero/gdrive_sync.yml create mode 100644 k8s/velero/gdrive_sync_manual_job.yml create mode 100644 k8s/velero/values_12.1.0_default.yaml create mode 100644 terraform/gcp_backups.tf diff --git a/bootstrap/inventory/prod/group_vars/k8s_cluster/k8s-cluster.yml b/bootstrap/inventory/prod/group_vars/k8s_cluster/k8s-cluster.yml index e116d17..90d5bfc 100644 --- a/bootstrap/inventory/prod/group_vars/k8s_cluster/k8s-cluster.yml +++ b/bootstrap/inventory/prod/group_vars/k8s_cluster/k8s-cluster.yml @@ -122,6 +122,9 @@ kube_proxy_mode: iptables # Moritz: changed to iptables for tegra #kube_proxy_mode: ipvs +# Expose kube-proxy metrics on all interfaces so Prometheus can scrape port 10249 +kube_proxy_metrics_bind_address: "0.0.0.0:10249" + # configure arp_ignore and arp_announce to avoid answering ARP queries from kube-ipvs0 interface # must be set to true for MetalLB to work kube_proxy_strict_arp: false diff --git a/k8s/README.md b/k8s/README.md index 0ba2a42..c14e2e6 100644 --- a/k8s/README.md +++ b/k8s/README.md @@ -121,19 +121,24 @@ Backup tool. See chart [README](https://github.com/vmware-tanzu/helm-charts/blob ```sh helm repo add vmware-tanzu https://vmware-tanzu.github.io/helm-charts helm repo update -helm upgrade --install --create-namespace --namespace velero -f ./velero/velero.secret.yaml velero vmware-tanzu/velero --version 5.3.0 -kubectl create secret generic rclone-config --from-file=./velero/rclone.secret -kubectl apply -f velero/dropbox_sync.yml -# # +# Apply CRDs first (required on upgrades across major versions) +helm show crds vmware-tanzu/velero --version 12.1.0 | kubectl apply -f - +helm upgrade --install --create-namespace --namespace velero -f ./velero/velero.secret.yaml velero vmware-tanzu/velero --version 12.1.0 +# Secrets for rclone (minio + gdrive) +kubectl create secret generic rclone-config --from-file=./velero/rclone.secret -n velero +kubectl create secret generic gdrive-sa-key --from-file=gdrive_sa.json.secret=./velero/gdrive_sa.json.secret -n velero +# Deploy GDrive sync cronjob +kubectl apply -f velero/gdrive_sync.yml +# Teardown helm delete velero -n velero kubectl delete ns velero ``` -A manual backup may be created executing the following command. **Note: Keep backuped namespaces in sync with config from helm chart!!!** +A manual backup may be created executing the following command. **Note: Keep backed-up namespaces in sync with config from helm chart!!!** ```sh DATE=$( date +%Y%m%d ) -velero backup create $DATE --include-namespaces datalab,development,nextcloud,tt-rss,mailu --wait +velero backup create $DATE --include-namespaces coder,home-assistant,n8n,influxdb --wait ``` ## coder diff --git a/k8s/cert-manager/production-issuer.yaml b/k8s/cert-manager/production-issuer.yaml index b9acefc..025f6c6 100644 --- a/k8s/cert-manager/production-issuer.yaml +++ b/k8s/cert-manager/production-issuer.yaml @@ -18,4 +18,4 @@ spec: solvers: - http01: ingress: - class: nginx + ingressClassName: traefik diff --git a/k8s/cert-manager/staging-issuer.yaml b/k8s/cert-manager/staging-issuer.yaml index 51f74ed..0367166 100644 --- a/k8s/cert-manager/staging-issuer.yaml +++ b/k8s/cert-manager/staging-issuer.yaml @@ -18,4 +18,4 @@ spec: solvers: - http01: ingress: - class: nginx + ingressClassName: traefik diff --git a/k8s/home-assistant/home-assistant.secret.yaml b/k8s/home-assistant/home-assistant.secret.yaml index 8bdb03e2c61f5fdbead1a6790cfc24c075437479..55b86d7fff50f0806bce82e3c30977134aa65daa 100644 GIT binary patch literal 7352 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 literal 8122 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 literal 622 zcmV-!0+IayM@dveQdv+`02+gLwad--<#7Ct^q&TvGz&?9)BY*|DGg^MX#{LYQ~SPY zSx<%h-dcqJVE7U}J4g^E5p}5n)f*0n^o$1oa@Q1fZ8?2xO4`WkT1;XHi7zapK&R|O zLCuXGH>Ec!x|UdKoL9g-ZqH>`tOjt+UYop-OH!d`!a0uKzv(@vaPkhBLw!b%b!O^X zaWkCMFme_bQ@?5oirV|K7zK1kCZdadYJVxEuxgN*0=uyH8A53sXt&TPLK$~l^mxqq zOY$6wUK$M1?nMIy6V5qhH9F`!_q_BHx1B0JNI9{Q`IAOTM|buI>p`j!EYD2QgSU-rD&z z3~>k^)d?*iOwanXy5#%-kM`ZMXw2}n*tp zkDo3XQs2CU+cziwm;E^QNVEL%V~)$F9xFY~pFteF!UA#ypXgbGo3~pe;hP#1MU`+1 z2HrM1*;e|o>S>nH_UA3%5~H{`I^BNRUq?-K{GkeIJl@MByTTLu+A{=vu>894h6u=> zn`8UkvzKMm$GtDONk~6Ot{ENg>;- I9?04IcDd3rLjV8( diff --git a/k8s/monitoring/AGENTS.md b/k8s/monitoring/AGENTS.md new file mode 100644 index 0000000..c3c4983 --- /dev/null +++ b/k8s/monitoring/AGENTS.md @@ -0,0 +1,164 @@ +# AGENTS.md — Monitoring Folder Guide for AI Agents + +## Overview + +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. + +## Folder Structure + +``` +monitoring/ +├── AGENTS.md ← You are here +├── README.md ← Human-oriented deployment guide +├── DASHBOARD_CREATION.md ← How to create/update dashboards (read this!) +├── prometheus-operator.secret.yml ← Helm values for kube-prometheus-stack +├── alertmanagerconfig.secret.yaml ← Alertmanager config (Telegram alerts) +├── servicemonitor.secret.yml ← ServiceMonitor examples (commented out) +├── 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 +└── scripts/ ← Discovery & helper scripts + ├── discover-influxdb.sh ← Query InfluxDB schema + └── discover-home-assistant.sh ← Query Home Assistant entities +``` + +## Key Infrastructure Context + +### Namespaces & Services + +| Namespace | Service | Purpose | +|-----------|---------|---------| +| `monitoring` | kube-prometheus-stack | Prometheus, Alertmanager, Grafana | +| `influxdb` | InfluxDB2 | Time-series storage for Home Assistant sensor data | +| `home-assistant` | Home Assistant | Smart home hub, writes sensor data to InfluxDB | + +### Data Flow + +``` +Home Assistant sensors → InfluxDB (bucket: default, org: influxdata) + ↓ + Grafana (Flux queries via datasource) +``` + +### Grafana Datasources + +| Name | UID | Type | URL | +|------|-----|------|-----| +| InfluxDB Home Assistant | `P2AB959DC95E5519F` | influxdb (Flux) | `http://influxdb-influxdb2.influxdb.svc.cluster.local:80` | +| InfluxDB Home Assistant InfluxQL | `P86455D1023ECC470` | influxdb (InfluxQL) | same | +| Prometheus | `prometheus` | prometheus | internal | + +> **⚠️ IMPORTANT**: The datasource UIDs above are the **current** values. If the Grafana deployment is recreated, these UIDs will change. Always re-discover UIDs via `kubectl -n monitoring exec deploy/prometheus-operator-grafana -c grafana -- curl -s -u "admin:" http://localhost:3000/api/datasources` before creating new dashboards. + +### Grafana Admin Credentials + +- URL: `https://grafana.haumdaucher.de` +- User: `admin` +- Password: stored in `prometheus-operator.secret.yml` under `grafana.adminPassword` + +### Home Assistant API + +- URL: `https://hass.moritzgraf.de` +- Token: stored in `../home-assistant/tmp_long_lived_token.secret.yml` + +### InfluxDB API + +- Internal URL: `http://influxdb-influxdb2.influxdb.svc.cluster.local:80` +- Org: `influxdata` +- Bucket: `default` +- Token: stored in `prometheus-operator.secret.yml` under `grafana.additionalDataSources[0].secureJsonData.token` + +## 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. + +### 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. + +### ConfigMap Template + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard- + namespace: monitoring + labels: + grafana_dashboard: "1" + annotations: + grafana_dashboard_folder: "🏠 Home" +data: + .json: | + { + "uid": "", + "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. + +## 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 \ No newline at end of file diff --git a/k8s/monitoring/DASHBOARD_CREATION.md b/k8s/monitoring/DASHBOARD_CREATION.md new file mode 100644 index 0000000..4377b33 --- /dev/null +++ b/k8s/monitoring/DASHBOARD_CREATION.md @@ -0,0 +1,326 @@ +# DASHBOARD_CREATION.md — How to Create & Update Grafana Dashboards + +> **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. + +--- + +## Step-by-Step Dashboard Creation Process + +### Step 1: Discovery — Find Available Metrics + +Before creating any dashboard, discover what data actually exists: + +#### 1a. Query Home Assistant for Entity Metadata + +```bash +# Get all entities with their friendly names, units, and device classes +curl -s -H "Authorization: Bearer " \ + 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)' +``` + +Or use the helper script: `./scripts/discover-home-assistant.sh` + +#### 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 " \ + -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 " \ + -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 " \ + -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: + +```bash +kubectl -n monitoring exec deploy/prometheus-operator-grafana -c grafana -- \ + curl -s -u "admin:" \ + 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. + +```bash +# Find all Wandthermostat temperature sensors +curl -s -H "Authorization: Bearer " \ + https://hass.moritzgraf.de/api/states | \ + jq '[.[] | select(.attributes.unit_of_measurement == "°C" and + (.entity_id | test("wandthermostat|thermostat"; "i")))]' +``` + +### Step 4: Create the Dashboard ConfigMap YAML + +Create a new file in `dashboards/` following this template: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard- + namespace: monitoring + labels: + grafana_dashboard: "1" + annotations: + grafana_dashboard_folder: "🏠 Home" +data: + .json: | + { + "uid": "", + "title": "", + "tags": ["home", "", "auto-generated"], + "timezone": "browser", + "schemaVersion": 39, + "version": 1, + "refresh": "5m", + "editable": false, + "graphTooltip": 1, + "time": { "from": "now-24h", "to": "now" }, + "panels": [ ... ] + } +``` + +#### Critical JSON Structure Rules + +**✅ CORRECT** (top-level properties, no wrapper): +```json +{ + "uid": "my-dashboard", + "title": "My Dashboard", + "panels": [...] +} +``` + +**❌ WRONG** (wrapped in "dashboard" key — causes "Dashboard title cannot be empty"): +```json +{ + "dashboard": { + "uid": "my-dashboard", + "title": "My Dashboard", + "panels": [...] + } +} +``` + +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 + +**✅ CORRECT** (by actual UID): +```json +"datasource": { "type": "influxdb", "uid": "P2AB959DC95E5519F" } +``` + +**❌ WRONG** (by name — won't resolve): +```json +"datasource": { "type": "influxdb", "uid": "InfluxDB Home Assistant" } +``` + +#### Flux Query Template + +**Exact match on entity_id:** +```flux +from(bucket: "default") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r["_measurement"] == "" and r["_field"] == "value") + |> filter(fn: (r) => r.entity_id == "") + |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) + |> 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"] == "" 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): +```flux +from(bucket: "default") + |> range(start: -15m) + |> filter(fn: (r) => r["_measurement"] == "" and r["_field"] == "value") + |> filter(fn: (r) => r.entity_id == "") + |> last() + |> 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-.yaml +``` + +### 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-.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- +``` + +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 | diff --git a/k8s/monitoring/README.md b/k8s/monitoring/README.md new file mode 100644 index 0000000..c46e1ec --- /dev/null +++ b/k8s/monitoring/README.md @@ -0,0 +1,90 @@ +# Grafana Dashboards as Code + +This directory contains Grafana dashboards managed as Kubernetes ConfigMaps. Dashboards are automatically loaded into Grafana via the dashboard sidecar. + +## Architecture + +``` +dashboards/*.yaml (ConfigMap with embedded JSON) + │ + │ kubectl apply + ▼ +Kubernetes ConfigMap (label: grafana_dashboard=1) + │ + │ Sidecar watches for ConfigMaps + ▼ +Grafana Dashboard (read-only, GitOps managed) +``` + +## Quick Deploy + +```bash +# Apply all dashboards +kubectl apply -f dashboards/ + +# Apply a single dashboard +kubectl apply -f dashboards/grafana-dashboard-home-climate.yaml +``` + +## Verify + +```bash +# Check ConfigMaps are created and labeled correctly +kubectl get configmaps -n monitoring -l grafana_dashboard=1 + +# Check sidecar logs to confirm dashboards were loaded +kubectl logs -n monitoring deployment/kube-prometheus-stack-grafana -c grafana-sc-dashboards --tail=50 + +# List dashboards via Grafana API +kubectl -n monitoring exec deploy/kube-prometheus-stack-grafana -- \ + curl -s -u admin:$(kubectl -n monitoring get secret kube-prometheus-stack-grafana -o jsonpath='{.data.admin-password}' | base64 -d) \ + http://localhost:3000/api/search?type=dash-db | jq '.[].title' +``` + +## Discovery + +Before generating new dashboards, discover what metrics are available: + +```bash +# Discover InfluxDB measurements, fields, and tags +./scripts/discover-influxdb.sh + +# Discover Home Assistant entities and their metadata +./scripts/discover-home-assistant.sh +``` + +## Dashboard Structure + +Each dashboard is a single YAML file containing a Kubernetes ConfigMap with the dashboard JSON embedded: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard- + namespace: monitoring + labels: + grafana_dashboard: "1" + annotations: + grafana_dashboard_folder: "" +data: + .json: | + { + "dashboard": { ... } + } +``` + +## Adding a New Dashboard + +1. Run discovery scripts to understand available metrics +2. Create a new ConfigMap YAML in `dashboards/` +3. Apply: `kubectl apply -f dashboards/grafana-dashboard-.yaml` +4. Verify the dashboard appears in Grafana at https://grafana.haumdaucher.de + +## Removing a Dashboard + +```bash +kubectl delete configmap -n monitoring grafana-dashboard- +``` + +The sidecar will automatically remove the dashboard from Grafana within ~60 seconds. \ No newline at end of file diff --git a/k8s/monitoring/alertmanagerconfig.secret.yaml b/k8s/monitoring/alertmanagerconfig.secret.yaml index afd58905ca6ca87fe2a3023ca3e76cce6eb4977d..df713f48fed0778f33a89561cae18d92f4751df4 100644 GIT binary patch literal 1779 zcmVRgv4z#MjEGQX_n|#uPTR=2$rb3?9? zI;mDN8*DC!c^eTiG9!gCx!$=++b5A3T*LfmY}pMW z9c0B$c9LJ-#h%99M%KHGaq6L}bJ?$>bW!F*L6Sa-E)iF9wl+Pz0E&H;e|rUK`N4`8 zER`l{8jVE)L^^8Y!c+(a1Lix7nBYh*A{+M#Q8RY9-B@gVfoiFP1e@DEBq&~L~C9zIF{>ax3hpqJ+^2EMb zP;WjmJXCR~jwHR!TI|!OoKhXw0Cu7%a$FNZZxQTFAR=U<7GAL6Ura%nD70Vtj4-KI z33Rhu05Ho1g&WXyya$vthAt1Dbk_qimd!A!x+k}HqL{x;c{#;@KR7N4`oLBvBS-KR zNVdr>({}qunir>v3gn~8bixizLgssnR2D9^xL0S7ta;=8?uo0rGTVVc~3ygZR4EHxPMf$xA@?lhgkEBHuelHtFGZk^i1n*^j3jFbt zB18FS`{c`?J>hJ*bL&0c*plzpjJadn`o)sce0o)BCcMI&Eh~2}t4JYfBHs)mIg6iLynt z?gx-4hv{aM_@B1>glyXz1Z+E4&p756DIIML%W^GCyUG-aV*Hd^$@#s*M*V!rMJOST zn=RB%7v)=Yqu=UZ{gPDdRDU{Op~qE{OWSI052B3+{M4eHDX0ku`4o=@U=SuZeBO8b zJzyJqsfYeBIS4)G(fDyt3$>qvSw`S@Q<<;{FLYttp@aBr6IC-*?Hj5Pn#9 zE*}KBL{0JKZUVy1ru$v`K{|gqh$5JEIyzbjH2%&}}*u%n3M2lGfKHz&Q7r#J46T zl`b~pPvF!5f=csIb&s2gqMvKNr_U3~NGt}<=LV)AS}N_^}K?0(F|3u@uoR4gmUVI1|eQwO4SvYQx0K_LTHX z{(77z*R53E?4mMjt#{)a(;EPaC}b8Mn?m#co8!)h&O?FWs~TH>7UvzqoH{Wuokee2(tbhl>S(C86GX8E3Fb;WLlc*-?vyzRLz0HA5Sbl2Mkv%^OulaAu2V`xsaP$l( zjKmw2LHm_4%1SxZ(hecmlJ(i!r6N|#ix=mi*c_{EMGE0wBH`6vgTJ`EvK=uPmj1-n zUwK}M7GH+Ag8w_dl3c6HCzj$S>D`$dtX5=tElMxMaG$CX`E{WH7k{~*Ms0-Ro9VwL zUlO!(FO%Y3>QnG^A}eWrH?`@cHAH&0gV9vRwqIsNj4%yhcZ`!8Y$>u#7W77SA{FlW zmayoblqt1R`MPx^ym9!PEU;0A*$2~5qADWAzvQuVYJxd2aHBmfDlxq-#M`OdCSebr znKltNFZ*El0ggSY%~klP%0JxY-S{u9ap}I{uOmJPcd$~v zeTs)PsCVK+f~E{xKYt)c;|Z~kDehwJs{Ub;x5kO`rQXt`25=I5F(r<{_~xq$WINbu z2TYD#N`jmxHimz(LJHGR9@>N=eOe=$S?7xi22_~u#&GA1CA}nh4c+^2XqwR z!^b8kF;*6A7YoE(Zd`bMBoUo#SGe76O1OjR=DbpOC1#U_+o=t}pOdZ7*F%A+vDXRl zRGn#3eaVu(x{Fas3{<;jL(RQ-?*%uaeXH&%lxQD{{6A0fa??oXZ@)FVPX-}KHwJvC Pm?Ro@h^}ABYR# diff --git a/k8s/monitoring/dashboards/grafana-dashboard-energy-monitor.yaml b/k8s/monitoring/dashboards/grafana-dashboard-energy-monitor.yaml new file mode 100644 index 0000000..8d13aaf --- /dev/null +++ b/k8s/monitoring/dashboards/grafana-dashboard-energy-monitor.yaml @@ -0,0 +1,92 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard-energy-monitor + namespace: monitoring + labels: + grafana_dashboard: '1' + annotations: + grafana_dashboard_folder: 🏠 Home +data: + energy-monitor.json: '{"uid": "energy-monitor", "title": "⚡ Energy Monitor", "tags": ["home", "energy", "solar", "power", "auto-generated"], "timezone": "browser", "schemaVersion": 39, "version": 1, "refresh": + "5m", "editable": false, "graphTooltip": 1, "time": {"from": "now-24h", "to": "now"}, "panels": [{"title": "⚡ Live Power", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}, "collapsed": false}, + {"title": "☀️ Solar", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 0, "y": 1}, "fieldConfig": {"defaults": {"unit": "watt", "decimals": + 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#555", "value": null}, {"color": "#FFD700", "value": 100}, {"color": "#73BF69", "value": 500}, {"color": "#37872D", "value": 2000}]}, "color": + {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: + \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"W\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_systemleistung\")\n |> + aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "🏠 Self-Use", "type": "stat", "datasource": {"type": "influxdb", + "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 4, "y": 1}, "fieldConfig": {"defaults": {"unit": "watt", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#555", "value": + null}, {"color": "#73BF69", "value": 100}, {"color": "#FFD700", "value": 1000}, {"color": "#FF7383", "value": 3000}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": + "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> + filter(fn: (r) => r[\"_measurement\"] == \"W\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_eigene_leistung\")\n |> aggregateWindow(every: v.windowPeriod, + fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "🔌 Grid Import", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, + "gridPos": {"h": 4, "w": 4, "x": 8, "y": 1}, "fieldConfig": {"defaults": {"unit": "watt", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#73BF69", "value": null}, {"color": "#FFD700", + "value": 500}, {"color": "#FF7383", "value": 2000}, {"color": "#C4162A", "value": 5000}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": + "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) + => r[\"_measurement\"] == \"W\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_bezugsleistung\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: + false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "↗️ Grid Export", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, + "w": 4, "x": 12, "y": 1}, "fieldConfig": {"defaults": {"unit": "watt", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#555", "value": null}, {"color": "#5794F2", "value": 100}, + {"color": "#1F60C4", "value": 500}, {"color": "#3274D9", "value": 2000}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": + {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] + == \"W\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_einspeiseleistung\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> + yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "🔋 Battery SoC", "type": "gauge", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": + 16, "y": 1}, "fieldConfig": {"defaults": {"unit": "percent", "decimals": 0, "min": 0, "max": 100, "thresholds": {"mode": "absolute", "steps": [{"color": "#C4162A", "value": null}, {"color": "#FF7383", + "value": 20}, {"color": "#FFD700", "value": 40}, {"color": "#73BF69", "value": 60}]}, "color": {"mode": "thresholds"}}}, "options": {"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}, "orientation": + "auto", "showThresholdLabels": true, "showThresholdMarkers": true}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: -15m)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and + r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"ladeaufstellung_soc\")\n |> last()\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "🏡 Total House", "type": + "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 20, "y": 1}, "fieldConfig": {"defaults": {"unit": "watt", "decimals": 0, "thresholds": {"mode": + "absolute", "steps": [{"color": "#555", "value": null}, {"color": "#73BF69", "value": 200}, {"color": "#FFD700", "value": 1000}, {"color": "#FF7383", "value": 3000}, {"color": "#C4162A", "value": 5000}]}, + "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": + "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"W\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id + == \"home_strommesserat_currentpowerconsumption\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": + "📈 Power Flow", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 5}, "collapsed": false}, {"title": "Power Timeline", "type": "timeseries", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, + "gridPos": {"h": 10, "w": 24, "x": 0, "y": 6}, "fieldConfig": {"defaults": {"unit": "watt", "custom": {"lineWidth": 2, "fillOpacity": 5, "spanNulls": true, "showPoints": "never"}}, "overrides": [{"matcher": + {"id": "byName", "options": "Solar"}, "properties": [{"id": "color", "value": {"fixedColor": "#FFD700", "mode": "fixed"}}, {"id": "custom.fillOpacity", "value": 15}]}, {"matcher": {"id": "byName", "options": + "Self-Use"}, "properties": [{"id": "color", "value": {"fixedColor": "#73BF69", "mode": "fixed"}}]}, {"matcher": {"id": "byName", "options": "Grid Import"}, "properties": [{"id": "color", "value": {"fixedColor": + "#FF7383", "mode": "fixed"}}]}, {"matcher": {"id": "byName", "options": "Grid Export"}, "properties": [{"id": "color", "value": {"fixedColor": "#5794F2", "mode": "fixed"}}, {"id": "custom.lineStyle", + "value": {"fill": "dash", "dash": [8, 4]}}]}, {"matcher": {"id": "byName", "options": "Local Load"}, "properties": [{"id": "color", "value": {"fixedColor": "#C4162A", "mode": "fixed"}}]}]}, "options": + {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["max", "mean", "lastNotNull"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, "targets": [{"refId": "SOLAR", "query": "from(bucket: + \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"W\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_systemleistung\")\n |> + aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\") |> set(key: \"source\", value: \"Solar\")", "resultFormat": "time_series"}, {"refId": "SELF", "query": + "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"W\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id + == \"garten_bzp3n7g1b0_eigene_leistung\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\") |> set(key: \"source\", value: \"Self-Use\")", "resultFormat": + "time_series"}, {"refId": "IMPORT", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"W\" and r[\"_field\"] + == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_bezugsleistung\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\") |> + set(key: \"source\", value: \"Grid Import\")", "resultFormat": "time_series"}, {"refId": "EXPORT", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> + filter(fn: (r) => r[\"_measurement\"] == \"W\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_einspeiseleistung\")\n |> aggregateWindow(every: v.windowPeriod, + fn: mean, createEmpty: false)\n |> yield(name: \"mean\") |> set(key: \"source\", value: \"Grid Export\")", "resultFormat": "time_series"}, {"refId": "LOAD", "query": "from(bucket: \"default\")\n |> + range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"W\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_lokale_lastleistung\")\n |> + aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\") |> set(key: \"source\", value: \"Local Load\")", "resultFormat": "time_series"}]}, {"title": "📊 Energy + Summary (Today)", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 16}, "collapsed": false}, {"title": "☀️ Solar Produced", "type": "bargauge", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, + "gridPos": {"h": 4, "w": 6, "x": 0, "y": 17}, "fieldConfig": {"defaults": {"unit": "kwatth", "decimals": 1, "min": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#555", "value": null}, {"color": + "#FFD700", "value": 5}, {"color": "#73BF69", "value": 15}, {"color": "#37872D", "value": 30}]}, "color": {"mode": "thresholds"}}}, "options": {"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}, + "orientation": "horizontal", "displayMode": "basic", "showUnfilled": true}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: -15m)\n |> filter(fn: (r) => r[\"_measurement\"] == \"kWh\" + and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_systemerzeugung_heute\")\n |> last()\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": + "🏠 Self-Consumed", "type": "bargauge", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 6, "x": 6, "y": 17}, "fieldConfig": {"defaults": {"unit": "kwatth", "decimals": + 1, "min": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#555", "value": null}, {"color": "#73BF69", "value": 5}, {"color": "#FFD700", "value": 15}, {"color": "#FF7383", "value": 30}]}, + "color": {"mode": "thresholds"}}}, "options": {"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}, "orientation": "horizontal", "displayMode": "basic", "showUnfilled": true}, "targets": [{"query": + "from(bucket: \"default\")\n |> range(start: -15m)\n |> filter(fn: (r) => r[\"_measurement\"] == \"kWh\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_eigenverbrauch_heute\")\n |> + last()\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "🔌 Grid Imported", "type": "bargauge", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": + 4, "w": 6, "x": 12, "y": 17}, "fieldConfig": {"defaults": {"unit": "kwatth", "decimals": 1, "min": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#73BF69", "value": null}, {"color": "#FFD700", + "value": 2}, {"color": "#FF7383", "value": 10}, {"color": "#C4162A", "value": 20}]}, "color": {"mode": "thresholds"}}}, "options": {"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}, "orientation": + "horizontal", "displayMode": "basic", "showUnfilled": true}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: -15m)\n |> filter(fn: (r) => r[\"_measurement\"] == \"kWh\" and r[\"_field\"] + == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_netzbezug_heute\")\n |> last()\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "↗️ Grid Exported", + "type": "bargauge", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 6, "x": 18, "y": 17}, "fieldConfig": {"defaults": {"unit": "kwatth", "decimals": 1, "min": + 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#555", "value": null}, {"color": "#5794F2", "value": 2}, {"color": "#1F60C4", "value": 10}, {"color": "#3274D9", "value": 20}]}, "color": {"mode": + "thresholds"}}}, "options": {"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}, "orientation": "horizontal", "displayMode": "basic", "showUnfilled": true}, "targets": [{"query": "from(bucket: + \"default\")\n |> range(start: -15m)\n |> filter(fn: (r) => r[\"_measurement\"] == \"kWh\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"export_ins_netz_heute\")\n |> last()\n |> + yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "📈 Energy Trend", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 21}, "collapsed": false}, {"title": "Daily Energy", "type": + "timeseries", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 10, "w": 24, "x": 0, "y": 22}, "fieldConfig": {"defaults": {"unit": "kwatth", "custom": {"lineWidth": 2, + "fillOpacity": 5, "spanNulls": true, "showPoints": "never"}}, "overrides": [{"matcher": {"id": "byName", "options": "Solar"}, "properties": [{"id": "color", "value": {"fixedColor": "#FFD700", "mode": + "fixed"}}]}, {"matcher": {"id": "byName", "options": "Self-Use"}, "properties": [{"id": "color", "value": {"fixedColor": "#73BF69", "mode": "fixed"}}]}, {"matcher": {"id": "byName", "options": "Grid + Import"}, "properties": [{"id": "color", "value": {"fixedColor": "#FF7383", "mode": "fixed"}}]}, {"matcher": {"id": "byName", "options": "Grid Export"}, "properties": [{"id": "color", "value": {"fixedColor": + "#5794F2", "mode": "fixed"}}]}]}, "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["sum", "mean"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, "targets": [{"refId": + "SOLAR", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"kWh\" and r[\"_field\"] == \"value\")\n |> + filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_systemerzeugung_heute\")\n |> aggregateWindow(every: 1d, fn: mean, createEmpty: false)\n |> yield(name: \"mean\") |> set(key: \"type\", value: + \"Solar\")", "resultFormat": "time_series"}, {"refId": "SELF", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] + == \"kWh\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_eigenverbrauch_heute\")\n |> aggregateWindow(every: 1d, fn: mean, createEmpty: false)\n |> yield(name: + \"mean\") |> set(key: \"type\", value: \"Self-Use\")", "resultFormat": "time_series"}, {"refId": "IMPORT", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> + filter(fn: (r) => r[\"_measurement\"] == \"kWh\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"garten_bzp3n7g1b0_netzbezug_heute\")\n |> aggregateWindow(every: 1d, fn: mean, + createEmpty: false)\n |> yield(name: \"mean\") |> set(key: \"type\", value: \"Grid Import\")", "resultFormat": "time_series"}, {"refId": "EXPORT", "query": "from(bucket: \"default\")\n |> range(start: + v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"kWh\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"export_ins_netz_heute\")\n |> + aggregateWindow(every: 1d, fn: mean, createEmpty: false)\n |> yield(name: \"mean\") |> set(key: \"type\", value: \"Grid Export\")", "resultFormat": "time_series"}]}]}' diff --git a/k8s/monitoring/dashboards/grafana-dashboard-heating-gas.yaml b/k8s/monitoring/dashboards/grafana-dashboard-heating-gas.yaml new file mode 100644 index 0000000..4ce7a9f --- /dev/null +++ b/k8s/monitoring/dashboards/grafana-dashboard-heating-gas.yaml @@ -0,0 +1,62 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard-heating-gas + namespace: monitoring + labels: + grafana_dashboard: '1' + annotations: + grafana_dashboard_folder: 🏠 Home +data: + heating-gas.json: '{"uid": "heating-gas", "title": "🔥 Heating & Gas", "tags": ["home", "heating", "gas", "auto-generated"], "timezone": "browser", "schemaVersion": 39, "version": 1, "refresh": "5m", "editable": + false, "graphTooltip": 1, "time": {"from": "now-24h", "to": "now"}, "panels": [{"title": "⛽ Gas Overview", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}, "collapsed": false}, {"title": + "Current Gas Flow", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 6, "x": 0, "y": 1}, "fieldConfig": {"defaults": {"unit": "cms", "decimals": + 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#73BF69", "value": null}, {"color": "#FFD700", "value": 0.5}, {"color": "#FF7383", "value": 1.5}, {"color": "#C4162A", "value": 3}]}, "color": + {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: + \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"m³/h\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == + \"home_gaszahler_currentgasflow\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "Total Gas Volume", + "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 6, "x": 6, "y": 1}, "fieldConfig": {"defaults": {"unit": "cms", "decimals": 0, "thresholds": + {"mode": "absolute", "steps": [{"color": "#73BF69", "value": null}, {"color": "#FFD700", "value": 500}, {"color": "#FF7383", "value": 1000}, {"color": "#C4162A", "value": 2000}]}, "color": {"mode": + "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> + range(start: -15m)\n |> filter(fn: (r) => r[\"_measurement\"] == \"m³\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_gaszahler_gasvolume\")\n |> last()\n |> yield(name: + \"mean\")", "resultFormat": "time_series"}]}, {"title": "Gas Cost (EUR)", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 6, "x": 12, "y": 1}, + "fieldConfig": {"defaults": {"unit": "currencyEUR", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#73BF69", "value": null}, {"color": "#FFD700", "value": 200}, {"color": "#FF7383", + "value": 500}, {"color": "#C4162A", "value": 1000}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, + "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: -15m)\n |> filter(fn: (r) => r[\"_measurement\"] == \"EUR\" and r[\"_field\"] == \"value\")\n |> filter(fn: + (r) => r.entity_id == \"home_gaszahler_gasvolume_cost\")\n |> last()\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "Electricity Cost (EUR)", "type": "stat", "datasource": + {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 6, "x": 18, "y": 1}, "fieldConfig": {"defaults": {"unit": "currencyEUR", "decimals": 0, "thresholds": {"mode": "absolute", + "steps": [{"color": "#73BF69", "value": null}, {"color": "#FFD700", "value": 100}, {"color": "#FF7383", "value": 300}, {"color": "#C4162A", "value": 500}]}, "color": {"mode": "thresholds"}}}, "options": + {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: + -15m)\n |> filter(fn: (r) => r[\"_measurement\"] == \"EUR\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_strommesserat_energy_counter_usage_high_tariff_cost\")\n |> + last()\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "📈 Gas Consumption Trend", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 5}, "collapsed": false}, {"title": + "Gas Flow", "type": "timeseries", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 8, "w": 16, "x": 0, "y": 6}, "fieldConfig": {"defaults": {"unit": "cms", "custom": + {"lineWidth": 2, "fillOpacity": 5, "spanNulls": true, "showPoints": "never"}}, "overrides": [{"matcher": {"id": "byName", "options": "Gas Flow"}, "properties": [{"id": "color", "value": {"fixedColor": + "#FF7383", "mode": "fixed"}}]}]}, "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["max", "mean"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, "targets": [{"refId": + "FLOW", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"m³/h\" and r[\"_field\"] == \"value\")\n |> + filter(fn: (r) => r.entity_id == \"home_gaszahler_currentgasflow\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\") |> set(key: \"metric\", value: + \"Gas Flow\")", "resultFormat": "time_series"}]}, {"title": "Gas Volume", "type": "timeseries", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 8, "w": 8, "x": 16, "y": + 6}, "fieldConfig": {"defaults": {"unit": "cms", "custom": {"lineWidth": 2, "fillOpacity": 5, "spanNulls": true, "showPoints": "never"}}, "overrides": [{"matcher": {"id": "byName", "options": "Gas Volume"}, + "properties": [{"id": "color", "value": {"fixedColor": "#C4162A", "mode": "fixed"}}]}]}, "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "lastNotNull"]}, "tooltip": + {"mode": "multi", "sort": "desc"}}, "targets": [{"refId": "VOL", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] + == \"m³\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_gaszahler_gasvolume\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: + \"mean\") |> set(key: \"metric\", value: \"Gas Volume\")", "resultFormat": "time_series"}]}, {"title": "🏠 Heating Activity", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 14}, "collapsed": + false}, {"title": "Heating Valve Position", "type": "timeseries", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 10, "w": 24, "x": 0, "y": 15}, "fieldConfig": {"defaults": + {"unit": "percent", "custom": {"lineWidth": 2, "fillOpacity": 5, "spanNulls": true, "showPoints": "never"}}, "overrides": [{"matcher": {"id": "byName", "options": "Wohnzimmer"}, "properties": [{"id": + "color", "value": {"fixedColor": "#FF7383", "mode": "fixed"}}]}, {"matcher": {"id": "byName", "options": "Wohnzimmer #2"}, "properties": [{"id": "color", "value": {"fixedColor": "#FF9830", "mode": "fixed"}}]}, + {"matcher": {"id": "byName", "options": "Arbeitszimmer"}, "properties": [{"id": "color", "value": {"fixedColor": "#5794F2", "mode": "fixed"}}]}, {"matcher": {"id": "byName", "options": "Schlafzimmer"}, + "properties": [{"id": "color", "value": {"fixedColor": "#8F3BB8", "mode": "fixed"}}]}, {"matcher": {"id": "byName", "options": "Küche"}, "properties": [{"id": "color", "value": {"fixedColor": "#73BF69", + "mode": "fixed"}}]}, {"matcher": {"id": "byName", "options": "Bad"}, "properties": [{"id": "color", "value": {"fixedColor": "#1F60C4", "mode": "fixed"}}]}]}, "options": {"legend": {"displayMode": "table", + "placement": "bottom", "calcs": ["mean", "lastNotNull"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, "targets": [{"refId": "Wo", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, + stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_wohnzimmer_heating\")\n |> aggregateWindow(every: + v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")\n |> set(key: \"room\", value: \"Wohnzimmer\")", "resultFormat": "time_series"}, {"refId": "Wo", "query": "from(bucket: \"default\")\n |> + range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_wohnzimmer_2_heating\")\n |> + aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")\n |> set(key: \"room\", value: \"Wohnzimmer #2\")", "resultFormat": "time_series"}, {"refId": "Ar", + "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: + (r) => r.entity_id == \"home_arbeitszimmer_heating\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")\n |> set(key: \"room\", value: \"Arbeitszimmer\")", + "resultFormat": "time_series"}, {"refId": "Sc", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and + r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_schlafzimmer_og_heating\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")\n |> + set(key: \"room\", value: \"Schlafzimmer\")", "resultFormat": "time_series"}, {"refId": "Kü", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: + (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_kuche_vorne_heating\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: + false)\n |> yield(name: \"mean\")\n |> set(key: \"room\", value: \"Küche\")", "resultFormat": "time_series"}, {"refId": "Ba", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, + stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_badezimmer_thermostat_heating\")\n |> aggregateWindow(every: + v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")\n |> set(key: \"room\", value: \"Bad\")", "resultFormat": "time_series"}]}]}' diff --git a/k8s/monitoring/dashboards/grafana-dashboard-home-climate.yaml b/k8s/monitoring/dashboards/grafana-dashboard-home-climate.yaml new file mode 100644 index 0000000..71703c6 --- /dev/null +++ b/k8s/monitoring/dashboards/grafana-dashboard-home-climate.yaml @@ -0,0 +1,98 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard-home-climate + namespace: monitoring + labels: + grafana_dashboard: '1' + annotations: + grafana_dashboard_folder: 🏠 Home +data: + home-climate.json: '{"uid": "home-climate", "title": "🏠 Home Climate", "tags": ["home", "climate", "temperature", "humidity", "auto-generated"], "timezone": "browser", "schemaVersion": 39, "version": + 1, "refresh": "5m", "editable": false, "graphTooltip": 1, "time": {"from": "now-24h", "to": "now"}, "panels": [{"title": "🌡️ Current Temperatures", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, + "y": 0}, "collapsed": false}, {"title": "Wohnzimmer", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 0, "y": 1}, "fieldConfig": {"defaults": + {"unit": "celsius", "decimals": 1, "thresholds": {"mode": "absolute", "steps": [{"color": "blue", "value": null}, {"color": "green", "value": 18}, {"color": "orange", "value": 22}, {"color": "red", + "value": 26}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": + [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"°C\" and r[\"_field\"] == \"value\")\n |> filter(fn: + (r) => r.entity_id == \"home_wall_mounted_thermostat_pro_temperature\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, + {"title": "Wohnzimmer #2", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 4, "y": 1}, "fieldConfig": {"defaults": {"unit": "celsius", + "decimals": 1, "thresholds": {"mode": "absolute", "steps": [{"color": "blue", "value": null}, {"color": "green", "value": 18}, {"color": "orange", "value": 22}, {"color": "red", "value": 26}]}, "color": + {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: + \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"°C\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == + \"home_wohnzimmer_2_thermostat_temperature\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "Arbeitszimmer", + "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 8, "y": 1}, "fieldConfig": {"defaults": {"unit": "celsius", "decimals": 1, "thresholds": + {"mode": "absolute", "steps": [{"color": "blue", "value": null}, {"color": "green", "value": 18}, {"color": "orange", "value": 22}, {"color": "red", "value": 26}]}, "color": {"mode": "thresholds"}}}, + "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> + range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"°C\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_arbeitszimmer_thermostat_temperature\")\n |> + aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "Küche", "type": "stat", "datasource": {"type": "influxdb", + "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 12, "y": 1}, "fieldConfig": {"defaults": {"unit": "celsius", "decimals": 1, "thresholds": {"mode": "absolute", "steps": [{"color": "blue", + "value": null}, {"color": "green", "value": 18}, {"color": "orange", "value": 22}, {"color": "red", "value": 26}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", + "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> + filter(fn: (r) => r[\"_measurement\"] == \"°C\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_kuche_wandthermostat_temperature\")\n |> aggregateWindow(every: v.windowPeriod, + fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "Bad", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": + {"h": 4, "w": 4, "x": 16, "y": 1}, "fieldConfig": {"defaults": {"unit": "celsius", "decimals": 1, "thresholds": {"mode": "absolute", "steps": [{"color": "blue", "value": null}, {"color": "green", "value": + 18}, {"color": "orange", "value": 22}, {"color": "red", "value": 26}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": + {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] + == \"°C\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_wandthermostat_bad_temperature\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> + yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "📈 Temperature Trends", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 5}, "collapsed": false}, {"title": "All Rooms Temperature", + "type": "timeseries", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 10, "w": 16, "x": 0, "y": 6}, "fieldConfig": {"defaults": {"unit": "celsius", "custom": {"lineWidth": + 2, "fillOpacity": 5, "spanNulls": true, "showPoints": "never"}}}, "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "lastNotNull", "min", "max"]}, "tooltip": {"mode": + "multi", "sort": "desc"}}, "targets": [{"refId": "Wo", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == + \"°C\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_wall_mounted_thermostat_pro_temperature\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> + set(key: \"room\", value: \"Wohnzimmer\")\n |> yield(name: \"mean\")", "resultFormat": "time_series"}, {"refId": "Wo2", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: + v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"°C\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_wohnzimmer_2_thermostat_temperature\")\n |> aggregateWindow(every: + v.windowPeriod, fn: mean, createEmpty: false)\n |> set(key: \"room\", value: \"Wohnzimmer #2\")\n |> yield(name: \"mean\")", "resultFormat": "time_series"}, {"refId": "Ar", "query": "from(bucket: + \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"°C\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == + \"home_arbeitszimmer_thermostat_temperature\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> set(key: \"room\", value: \"Arbeitszimmer\")\n |> yield(name: \"mean\")", + "resultFormat": "time_series"}, {"refId": "Kü", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"°C\" + and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_kuche_wandthermostat_temperature\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> set(key: \"room\", value: \"Küche\")\n |> yield(name: + \"mean\")", "resultFormat": "time_series"}, {"refId": "Ba", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> + filter(fn: (r) => r[\"_measurement\"] == \"°C\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_wandthermostat_bad_temperature\")\n |> aggregateWindow(every: v.windowPeriod, + fn: mean, createEmpty: false)\n |> set(key: \"room\", value: \"Bad\")\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "Indoor vs Outdoor", "type": "timeseries", "datasource": + {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 10, "w": 8, "x": 16, "y": 6}, "fieldConfig": {"defaults": {"unit": "celsius", "custom": {"lineWidth": 2, "fillOpacity": 5, "spanNulls": + true, "showPoints": "never"}}, "overrides": [{"matcher": {"id": "byName", "options": "Outdoor"}, "properties": [{"id": "color", "value": {"fixedColor": "light-blue", "mode": "fixed"}}, {"id": "custom.lineStyle", + "value": {"fill": "dash", "dash": [10, 5]}}]}, {"matcher": {"id": "byName", "options": "Indoor Avg"}, "properties": [{"id": "color", "value": {"fixedColor": "orange", "mode": "fixed"}}]}]}, "options": + {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "lastNotNull"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, "targets": [{"refId": "OUT", "query": "from(bucket: \"default\")\n |> + range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"°C\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"regensburg_regensburg_temperatur\")\n |> + aggregateWindow(every: 1h, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")\n |> set(key: \"location\", value: \"Outdoor\")", "resultFormat": "time_series"}, {"refId": "IN", "query": "from(bucket: + \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"°C\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id =~ + /^home_/)\n |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")\n |> group()\n |> mean()\n |> set(key: \"location\", value: \"Indoor Avg\")", "resultFormat": + "time_series"}]}, {"title": "💧 Humidity", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 16}, "collapsed": false}, {"title": "Wohnzimmer", "type": "stat", "datasource": {"type": "influxdb", + "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 0, "y": 17}, "fieldConfig": {"defaults": {"unit": "percent", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "red", + "value": null}, {"color": "orange", "value": 30}, {"color": "green", "value": 40}, {"color": "orange", "value": 60}, {"color": "red", "value": 70}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": + "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, + stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_wall_mounted_thermostat_pro_humidity\")\n |> + aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "Arbeitszimmer", "type": "stat", "datasource": {"type": + "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 4, "y": 17}, "fieldConfig": {"defaults": {"unit": "percent", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": + "red", "value": null}, {"color": "orange", "value": 30}, {"color": "green", "value": 40}, {"color": "orange", "value": 60}, {"color": "red", "value": 70}]}, "color": {"mode": "thresholds"}}}, "options": + {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: + v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_arbeitszimmer_thermostat_humidity\")\n |> + aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "Wohnzimmer #2", "type": "stat", "datasource": {"type": + "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 8, "y": 17}, "fieldConfig": {"defaults": {"unit": "percent", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": + "red", "value": null}, {"color": "orange", "value": 30}, {"color": "green", "value": 40}, {"color": "orange", "value": 60}, {"color": "red", "value": 70}]}, "color": {"mode": "thresholds"}}}, "options": + {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: + v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_wohnzimmer_2_thermostat_humidity\")\n |> + aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "Bad", "type": "stat", "datasource": {"type": "influxdb", + "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 12, "y": 17}, "fieldConfig": {"defaults": {"unit": "percent", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "red", + "value": null}, {"color": "orange", "value": 30}, {"color": "green", "value": 40}, {"color": "orange", "value": 60}, {"color": "red", "value": 70}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": + "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, + stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_wandthermostat_bad_humidity\")\n |> aggregateWindow(every: + v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "Küche", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, + "gridPos": {"h": 4, "w": 4, "x": 16, "y": 17}, "fieldConfig": {"defaults": {"unit": "percent", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "red", "value": null}, {"color": + "orange", "value": 30}, {"color": "green", "value": 40}, {"color": "orange", "value": 60}, {"color": "red", "value": 70}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": + "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> + filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_kuche_wandthermostat_humidity\")\n |> aggregateWindow(every: v.windowPeriod, + fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "Humidity Trends", "type": "timeseries", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, + "gridPos": {"h": 8, "w": 24, "x": 0, "y": 20}, "fieldConfig": {"defaults": {"unit": "percent", "custom": {"lineWidth": 2, "fillOpacity": 5, "spanNulls": true, "showPoints": "never"}}}, "options": {"legend": + {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "lastNotNull"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, "targets": [{"refId": "Wo", "query": "from(bucket: \"default\")\n |> + range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_wall_mounted_thermostat_pro_humidity\")\n |> + aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> set(key: \"room\", value: \"Wohnzimmer\")\n |> yield(name: \"mean\")", "resultFormat": "time_series"}, {"refId": "Ar", "query": + "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id + == \"home_arbeitszimmer_thermostat_humidity\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> set(key: \"room\", value: \"Arbeitszimmer\")\n |> yield(name: \"mean\")", + "resultFormat": "time_series"}, {"refId": "Wo2", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and + r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_wohnzimmer_2_thermostat_humidity\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> set(key: \"room\", value: \"Wohnzimmer #2\")\n |> yield(name: + \"mean\")", "resultFormat": "time_series"}, {"refId": "Ba", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> + filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_wandthermostat_bad_humidity\")\n |> aggregateWindow(every: v.windowPeriod, + fn: mean, createEmpty: false)\n |> set(key: \"room\", value: \"Bad\")\n |> yield(name: \"mean\")", "resultFormat": "time_series"}, {"refId": "Kü", "query": "from(bucket: \"default\")\n |> range(start: + v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"home_kuche_wandthermostat_humidity\")\n |> + aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> set(key: \"room\", value: \"Küche\")\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}]}' diff --git a/k8s/monitoring/dashboards/grafana-dashboard-outdoor-weather.yaml b/k8s/monitoring/dashboards/grafana-dashboard-outdoor-weather.yaml new file mode 100644 index 0000000..1fa264f --- /dev/null +++ b/k8s/monitoring/dashboards/grafana-dashboard-outdoor-weather.yaml @@ -0,0 +1,75 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: grafana-dashboard-outdoor-weather + namespace: monitoring + labels: + grafana_dashboard: '1' + annotations: + grafana_dashboard_folder: 🏠 Home +data: + outdoor-weather.json: '{"uid": "outdoor-weather", "title": "🌡️ Outdoor & Weather", "tags": ["home", "weather", "outdoor", "auto-generated"], "timezone": "browser", "schemaVersion": 39, "version": 1, "refresh": + "5m", "editable": false, "graphTooltip": 1, "time": {"from": "now-24h", "to": "now"}, "panels": [{"title": "🌤️ Current Conditions", "type": "row", "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}, "collapsed": + false}, {"title": "🌡️ Temperature", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 0, "y": 1}, "fieldConfig": {"defaults": {"unit": + "celsius", "decimals": 1, "thresholds": {"mode": "absolute", "steps": [{"color": "#5794F2", "value": null}, {"color": "#73BF69", "value": 0}, {"color": "#FFD700", "value": 15}, {"color": "#FF7383", + "value": 25}, {"color": "#C4162A", "value": 30}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, + "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"°C\" and r[\"_field\"] + == \"value\")\n |> filter(fn: (r) => r.entity_id == \"regensburg_regensburg_temperatur\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": + "time_series"}]}, {"title": "💧 Humidity", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 4, "y": 1}, "fieldConfig": {"defaults": {"unit": + "percent", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#FF7383", "value": null}, {"color": "#FFD700", "value": 30}, {"color": "#73BF69", "value": 40}, {"color": "#FFD700", + "value": 70}, {"color": "#5794F2", "value": 90}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, + "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] + == \"value\")\n |> filter(fn: (r) => r.entity_id == \"regensburg_regensburg_luftfeuchtigkeit\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "resultFormat": "time_series"}]}, {"title": "🔵 Pressure", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 8, "y": 1}, "fieldConfig": + {"defaults": {"unit": "pressurehpa", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#FF7383", "value": null}, {"color": "#FFD700", "value": 990}, {"color": "#73BF69", "value": + 1010}, {"color": "#FFD700", "value": 1030}, {"color": "#FF7383", "value": 1040}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", + "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] + == \"hPa\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"regensburg_regensburg_druck\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: + \"mean\")", "resultFormat": "time_series"}]}, {"title": "💨 Wind", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 12, "y": 1}, "fieldConfig": + {"defaults": {"unit": "velocitykmh", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#73BF69", "value": null}, {"color": "#FFD700", "value": 20}, {"color": "#FF7383", "value": + 40}, {"color": "#C4162A", "value": 60}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": + ["lastNotNull"]}}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"km/h\" and r[\"_field\"] + == \"value\")\n |> filter(fn: (r) => r.entity_id == \"regensburg_regensburg_windgeschwindigkeit\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", + "resultFormat": "time_series"}]}, {"title": "🌧️ Rain", "type": "stat", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 16, "y": 1}, "fieldConfig": {"defaults": + {"unit": "mm/h", "decimals": 0, "thresholds": {"mode": "absolute", "steps": [{"color": "#73BF69", "value": null}, {"color": "#5794F2", "value": 0.1}, {"color": "#1F60C4", "value": 1}, {"color": "#3274D9", + "value": 5}]}, "color": {"mode": "thresholds"}}}, "options": {"graphMode": "area", "colorMode": "background", "textMode": "auto", "reduceOptions": {"values": false, "calcs": ["lastNotNull"]}}, "targets": + [{"query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"mm/h\" and r[\"_field\"] == \"value\")\n |> filter(fn: + (r) => r.entity_id == \"regensburg_regensburg_niederschlag\")\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, + {"title": "☁️ Cloud Cover", "type": "gauge", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 4, "w": 4, "x": 20, "y": 1}, "fieldConfig": {"defaults": {"unit": "percent", + "decimals": 0, "min": 0, "max": 100, "thresholds": {"mode": "absolute", "steps": [{"color": "#5794F2", "value": null}, {"color": "#73BF69", "value": 20}, {"color": "#FFD700", "value": 50}, {"color": + "#808080", "value": 80}]}, "color": {"mode": "thresholds"}}}, "options": {"reduceOptions": {"values": false, "calcs": ["lastNotNull"]}, "orientation": "auto", "showThresholdLabels": true, "showThresholdMarkers": + true}, "targets": [{"query": "from(bucket: \"default\")\n |> range(start: -15m)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id + == \"regensburg_regensburg_bewolkungsgrad\")\n |> last()\n |> yield(name: \"mean\")", "resultFormat": "time_series"}]}, {"title": "📈 Temperature & Humidity Trend", "type": "row", "gridPos": {"h": + 1, "w": 24, "x": 0, "y": 5}, "collapsed": false}, {"title": "Temperature", "type": "timeseries", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 8, "w": 12, "x": 0, + "y": 6}, "fieldConfig": {"defaults": {"unit": "celsius", "custom": {"lineWidth": 2, "fillOpacity": 5, "spanNulls": true, "showPoints": "never"}}, "overrides": [{"matcher": {"id": "byName", "options": + "Temperature"}, "properties": [{"id": "color", "value": {"fixedColor": "#FF7383", "mode": "fixed"}}]}]}, "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", + "mean"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, "targets": [{"refId": "T", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) + => r[\"_measurement\"] == \"°C\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"regensburg_regensburg_temperatur\")\n |> aggregateWindow(every: 30m, fn: mean, createEmpty: + false)\n |> yield(name: \"mean\") |> set(key: \"metric\", value: \"Temperature\")", "resultFormat": "time_series"}]}, {"title": "Humidity", "type": "timeseries", "datasource": {"type": "influxdb", + "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 8, "w": 12, "x": 12, "y": 6}, "fieldConfig": {"defaults": {"unit": "percent", "custom": {"lineWidth": 2, "fillOpacity": 5, "spanNulls": true, "showPoints": + "never"}}, "overrides": [{"matcher": {"id": "byName", "options": "Humidity"}, "properties": [{"id": "color", "value": {"fixedColor": "#5794F2", "mode": "fixed"}}]}]}, "options": {"legend": {"displayMode": + "table", "placement": "bottom", "calcs": ["min", "max", "mean"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, "targets": [{"refId": "H", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, + stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"regensburg_regensburg_luftfeuchtigkeit\")\n |> aggregateWindow(every: + 30m, fn: mean, createEmpty: false)\n |> yield(name: \"mean\") |> set(key: \"metric\", value: \"Humidity\")", "resultFormat": "time_series"}]}, {"title": "📊 Pressure & Wind", "type": "row", "gridPos": + {"h": 1, "w": 24, "x": 0, "y": 14}, "collapsed": false}, {"title": "Pressure", "type": "timeseries", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 8, "w": 12, "x": + 0, "y": 15}, "fieldConfig": {"defaults": {"unit": "pressurehpa", "custom": {"lineWidth": 2, "fillOpacity": 5, "spanNulls": true, "showPoints": "never"}}, "overrides": [{"matcher": {"id": "byName", "options": + "Pressure"}, "properties": [{"id": "color", "value": {"fixedColor": "#8F3BB8", "mode": "fixed"}}]}]}, "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["min", "max", "mean"]}, + "tooltip": {"mode": "multi", "sort": "desc"}}, "targets": [{"refId": "P", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] + == \"hPa\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"regensburg_regensburg_druck\")\n |> aggregateWindow(every: 30m, fn: mean, createEmpty: false)\n |> yield(name: \"mean\") |> + set(key: \"metric\", value: \"Pressure\")", "resultFormat": "time_series"}]}, {"title": "Wind Speed", "type": "timeseries", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": + {"h": 8, "w": 12, "x": 12, "y": 15}, "fieldConfig": {"defaults": {"unit": "velocitykmh", "custom": {"lineWidth": 2, "fillOpacity": 5, "spanNulls": true, "showPoints": "never"}}, "overrides": [{"matcher": + {"id": "byName", "options": "Wind"}, "properties": [{"id": "color", "value": {"fixedColor": "#5794F2", "mode": "fixed"}}]}]}, "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": + ["max", "mean"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, "targets": [{"refId": "W", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: + (r) => r[\"_measurement\"] == \"km/h\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"regensburg_regensburg_windgeschwindigkeit\")\n |> aggregateWindow(every: 30m, fn: mean, + createEmpty: false)\n |> yield(name: \"mean\") |> set(key: \"metric\", value: \"Wind\")", "resultFormat": "time_series"}]}, {"title": "🌧️ Precipitation & Cloud", "type": "row", "gridPos": {"h": 1, + "w": 24, "x": 0, "y": 23}, "collapsed": false}, {"title": "Rain", "type": "timeseries", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": 8, "w": 12, "x": 0, "y": 24}, + "fieldConfig": {"defaults": {"unit": "mm/h", "custom": {"lineWidth": 2, "fillOpacity": 5, "spanNulls": true, "showPoints": "never"}}, "overrides": [{"matcher": {"id": "byName", "options": "Rain"}, "properties": + [{"id": "color", "value": {"fixedColor": "#3274D9", "mode": "fixed"}}]}]}, "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "lastNotNull"]}, "tooltip": {"mode": + "multi", "sort": "desc"}}, "targets": [{"refId": "R", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_measurement\"] == \"mm/h\" + and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"regensburg_regensburg_niederschlag\")\n |> aggregateWindow(every: 30m, fn: mean, createEmpty: false)\n |> yield(name: \"mean\") |> + set(key: \"metric\", value: \"Rain\")", "resultFormat": "time_series"}]}, {"title": "Cloud Cover", "type": "timeseries", "datasource": {"type": "influxdb", "uid": "P2AB959DC95E5519F"}, "gridPos": {"h": + 8, "w": 12, "x": 12, "y": 24}, "fieldConfig": {"defaults": {"unit": "percent", "custom": {"lineWidth": 2, "fillOpacity": 5, "spanNulls": true, "showPoints": "never"}}, "overrides": [{"matcher": {"id": + "byName", "options": "Cloud Cover"}, "properties": [{"id": "color", "value": {"fixedColor": "#808080", "mode": "fixed"}}]}]}, "options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": + ["mean"]}, "tooltip": {"mode": "multi", "sort": "desc"}}, "targets": [{"refId": "C", "query": "from(bucket: \"default\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: + (r) => r[\"_measurement\"] == \"%\" and r[\"_field\"] == \"value\")\n |> filter(fn: (r) => r.entity_id == \"regensburg_regensburg_bewolkungsgrad\")\n |> aggregateWindow(every: 30m, fn: mean, createEmpty: + false)\n |> yield(name: \"mean\") |> set(key: \"metric\", value: \"Cloud Cover\")", "resultFormat": "time_series"}]}]}' diff --git a/k8s/monitoring/prometheus-operator.secret.yml b/k8s/monitoring/prometheus-operator.secret.yml index e5ac5d4..ddfe33b 100644 --- a/k8s/monitoring/prometheus-operator.secret.yml +++ b/k8s/monitoring/prometheus-operator.secret.yml @@ -7,12 +7,11 @@ kubeControllerManager: serviceMonitor: https: true insecureSkipVerify: true -# proxy not working, svc not exposed by port kubeProxy: service: targetPort: 10249 serviceMonitor: - https: true + https: false insecureSkipVerify: true ################## @@ -61,6 +60,7 @@ prometheus: storage: 50Gi ingress: enabled: true + ingressClassName: traefik hosts: - prometheus.haumdaucher.de tls: @@ -68,7 +68,7 @@ prometheus: - "prometheus.haumdaucher.de" secretName: prometheus-haumdaucher-de annotations: - kubernetes.io/ingress.class: "nginx" + kubernetes.io/ingress.class: "traefik" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" cert-manager.io/cluster-issuer: "letsencrypt-prod" # see https://kubernetes.github.io/ingress-nginx/examples/auth/basic/ @@ -89,6 +89,7 @@ alertmanager: enabled: true ingress: enabled: true + ingressClassName: traefik hosts: - alertmanager.haumdaucher.de tls: @@ -96,7 +97,7 @@ alertmanager: - "alertmanager.haumdaucher.de" secretName: alertmanager-haumdaucher-de annotations: - kubernetes.io/ingress.class: "nginx" + kubernetes.io/ingress.class: "traefik" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" cert-manager.io/cluster-issuer: "letsencrypt-prod" # see https://kubernetes.github.io/ingress-nginx/examples/auth/basic/ @@ -126,8 +127,31 @@ grafana: imageRenderer: enabled: true adminPassword: kohkohh5sah1Do3ize2x + additionalDataSources: + - name: InfluxDB Home Assistant + type: influxdb + access: proxy + url: http://influxdb-influxdb2.influxdb.svc.cluster.local:80 + jsonData: + version: Flux + organization: influxdata + defaultBucket: default + httpMode: POST + secureJsonData: + token: enaiY9yaiWi6ahv0phoph3FaiphoGh + - name: InfluxDB Home Assistant InfluxQL + type: influxdb + access: proxy + url: http://influxdb-influxdb2.influxdb.svc.cluster.local:80 + jsonData: + dbName: default + httpHeaderName1: 'Authorization' + httpMode: POST + secureJsonData: + httpHeaderValue1: 'Token enaiY9yaiWi6ahv0phoph3FaiphoGh' ingress: enabled: true + ingressClassName: traefik hosts: - grafana.haumdaucher.de tls: @@ -135,7 +159,7 @@ grafana: - "grafana.haumdaucher.de" secretName: grafana-haumdaucher-de annotations: - kubernetes.io/ingress.class: "nginx" + kubernetes.io/ingress.class: "traefik" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" cert-manager.io/cluster-issuer: "letsencrypt-prod" env: @@ -149,4 +173,22 @@ grafana: # interval_day: "MMM YYYY" # interval_month: "YYYY-MM" # interval_year: "YYYY" + sidecar: + dashboards: + enabled: true + label: grafana_dashboard + labelValue: "1" + searchNamespace: monitoring + folder: /tmp/dashboards + defaultFolderName: null + provider: + name: sidecarProvider + orgId: 1 + folder: "" + type: file + disableDeletion: false + editable: false + allowUiUpdates: false + options: + path: /tmp/dashboards diff --git a/k8s/monitoring/scripts/discover-home-assistant.sh b/k8s/monitoring/scripts/discover-home-assistant.sh new file mode 100755 index 0000000..a48fbd0 --- /dev/null +++ b/k8s/monitoring/scripts/discover-home-assistant.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# discover-home-assistant.sh — Query Home Assistant REST API for entity metadata +# Usage: ./discover-home-assistant.sh +# Requires: curl, jq + +set -euo pipefail + +HA_URL="https://hass.moritzgraf.de" +HA_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI2NDMyZTI1MDIzZjU0ZmFiYTU3OTc0ODYyMzg3M2ExZSIsImlhdCI6MTc4MjczMTY4NiwiZXhwIjoyMDk4MDkxNjg2fQ.u8kFHoYL2kdhVenfe3BiWcNkN_en3yn91MRGSVJvbmw" + +echo "=== Home Assistant Entity Discovery ===" +echo "URL: $HA_URL" +echo "" + +# Get all entities +echo "→ Fetching all entities..." +ENTITIES=$(curl -s -H "Authorization: Bearer $HA_TOKEN" "$HA_URL/api/states") + +if [ -z "$ENTITIES" ] || echo "$ENTITIES" | jq -e '. == null' >/dev/null 2>&1; then + echo "ERROR: Failed to fetch entities. Check token and connectivity." + exit 1 +fi + +echo "" +echo "=== Entity Summary ===" +echo "$ENTITIES" | jq -r ' + [.[] | { + entity_id, + state, + friendly_name: (.attributes.friendly_name // ""), + unit: (.attributes.unit_of_measurement // ""), + device_class: (.attributes.device_class // ""), + domain: (.entity_id | split(".")[0]) + }] + | sort_by(.domain, .entity_id) + | .[] + | "\(.entity_id) | state=\(.state) | name=\(.friendly_name) | unit=\(.unit) | class=\(.device_class) | domain=\(.domain)" +' + +echo "" +echo "=== Domains (entity types) ===" +echo "$ENTITIES" | jq -r '[.[].entity_id | split(".")[0]] | unique | .[]' + +echo "" +echo "=== Entities with unit_of_measurement (likely sensor data in InfluxDB) ===" +echo "$ENTITIES" | jq -r ' + [.[] | select(.attributes.unit_of_measurement != null) | { + entity_id, + unit: .attributes.unit_of_measurement, + friendly_name: (.attributes.friendly_name // ""), + device_class: (.attributes.device_class // "") + }] + | sort_by(.unit, .entity_id) + | .[] + | "\(.entity_id) | \(.unit) | \(.device_class) | \(.friendly_name)" +' + +echo "" +echo "=== Device classes ===" +echo "$ENTITIES" | jq -r '[.[].attributes.device_class | select(. != null)] | unique | .[]' + +echo "" +echo "=== Discovery complete ===" \ No newline at end of file diff --git a/k8s/monitoring/scripts/discover-influxdb.sh b/k8s/monitoring/scripts/discover-influxdb.sh new file mode 100755 index 0000000..c85272f --- /dev/null +++ b/k8s/monitoring/scripts/discover-influxdb.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# discover-influxdb.sh — Query InfluxDB to discover measurements, fields, and tags +# Usage: ./discover-influxdb.sh [namespace] +# Requires: kubectl, curl, jq + +set -euo pipefail + +NAMESPACE="${1:-influxdb}" +INFLUXDB_TOKEN="enaiY9yaiWi6ahv0phoph3FaiphoGh" +INFLUXDB_ORG="influxdata" +INFLUXDB_BUCKET="default" +LOCAL_PORT="8086" + +echo "=== InfluxDB Discovery ===" +echo "Namespace: $NAMESPACE" +echo "" + +# Start port-forward in background +echo "→ Starting port-forward to influxdb-influxdb2..." +kubectl -n "$NAMESPACE" port-forward "svc/influxdb-influxdb2" "$LOCAL_PORT:80" &>/dev/null & +PF_PID=$! +sleep 2 + +# Ensure cleanup on exit +cleanup() { + kill "$PF_PID" 2>/dev/null || true +} +trap cleanup EXIT + +API="http://localhost:$LOCAL_PORT/api/v2/query?org=$INFLUXDB_ORG" +AUTH="Authorization: Token $INFLUXDB_TOKEN" +CT="Content-Type: application/vnd.flux" + +echo "" +echo "=== Measurements (tables) in bucket '$INFLUXDB_BUCKET' ===" +curl -s "$API" -H "$AUTH" -H "$CT" \ + -d "import \"influxdata/influxdb/schema\" + schema.measurements(bucket: \"$INFLUXDB_BUCKET\")" \ + | jq -r '.[] | ._value' 2>/dev/null || echo "(no results or parse error)" + +echo "" +echo "=== Measurements with data in last 6 months ===" +curl -s "$API" -H "$AUTH" -H "$CT" \ + -d "from(bucket: \"$INFLUXDB_BUCKET\") + |> range(start: -6mo) + |> keep(columns: [\"_measurement\"]) + |> distinct() + |> sort()" \ + | jq -r '.[] | ._value' 2>/dev/null || echo "(no results or parse error)" + +echo "" +echo "=== For each measurement: fields, tags, and sample count ===" + +# Get measurements +MEASUREMENTS=$(curl -s "$API" -H "$AUTH" -H "$CT" \ + -d "import \"influxdata/influxdb/schema\" + schema.measurements(bucket: \"$INFLUXDB_BUCKET\")" \ + | jq -r '.[] | ._value' 2>/dev/null) + +for m in $MEASUREMENTS; do + echo "" + echo "--- Measurement: $m ---" + + # Fields + echo " Fields:" + curl -s "$API" -H "$AUTH" -H "$CT" \ + -d "import \"influxdata/influxdb/schema\" + schema.measurementFieldKeys(bucket: \"$INFLUXDB_BUCKET\", measurement: \"$m\")" \ + | jq -r '.[] | " - \(._value)"' 2>/dev/null || echo " (none)" + + # Tags + echo " Tags:" + curl -s "$API" -H "$AUTH" -H "$CT" \ + -d "import \"influxdata/influxdb/schema\" + schema.measurementTagKeys(bucket: \"$INFLUXDB_BUCKET\", measurement: \"$m\")" \ + | jq -r '.[] | " - \(._value)"' 2>/dev/null || echo " (none)" + + # Tag values for entity_id (most common tag in HA data) + echo " entity_id values (sample):" + curl -s "$API" -H "$AUTH" -H "$CT" \ + -d "import \"influxdata/influxdb/schema\" + schema.tagValues(bucket: \"$INFLUXDB_BUCKET\", tag: \"entity_id\") + |> filter(fn: (r) => r._measurement == \"$m\") + |> limit(n: 20)" \ + | jq -r '.[] | " - \(._value)"' 2>/dev/null || echo " (none)" + + # Sample count in last 6 months + echo " Data points (last 6 months):" + curl -s "$API" -H "$AUTH" -H "$CT" \ + -d "from(bucket: \"$INFLUXDB_BUCKET\") + |> range(start: -6mo) + |> filter(fn: (r) => r._measurement == \"$m\") + |> count()" \ + | jq -r '.[] | " - \(._value)"' 2>/dev/null || echo " (none)" +done + +echo "" +echo "=== Discovery complete ===" \ No newline at end of file diff --git a/k8s/tt-rss/tt-rss-mariadb.secret.yml b/k8s/tt-rss/tt-rss-mariadb.secret.yml deleted file mode 100644 index 1a17a9b..0000000 --- a/k8s/tt-rss/tt-rss-mariadb.secret.yml +++ /dev/null @@ -1,10 +0,0 @@ -rootUser: - password: cah6eepai1Saepho8tho - forcePassword: true -db: - name: tt-rss - user: tt-rss - password: AhS5cai7oph3ahN4Aile - forcePassword: true -replication: - enabled: false \ No newline at end of file diff --git a/k8s/tt-rss/tt-rss-phpmyadmin.yml b/k8s/tt-rss/tt-rss-phpmyadmin.yml deleted file mode 100644 index d6f6823..0000000 --- a/k8s/tt-rss/tt-rss-phpmyadmin.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- -db: - host: tt-rss-mariadb -ingress: - enabled: true - hosts: - - path: "/" - tls: true - name: "tt-rss.phpmyadmin.haumdaucher.de" - tlsSecret: "tt-rss-phpmyadmin-haumdaucher-de" - annotations: - cert-manager.io/cluster-issuer: "letsencrypt-prod" - nginx.ingress.kubernetes.io/proxy-body-size: "0" \ No newline at end of file diff --git a/k8s/tt-rss/tt-rss.helm.secret.yml b/k8s/tt-rss/tt-rss.helm.secret.yml deleted file mode 100644 index c6c3fcd..0000000 --- a/k8s/tt-rss/tt-rss.helm.secret.yml +++ /dev/null @@ -1,32 +0,0 @@ -env: - # -- External URL you use to connect to the RSS (the one you enter in your browser) - TTRSS_SELF_URL_PATH: "tt-rss.moritzgraf.de" - # -- Postgres database hostname - # @default -- internal postgresql URL - TTRSS_DB_HOST: "tt-rss-mariadb" - # -- Postgres database port. - TTRSS_DB_PORT: "3306" - # -- Postgres database user name - # @default -- postgresql.postgresqlUsername value - TTRSS_DB_USER: "tt-rss" - # -- Postgres database password - # @default -- postgresql.postgresqlPassword value - TTRSS_DB_PASS: "AhS5cai7oph3ahN4Aile" - # -- Postgres database password - # @default -- postgresql.postgresqlDatabase value - TTRSS_DB_NAME: "tt-rss" - -ingress: - main: - enabled: true - annotations: - kubernetes.io/ingress.class: "nginx" - nginx.ingress.kubernetes.io/force-ssl-redirect: "true" - cert-manager.io/cluster-issuer: "letsencrypt-prod" - tls: - - hosts: - - "tt-rss.moritzgraf.de" - secretName: tt-rss-moritzgraf-de -postgresql: - enabled: false - \ No newline at end of file diff --git a/k8s/tt-rss/tt-rss.secret.yaml b/k8s/tt-rss/tt-rss.secret.yaml deleted file mode 100644 index 040e15407738f1470876154391595c577f4213cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9653 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 diff --git a/k8s/velero/dropbox_sync.yml b/k8s/velero/dropbox_sync.yml deleted file mode 100644 index b4fae60..0000000 --- a/k8s/velero/dropbox_sync.yml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: batch/v1 -kind: CronJob -metadata: - name: dropbox-sync - namespace: velero -spec: - schedule: "0 5 * * *" - jobTemplate: - spec: - template: - spec: - serviceAccountName: default - containers: - - name: dopybox-sync - image: iptizer/swiss - volumeMounts: - - name: rclone-config - mountPath: /rclone.secret - subPath: "rclone.secret" - command: - - "devbox" - - "run" - args: - - "rclone --config /rclone.secret -v copy minio:backup/ dropbox:server_backup/velero/" - volumes: - - name: rclone-config - secret: - secretName: rclone-config - restartPolicy: Never \ No newline at end of file diff --git a/k8s/velero/gdrive_sa.json.secret b/k8s/velero/gdrive_sa.json.secret new file mode 100755 index 0000000000000000000000000000000000000000..2fa38ee820de4101b19b0bf4c1f8b463728b2523 GIT binary patch literal 2414 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 literal 427 zcmV;c0aX3~M@dveQdv+`0HU_|(2^BMoptV%%Xc>x2nDcAg49cR;ZPTrB(~R203dgw zbBS{ zzsU1we5(1;d#*X67}j4TAlDCyCL<$sskJQTjq%Rq-)^d0#z&wT<H1mZqu0Tq3vR z4$nCgIQS=D<_3phfdM{=01CokIhS`9^4-_Hah^XTq}YUVi%a74#!mgP_UWaH>Vo>t zrB>*wo`%M#3F>hAzI23uA4;0;{j5gGc9~^!+G=7S#J`s^3@!g7AHPOG`gD4R>+6xA1>E<9s9^f-vZqF61YD}6@u+I-Oo$Er V3VMEMo+iZ~(14-hx7KxGg)k" + +# Annotations to add to secret +secretAnnotations: {} + +# Labels to add to the Velero deployment's. Optional. +labels: {} + +# Annotations to add to the Velero deployment's pod template. Optional. +# +# If using kube2iam or kiam, use the following annotation with your AWS_ACCOUNT_ID +# and VELERO_ROLE_NAME filled in: +podAnnotations: {} + # iam.amazonaws.com/role: "arn:aws:iam:::role/" + +# Additional pod labels for Velero deployment's template. Optional +# ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +# Number of old history to retain to allow rollback (If not set, default Kubernetes value is set to 10) +# revisionHistoryLimit: 1 + +# Resource requests/limits to specify for the Velero deployment. +# https://velero.io/docs/v1.6/customize-installation/#customize-resource-requests-and-limits +resources: {} + # requests: + # cpu: 500m + # memory: 128Mi + # limits: + # cpu: 1000m + # memory: 512Mi + +# Container resize policy for the Velero deployment. +# See: https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/ +resizePolicy: [] + # - resourceName: cpu + # restartPolicy: NotRequired + # - resourceName: memory + # restartPolicy: RestartContainer + +# Configure hostAliases for Velero deployment. Optional +# For more information, check: https://kubernetes.io/docs/tasks/network/customize-hosts-file-for-pods/ +hostAliases: [] + # - ip: "127.0.0.1" + # hostnames: + # - "foo.local" + # - "bar.local" + +# Resource requests/limits to specify for the upgradeCRDs job pod. Need to be adjusted by user accordingly. +upgradeJobResources: {} +# requests: +# cpu: 50m +# memory: 128Mi +# limits: +# cpu: 100m +# memory: 256Mi +upgradeCRDsJob: + # Extra volumes for the Upgrade CRDs Job. Optional. + extraVolumes: [] + # Extra volumeMounts for the Upgrade CRDs Job. Optional. + extraVolumeMounts: [] + # Additional values to be used as environment variables. Optional. + extraEnvVars: [] + # Simple value + # - name: SIMPLE_VAR + # value: "simple-value" + + # FieldRef example + # - name: MY_POD_LABEL + # valueFrom: + # fieldRef: + # fieldPath: metadata.labels['my_label'] + + # Configure if API credential for Service Account is automounted. + automountServiceAccountToken: true + # Configure the shell cmd in case you are using custom image + # shellCmd: /tmp/sh + # updateCmd: /velero install --crds-only --dry-run -o yaml | /tmp/kubectl apply -f - + +# Configure the dnsPolicy of the Velero deployment +# See: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy +dnsPolicy: ClusterFirst + +# Init containers to add to the Velero deployment's pod spec. At least one plugin provider image is required. +# If the value is a string then it is evaluated as a template. +initContainers: + # - name: velero-plugin-for-aws + # image: velero/velero-plugin-for-aws:v1.13.1 + # imagePullPolicy: IfNotPresent + # volumeMounts: + # - mountPath: /target + # name: plugins + +# SecurityContext to use for the Velero deployment. Optional. +# Set fsGroup for `AWS IAM Roles for Service Accounts` +# see more informations at: https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html +podSecurityContext: {} + # fsGroup: 1337 + +# Container Level Security Context for the 'velero' container of the Velero deployment. Optional. +# See: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container +containerSecurityContext: {} + # allowPrivilegeEscalation: false + # capabilities: + # drop: ["ALL"] + # add: [] + # readOnlyRootFilesystem: true + +# Container Lifecycle Hooks to use for the Velero deployment. Optional. +lifecycle: {} + +# Pod priority class name to use for the Velero deployment. Optional. +priorityClassName: "" + +# Pod runtime class name to use for the Velero deployment. Optional. +runtimeClassName: "" + +# The number of seconds to allow for graceful termination of the pod. Optional. +terminationGracePeriodSeconds: 3600 + +# Liveness probe of the pod +livenessProbe: + httpGet: + path: /metrics + port: http-monitoring + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 5 + +# Readiness probe of the pod +readinessProbe: + httpGet: + path: /metrics + port: http-monitoring + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 30 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 5 + +# Tolerations to use for the Velero deployment. Optional. +tolerations: [] + +# Affinity to use for the Velero deployment. Optional. +affinity: {} + +# Node selector to use for the Velero deployment. Optional. +nodeSelector: {} + +# DNS configuration to use for the Velero deployment. Optional. +dnsConfig: {} + +# Pod Disruption Budget for the Velero deployment. Optional. +# Velero runs a single replica with a Recreate strategy by default, so this is +# disabled by default. Enable it when running Velero with multiple replicas. +podDisruptionBudget: + enabled: false + # minAvailable and maxUnavailable are mutually exclusive; set only one. + minAvailable: 1 + maxUnavailable: "" + # See https://kubernetes.io/docs/tasks/run-application/configure-pdb/ + # unhealthyPodEvictionPolicy: IfHealthyBudget + annotations: {} + labels: {} + +# Extra volumes for the Velero deployment. Optional. +extraVolumes: [] + +# Extra volumeMounts for the Velero deployment. Optional. +extraVolumeMounts: [] + +# Extra K8s manifests to deploy +extraObjects: [] + # - apiVersion: secrets-store.csi.x-k8s.io/v1 + # kind: SecretProviderClass + # metadata: + # name: velero-secrets-store + # spec: + # provider: aws + # parameters: + # objects: | + # - objectName: "velero" + # objectType: "secretsmanager" + # jmesPath: + # - path: "access_key" + # objectAlias: "access_key" + # - path: "secret_key" + # objectAlias: "secret_key" + # secretObjects: + # - data: + # - key: access_key + # objectName: client-id + # - key: client-secret + # objectName: client-secret + # secretName: velero-secrets-store + # type: Opaque + +# Settings for Velero's prometheus metrics. Enabled by default. +metrics: + enabled: true + scrapeInterval: 30s + scrapeTimeout: 10s + + # service metdata if metrics are enabled + service: + annotations: {} + type: ClusterIP + labels: {} + nodePort: null + + # External/Internal traffic policy setting (Cluster, Local) + # https://kubernetes.io/docs/reference/networking/virtual-ips/#traffic-policies + externalTrafficPolicy: "" + internalTrafficPolicy: "" + + # the IP family policy for the metrics Service to be able to configure dual-stack; see [Configure dual-stack](https://kubernetes.io/docs/concepts/services-networking/dual-stack/#services). + ipFamilyPolicy: "" + # a list of IP families for the metrics Service that should be supported, in the order in which they should be applied to ClusterIP. Can be "IPv4" and/or "IPv6". + ipFamilies: [] + + # Pod annotations for Prometheus + podAnnotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8085" + prometheus.io/path: "/metrics" + + serviceMonitor: + autodetect: true + enabled: false + annotations: {} + additionalLabels: {} + + # metrics.serviceMonitor.metricRelabelings Specify Metric Relabelings to add to the scrape endpoint + # ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#relabelconfig + # metricRelabelings: [] + # metrics.serviceMonitor.relabelings [array] Prometheus relabeling rules + # relabelings: [] + # ServiceMonitor namespace. Default to Velero namespace. + # namespace: + # ServiceMonitor connection scheme. Defaults to HTTP. + # scheme: "" + # ServiceMonitor connection tlsConfig. Defaults to {}. + # tlsConfig: {} + nodeAgentPodMonitor: + autodetect: true + enabled: false + annotations: {} + additionalLabels: {} + # metrics.nodeAgentPodMonitor.metricRelabelings Specify Metric Relabelings to add to the scrape endpoint + # ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#relabelconfig + # metricRelabelings: [] + # metrics.nodeAgentPodMonitor.relabelings [array] Prometheus relabeling rules + # relabelings: [] + # PodMonitor namespace. Default to Velero namespace. + # namespace: + # PodMonitor connection scheme. Defaults to HTTP. + # scheme: "" + # PodMonitor connection tlsConfig. Defaults to {}. + # tlsConfig: {} + + prometheusRule: + autodetect: true + enabled: false + # Additional labels to add to deployed PrometheusRule + additionalLabels: {} + # PrometheusRule namespace. Defaults to Velero namespace. + # namespace: "" + # Rules to be deployed + spec: [] + # - alert: VeleroBackupFailed + # annotations: + # message: Velero backup {{ $labels.schedule }} has failed + # expr: |- + # velero_backup_last_status{schedule!=""} != 1 + # for: 15m + # labels: + # severity: warning + # - alert: VeleroBackupFailing + # annotations: + # message: Velero backup {{ $labels.schedule }} has been failing for the last 12h + # expr: |- + # velero_backup_last_status{schedule!=""} != 1 + # for: 12h + # labels: + # severity: critical + # - alert: VeleroNoNewBackup + # annotations: + # message: Velero backup {{ $labels.schedule }} has not run successfully in the last 25h + # expr: |- + # ( + # (time() - velero_backup_last_successful_timestamp{schedule!=""}) >bool (25 * 3600) + # or + # absent(velero_backup_last_successful_timestamp{schedule!=""}) + # ) == 1 + # for: 1h + # labels: + # severity: critical + # - alert: VeleroBackupPartialFailures + # annotations: + # message: Velero backup {{ $labels.schedule }} has {{ $value | humanizePercentage }} partialy failed backups + # expr: |- + # rate(velero_backup_partial_failure_total{schedule!=""}[25m]) + # / rate(velero_backup_attempt_total{schedule!=""}[25m]) > 0.5 + # for: 15m + # labels: + # severity: warning + +kubectl: + image: + repository: registry.k8s.io/kubectl + # Digest value example: sha256:c7bf8cf6e79f474fda2623a7b98572a1b909c4d912b05062b444ffe79883fdbe. + # If used, it will take precedence over the kubectl.image.tag. + # digest: + # kubectl image tag. If used, it will take precedence over the cluster Kubernetes version. + # tag: v1.34.5 + # Container Level Security Context for the 'kubectl' container of the crd jobs. Optional. + # See: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container + containerSecurityContext: {} + # Resource requests/limits to specify for the upgrade/cleanup job. Optional + resources: {} + # Annotations to set for the upgrade/cleanup job. Optional. + annotations: {} + # Labels to set for the upgrade/cleanup job. Optional. + labels: {} + # Extra volumes for the upgrade/cleanup job. Optional. + extraVolumes: [] + # Extra volumeMounts for the upgrade/cleanup job. Optional. + extraVolumeMounts: [] + +# This job upgrades the CRDs. +upgradeCRDs: true + +# This job is meant primarily for cleaning up CRDs on CI systems. +# Using this on production systems, especially those that have multiple releases of Velero, will be destructive. +cleanUpCRDs: false + +## +## End of deployment-related settings. +## + + +## +## Parameters for the `default` BackupStorageLocation and VolumeSnapshotLocation, +## and additional server settings. +## +configuration: + # Parameters for the BackupStorageLocation(s). Configure multiple by adding other element(s) to the backupStorageLocation slice. + # See https://velero.io/docs/v1.6/api-types/backupstoragelocation/ + backupStorageLocation: + # name is the name of the backup storage location where backups should be stored. If a name is not provided, + # a backup storage location will be created with the name "default". Optional. + - name: + # provider is the name for the backup storage location provider. + provider: "" + # bucket is the name of the bucket to store backups in. Required. + bucket: "" + # caCert defines a base64 encoded CA bundle to use when verifying TLS connections to the provider. Optional. + caCert: + # prefix is the directory under which all Velero data should be stored within the bucket. Optional. + prefix: + # default indicates this location is the default backup storage location. Optional. + default: false + # validationFrequency defines how frequently Velero should validate the object storage. Optional. + validationFrequency: + # backupSyncPeriod defines how frequently Velero should synchronize backups in object storage. Optional. + backupSyncPeriod: + # accessMode determines if velero can write to this backup storage location. Optional. + # default to ReadWrite, ReadOnly is used during migrations and restores. + accessMode: ReadWrite + credential: + # name of the secret used by this backupStorageLocation. + name: + # name of key that contains the secret data to be used. + key: + # Additional provider-specific configuration. See link above + # for details of required/optional fields for your provider. + config: {} + # region: + # s3ForcePathStyle: + # s3Url: + # kmsKeyId: + # resourceGroup: + # The ID of the subscription containing the storage account, if different from the cluster’s subscription. (Azure only) + # subscriptionId: + # storageAccount: + # publicUrl: + # Name of the GCP service account to use for this backup storage location. Specify the + # service account here if you want to use workload identity instead of providing the key file.(GCP only) + # serviceAccount: + # Option to skip certificate validation or not if insecureSkipTLSVerify is set to be true, the client side should set the + # flag. For Velero client Command like velero backup describe, velero backup logs needs to add the flag --insecure-skip-tls-verify + # insecureSkipTLSVerify: + + # annotations allows adding arbitrary annotations to this BackupStorageLocation resource. Optional. + annotations: {} + + # Parameters for the VolumeSnapshotLocation(s). Configure multiple by adding other element(s) to the volumeSnapshotLocation slice. + # See https://velero.io/docs/v1.6/api-types/volumesnapshotlocation/ + volumeSnapshotLocation: + # name is the name of the volume snapshot location where snapshots are being taken. If a name is not provided, + # a volume snapshot location will be created with the name "default". Optional. + - name: + # provider is the name for the volume snapshot provider. + provider: "" + credential: + # name of the secret used by this volumeSnapshotLocation. + name: + # name of key that contains the secret data to be used. + key: + # Additional provider-specific configuration. See link above + # for details of required/optional fields for your provider. + config: {} + # region: + # apiTimeout: + # resourceGroup: + # The ID of the subscription where volume snapshots should be stored, if different from the cluster’s subscription. If specified, also requires `configuration.volumeSnapshotLocation.config.resourceGroup`to be set. (Azure only) + # subscriptionId: + # incremental: + # snapshotLocation: + # project: + + # annotations allows adding arbitrary annotations to this VolumeSnapshotLocation resource. Optional. + annotations: {} + + # These are server-level settings passed as CLI flags to the `velero server` command. Velero + # uses default values if they're not passed in, so they only need to be explicitly specified + # here if using a non-default value. The `velero server` default values are shown in the + # comments below. + # -------------------- + # `velero server` default: kopia + uploaderType: + # `velero server` default: 1m + backupSyncPeriod: + # `velero server` default: 4h + fsBackupTimeout: + # `velero server` default: 30 + clientBurst: + # `velero server` default: 500 + clientPageSize: + # `velero server` default: 20.0 + clientQPS: + # Name of the default backup storage location. Default: default + defaultBackupStorageLocation: + # The default duration any single item operation can take before timing out, especially important for large volume schedules. Default 4h + defaultItemOperationTimeout: + # How long to wait by default before backups can be garbage collected. Default: 72h + defaultBackupTTL: + # Name of the default volume snapshot location. + defaultVolumeSnapshotLocations: + # `velero server` default: empty + disableControllers: + # `velero server` default: false + disableInformerCache: false + # `velero server` default: 1h + garbageCollectionFrequency: + # `velero server` default: 1 + itemBlockWorkerCount: + # Set log-format for Velero pod. Default: text. Other option: json. + logFormat: + # Set log-level for Velero pod. Default: info. Other options: debug, warning, error, fatal, panic. + logLevel: + # The address to expose prometheus metrics. Default: :8085 + metricsAddress: + # Directory containing Velero plugins. Default: /plugins + pluginDir: + # The address to expose the pprof profiler. Default: localhost:6060 + profilerAddress: + # `velero server` default: false + restoreOnlyMode: + # `velero server` default: customresourcedefinitions,namespaces,storageclasses,volumesnapshotclass.snapshot.storage.k8s.io,volumesnapshotcontents.snapshot.storage.k8s.io,volumesnapshots.snapshot.storage.k8s.io,persistentvolumes,persistentvolumeclaims,secrets,configmaps,serviceaccounts,limitranges,pods,replicasets.apps,clusterclasses.cluster.x-k8s.io,clusters.cluster.x-k8s.io,clusterresourcesets.addons.cluster.x-k8s.io + restoreResourcePriorities: + # `velero server` default: 1m + storeValidationFrequency: + # How long to wait on persistent volumes and namespaces to terminate during a restore before timing out. Default: 10m + terminatingResourceTimeout: + # Bool flag to configure Velero server to move data by default for all snapshots supporting data movement. Default: false + defaultSnapshotMoveData: + # Comma separated list of velero feature flags. default: empty + # features: EnableCSI + features: + # Configures the timeout for provisioning the volume created from the CSI snapshot. Default: 30m + dataMoverPrepareTimeout: + # Resource requests/limits to specify for the repository-maintenance job. Optional. + # https://velero.io/docs/v1.14/repository-maintenance/#resource-limitation + repositoryMaintenanceJob: + # Per-repository resource settings ConfigMap + # This ConfigMap allows specifying different settings for different repositories + # See: https://velero.io/docs/main/repository-maintenance/ + repositoryConfigData: + # Name of the ConfigMap to create. If not provided, will use "velero-repo-maintenance" + name: "velero-repo-maintenance" + # Global configuration applied to all repositories + # This configuration is used when no specific repository configuration is found + # global: + # podResources: + # cpuRequest: "100m" + # cpuLimit: "200m" + # memoryRequest: "100Mi" + # memoryLimit: "200Mi" + # keepLatestMaintenanceJobs: 1 + # loadAffinity: + # - nodeSelector: + # matchExpressions: + # - key: "cloud.google.com/machine-family" + # operator: "In" + # values: ["e2"] + # - nodeSelector: + # matchExpressions: + # - key: "topology.kubernetes.io/zone" + # operator: "In" + # values: ["us-central1-a", "us-central1-b", "us-central1-c"] + # priorityClassName: "low-priority" # Note: priorityClassName is only supported in global configuration + global: + keepLatestMaintenanceJobs: 3 + # Repository-specific configurations + # Repository keys are formed as: "{namespace}-{storageLocation}-{repositoryType}" + # For example: "default-default-kopia" or "prod-s3-backup-kopia" + # Note: priorityClassName is NOT supported in repository-specific configurations + # repositories: + # "kibishii-default-kopia": + # podResources: + # cpuRequest: "200m" + # cpuLimit: "400m" + # memoryRequest: "200Mi" + # memoryLimit: "400Mi" + # keepLatestMaintenanceJobs: 2 + repositories: {} + # `velero server` default: velero + namespace: + # additional command-line arguments that will be passed to the `velero server` + # e.g.: extraArgs: ["--foo=bar"] + extraArgs: [] + + # Additional values to be used as environment variables. Optional. + extraEnvVars: [] + # Simple value + # - name: SIMPLE_VAR + # value: "simple-value" + + # FieldRef example + # - name: MY_POD_LABEL + # valueFrom: + # fieldRef: + # fieldPath: metadata.labels['my_label'] + + # Set true for backup all pod volumes without having to apply annotation on the pod when used file system backup Default: false. + defaultVolumesToFsBackup: + + # How often repository maintain is run for repositories by default. + defaultRepoMaintainFrequency: + +## +## End of backup/snapshot location settings. +## + + +## +## Settings for additional Velero resources. +## + +rbac: + # Whether to create the Velero role and role binding to give all permissions to the namespace to Velero. + create: true + # Whether to create the cluster role binding to give administrator permissions to Velero + clusterAdministrator: true + # Name of the ClusterRole. + clusterAdministratorName: cluster-admin + +# Information about the Kubernetes service account Velero uses. +serviceAccount: + server: + create: true + name: + annotations: + labels: + imagePullSecrets: [] + # - registrySecretName + # Configure if API credential for Service Account is automounted. + automountServiceAccountToken: true + +# Info about the secret to be used by the Velero deployment, which +# should contain credentials for the cloud provider IAM account you've +# set up for Velero. +credentials: + # Whether a secret should be used. Set to false if, for examples: + # - using kube2iam or kiam to provide AWS IAM credentials instead of providing the key file. (AWS only) + # - using workload identity instead of providing the key file. (Azure/GCP only) + useSecret: true + # Name of the secret to create if `useSecret` is true and `existingSecret` is empty + name: + # Name of a pre-existing secret (if any) in the Velero namespace + # that should be used to get IAM account credentials. Optional. + existingSecret: + # Data to be stored in the Velero secret, if `useSecret` is true and `existingSecret` is empty. + # As of the current Velero release, Velero only uses one secret key/value at a time. + # The key must be named `cloud`, and the value corresponds to the entire content of your IAM credentials file. + # Note that the format will be different for different providers, please check their documentation. + # Here is a list of documentation for plugins maintained by the Velero team: + # [AWS] https://github.com/vmware-tanzu/velero-plugin-for-aws/blob/main/README.md + # [GCP] https://github.com/vmware-tanzu/velero-plugin-for-gcp/blob/main/README.md + # [Azure] https://github.com/vmware-tanzu/velero-plugin-for-microsoft-azure/blob/main/README.md + secretContents: {} + # cloud: | + # [default] + # aws_access_key_id= + # aws_secret_access_key= + # additional key/value pairs to be used as environment variables such as "DIGITALOCEAN_TOKEN: ". Values will be stored in the secret. + extraEnvVars: {} + # Name of a pre-existing secret (if any) in the Velero namespace + # that will be used to load environment variables into velero and node-agent. + # Secret should be in format - https://kubernetes.io/docs/concepts/configuration/secret/#use-case-as-container-environment-variables + extraSecretRef: "" + +# Whether to create backupstoragelocation crd, if false => do not create a default backup location +backupsEnabled: true +# Whether to create volumesnapshotlocation crd, if false => disable snapshot feature +snapshotsEnabled: true + +# Whether to deploy the node-agent daemonset. +deployNodeAgent: false + +nodeAgent: + # Disables Host Path volumes for node-agent. + disableHostPath: false + podVolumePath: /var/lib/kubelet/pods + pluginVolumePath: /var/lib/kubelet/plugins + # Pod priority class name to use for the node-agent daemonset. Optional. + priorityClassName: "" + # Pod runtime class name to use for the node-agent daemonset. Optional. + runtimeClassName: "" + # Resource requests/limits to specify for the node-agent daemonset deployment. Optional. + # https://velero.io/docs/v1.6/customize-installation/#customize-resource-requests-and-limits + resources: {} + # requests: + # cpu: 500m + # memory: 512Mi + # limits: + # cpu: 1000m + # memory: 1024Mi + # Container resize policy for the node-agent daemonset. + # See: https://kubernetes.io/docs/tasks/configure-pod-container/resize-container-resources/ + resizePolicy: [] + # - resourceName: cpu + # restartPolicy: NotRequired + # - resourceName: memory + # restartPolicy: RestartContainer + + # Tolerations to use for the node-agent daemonset. Optional. + tolerations: [] + + # Annotations to set for the node-agent daemonset. Optional. + annotations: {} + + # labels to set for the node-agent daemonset. Optional. + labels: {} + + # Additional pod labels for the node-agent daemonset. Optional + # ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + podLabels: {} + + # will map /scratch to emptyDir. Set to false and specify your own volume + # via extraVolumes and extraVolumeMounts that maps to /scratch + # if you don't want to use emptyDir. + useScratchEmptyDir: true + + # Extra volumes for the node-agent daemonset. Optional. + extraVolumes: [] + + # Extra volumeMounts for the node-agent daemonset. Optional. + extraVolumeMounts: [] + + # Additional values to be used as environment variables for node-agent daemonset. Optional. + extraEnvVars: [] + # Simple key/value + # - name: SIMPLE_VAR + # value: "simple-value" + + # FieldRef example + # - name: MY_POD_LABEL + # valueFrom: + # fieldRef: + # fieldPath: metadata.labels['my_label'] + + # Additional command-line arguments that will be passed to the node-agent. Optional. + # e.g.: extraArgs: ["--foo=bar"] + extraArgs: [] + + # Configure the dnsPolicy of the node-agent daemonset + # See: https://kubernetes.io/docs/concepts/services-networking/dns-pod-service/#pod-s-dns-policy + dnsPolicy: ClusterFirst + + # Configure hostAliases for node-agent daemonset. Optional + # For more information, check: https://kubernetes.io/docs/tasks/network/customize-hosts-file-for-pods/ + hostAliases: [] + # - ip: "127.0.0.1" + # hostnames: + # - "foo.local" + # - "bar.local" + + # SecurityContext to use for the Velero deployment. Optional. + # Set fsGroup for `AWS IAM Roles for Service Accounts` + # see more informations at: https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html + podSecurityContext: + runAsUser: 0 + # fsGroup: 1337 + + # Container Level Security Context for the 'node-agent' container of the node-agent daemonset. Optional. + # See: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container + containerSecurityContext: {} + + # Container Lifecycle Hooks to use for the node-agent daemonset. Optional. + lifecycle: {} + + # Node selector to use for the node-agent daemonset. Optional. + nodeSelector: {} + + # Affinity to use with node-agent daemonset. Optional. + affinity: {} + + # DNS configuration to use for the node-agent daemonset. Optional. + dnsConfig: {} + + # Update strategy to use for the node-agent daemonset. Optional. + updateStrategy: {} + +# Backup schedules to create. +# Eg: +# schedules: +# mybackup: +# disabled: false +# labels: +# myenv: foo +# annotations: +# myenv: foo +# schedule: "0 0 * * *" +# useOwnerReferencesInBackup: false +# paused: false +# skipImmediately: false +# template: +# ttl: "240h" +# storageLocation: default +# includedNamespaces: +# - foo +# # See: https://velero.io/docs/v1.14/resource-filtering/#excludes +# excludedNamespaceScopedResources: +# - persistentVolumeClaims +# excludedClusterScopedResources: +# - persistentVolumes +schedules: {} + +# Velero ConfigMaps. +# Eg: +# configMaps: + # See: https://velero.io/docs/v1.11/file-system-backup/ +# fs-restore-action-config: +# labels: +# velero.io/plugin-config: "" +# velero.io/pod-volume-restore: RestoreItemAction +# data: +# image: velero/velero:v1.17.1 +# cpuRequest: 200m +# memRequest: 128Mi +# cpuLimit: 200m +# memLimit: 128Mi +# secCtx: | +# capabilities: +# drop: +# - ALL +# add: [] +# allowPrivilegeEscalation: false +# readOnlyRootFilesystem: true +# runAsUser: 1001 +# runAsGroup: 999 +configMaps: {} + +## +## End of additional Velero resource settings. +## + diff --git a/k8s/velero/velero.secret.yaml b/k8s/velero/velero.secret.yaml index 1a479f4f444a91e2bb90eae4acf161fb4eeb84d9..494373669c9979c0753ddad70fe45d1ab3922080 100644 GIT binary patch literal 1330 zcmV-21ZM@dveQdv+`0G#k|c$*7!&ej``3Wpn2~vzY@n_zOF5J#qCHayRXNASsOI0yv zp6&4V9W}}YmP1#m{cQIL2siUrnIR10Es+Jti#+1)d-N!mEcGRTnK=lEcSfq@7_MTn z`4Dme^OAy8ftfArW>ECtgl~hX3Asub9~;3DW}`9M*~3l-D75rS%^HE`uI~KdapKrB zS;>f;%Z{O9g5>FuP$Jghz8K}UT0#pwT3{!7hE(@YJHBR1XpxtVQeE>gkfo1&Z4DWd zAA;`85Ufwpsa(KIh!0Qm;Rq)_b@2*HFj{^rS?{)wQ1%t4$j)H0sL}FUgIJQ|@rv3M z@NH~lnoxq#_(SZ=f^6<+H6X`T_`h@@>$rWZ25`?QYOC|~wn+-|=drC^bkk9zcC>}- zBS@iG(<)Bth%;}i*Mrya)FfYzVTspbfoUTFjp=0we1bB5ZlQEkGJcgTVbzdvYepdM z71x0+OdMW7L>9d@d_C8vXgsc#$zn}}IKzud46c1X9Bpbjnt^yboR6^z7a0G;Tyk2e zmC)Li&}}jqj&o<%<+xPt7rIM-Eqp9mUG0+_T5dR9R;p~%;3HA#*#1bg#q-3(gsn-u z$<_piBNiv#ZPVlq{x=E9CBhYka2;v^(i$D#4e;RM&EFOmhxA1-d>6J^obDB1ktuDN z(3rwQbdji=lW!W-GWlkK-!4wer{Oi=(%O0D-P%`W=;0dt!7psN6^LH z6Gas#(h0)kQcYrsABvh_IBcMrr4f*F@} z-Y}LmO`CX0@!X)SeTPKz-$Bw&U2aWJYVVppFb$_uZirBxzAg+ogP7T-E~hmf zQ6}#AYJi-c=VI)RW#s(hB9I|R&z4uWs=kPYeaNd%UH4RM$soF2zm~V3n?G`ncIw~B z#f5}VTh#eO>M%xiiU@I#aI#={P3fp8WZmWB@p12EPaMZDu=;f{S@txoL~%4euD94= zHTuotWb{r?KOg^7sS@fuO67VgMGI!8Ly^qmpUeCVH{auYqk|LtDmw^?si%1Ki$T)G zEa<>^so(TmXU|3~x*LZ^-pZ%M_p(7aGNt9B%id*TdlY(s>1;D1i|AW{m$)1o-D_$n z3hhi?$c&b?^~#YM_A!-alJLvB`3H@7)t4z3O>b+kv-{OU8JKbr2j91f5a-;iKNsl; z)6_lYxvbsG1L(6a;t8?sCZa(G3_OE@jQm<1{PIes@4S5CfDvCOb&NUsA~frrVzrN8vR!x5m6LZ#b5BuOEOLa{Wbm?;js>#Po7X%_Ujf`M448@}I zi5W!tri{&ZXF!$r;dg*p`#YuSGnM^W&Y9YE@y1+huE#cy+){xQdP$xvyq)(M8Q zV{yu@4`m^?h6@cz5b=zg3#G0>=|@Ykl0bQPe^%+BlWBAg1elsJzERCERWx-^2>`Q? o!0IHn#lBS{kFF1>MnV5x`HB~kCH@W~8umW>Ai!1X_Q^%~a>Xf}kpKVy literal 1329 zcmV-11%-UXHTy_xG}4E0i+g07_c81+AJ5@_d5~R!ubE|XjlSbZ>QhWT=Xn#%ey<# z!$XgVlnNk?(Z#0qw~`7V)tDYwWtPSgsI{~(*@NL?20R1~+xKMz+hAmI0^!J8Vf3R? ztZcaQl9Wt9#+({|dZxk*;O%(NHnBZ<5SmgKW$$uAGLOO$+5Tgn0oe|O?@u>`xLrc6 zOOjNE)VTGNC)95iSWFo(w>U7E6TQvw!GoHmwLG2BiZk(s_Up_t8U8H9jG?e_gS;qT%RRG7P2orRl|BbEDPbA5c@qI@I;5~l?d;p z!W2l8FpFY-#Q!8!#!;kFetMCYy?y2oqd!LA0IwN-jVE^}O9My7Wo81tDGft$qq{R! zO7mJvJ24fll_?YFQc90J`Q@MD-#i!^$DA?Z5V>IP`tTsnPDoUowP-GaggmxuFO~9b zl)dPt_UZ7u<*sj<_j9HPC@+xicD=pWaSui~a;(Fe!c`^@YHUZqG zPdOmNf&2qp*>DzF4C6$c7BvE%!69qW3^vQIACJ8k884(s!r4y)yNL!3x<2qv02DFH z^cRzbm<}p0(9m-W~$EwqcassK6S{BhCMW4 zy#lHG<9(=NcZ^Vx(Yn$iy}3PH`=_sf#Kx$mk!VJ$qX>vm+49vcw16w$yVH>kS8!wu z(@8-ZANapTZ44sqb)+yt^S~`PII#xn3`lu1WkGKu^M*qx_|F%1gS{I`eOf@FA&nlU zyaPH=HOqZoGdZQps_sQ>m?6v!Ef7%=>jG&n)iA4Rq-+PU34Evvq?0KD`*5)Kh_vbQ z`=BR){o?o-fw#r&s(NaR=G3-dauDIX>CF?i(l2hoH6<<|pP8F(m?|FT@N6eYvKT=< zNhWoP&_NsXa}y|3zC4*rK3p7Fm9=3&BN9}jUbrdxV@fl7=RN0SImc95r zvl#x+p7o1+#`RVH7CPK*PhF?EqKYCkC(W=pr@B?tcUK0EVWL=%r*KBVA_vOSVl@s^ zkAIx*&wk-``Hz8nUGm@iDoP5cal1$1*C~B_Td_YhO7-rltpENz`8O3)>-SQB2CW6l zU8o9KR2+Xfws@MO+&m45=Mk?^)I95donb^e*sA%vR5f$#XnL7yp+gu12+wAJI}{<`Yf8up z$$Cp2uTs-t&OcqA9tsK;1@d9_m?mgbA}!Trv-_68Jc<<9kt_@m>z(fV30{MFG9$6^ z8YB^k?q=j^r!uvEH1A4Q*!k7;IKM9QUZ}_n-tI3Ucd_Fz8T49A;IC(&w%`574K~>x zcCp)#HqV0Rulp#IP;V>~XYu!1a$QXKg+-s8$DW4-+x!B>FeX_d-vk6NX6%TP3XPfq zc7ZY##SWBzkFo~>A^SD+n1z6yX`sILukavw#G2<F^IbtGYFFA(;tzFw{Z06$bb%98f|s?Db4s0vFI