I have added a lot of changes from various ai sessions

This commit is contained in:
Moritz Graf 2026-07-01 11:01:01 +02:00
parent 68345648d9
commit 15e802bebc
37 changed files with 5704 additions and 252 deletions

View File

@ -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

View File

@ -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

View File

@ -18,4 +18,4 @@ spec:
solvers:
- http01:
ingress:
class: nginx
ingressClassName: traefik

View File

@ -18,4 +18,4 @@ spec:
solvers:
- http01:
ingress:
class: nginx
ingressClassName: traefik

View File

@ -0,0 +1,2 @@
# this token has been generated to enable AI to access HASS
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI2NDMyZTI1MDIzZjU0ZmFiYTU3OTc0ODYyMzg3M2ExZSIsImlhdCI6MTc4MjczMTY4NiwiZXhwIjoyMDk4MDkxNjg2fQ.u8kFHoYL2kdhVenfe3BiWcNkN_en3yn91MRGSVJvbmw

View File

@ -1,45 +0,0 @@
# kind: ConfigMap
# metadata:
# creationTimestamp: null
# name: rclone-dropbox-config
# namespace: mailu
# apiVersion: v1
# data:
# rclone.conf: |
# [dropbox]
# type = dropbox
# token = {"access_token":"17hvEArIh3cAAAAAAAGTXUfwkGdz8EC_rDcEseLmSRltQoUz_AVceuUewx0sGMQ1","token_type":"bearer","expiry":"0001-01-01T00:00:00Z"}
# ---
# apiVersion: batch/v1beta1
# kind: CronJob
# metadata:
# name: backup-mailu
# namespace: mailu
# spec:
# schedule: "30 4 * * *"
# jobTemplate:
# spec:
# template:
# spec:
# containers:
# - name: backup-mailu
# image: iptizer/swiss
# volumeMounts:
# - name: rclone-dropbox-config
# mountPath: /rclone.conf
# subPath: "rclone.conf"
# - mountPath: /data/
# name: data
# command:
# - "/bin/bash"
# - "-c"
# args:
# - "cd / && tar zcvf - /data/ | /usr/bin/rclone --config /rclone.conf -v rcat dropbox:server_backup/$( /bin/expr $( date +%m ) % 3 )_$( /bin/expr $( date +%j ) % 3 )_mailu.tar.gz"
# volumes:
# - name: rclone-dropbox-config
# configMap:
# name: rclone-dropbox-config
# - name: data
# persistentVolumeClaim:
# claimName: mailu-storage
# restartPolicy: Never

View File

@ -1,82 +0,0 @@
# DO NOT USE MASTER!!!!
#mailuVersion: master
# DO NOT USE MASTER!!!
hostnames:
- mail.moritzgraf.de
domain: moritzgraf.de
secretKey: fa5faeD9aegietaesahbiequ5Pe9au
subnet: 10.233.0.0/16
ingress:
annotations:
kubernetes.io/ingress.class: nginx
clamav:
enabled: false
front:
resources:
requests:
memory: 10Mi
cpu: 10m
limits:
memory: 200Mi
cpu: 200m
admin:
resources:
requests:
memory: 20Mi
cpu: 50m
limits:
memory: 500Mi
cpu: 500m
dovecot:
resources:
requests:
memory: 50Mi
cpu: 10m
limits:
memory: 500Mi
cpu: 500m
postfix:
resources:
requests:
memory: 1Gi
cpu: 10m
limits:
memory: 2Gi
cpu: 500m
redis:
resources:
requests:
memory: 20Mi
cpu: 10m
limits:
memory: 300Mi
cpu: 200m
roundcube:
enabled: true
logLevel: INFO
resources:
requests:
memory: 10Mi
cpu: 10m
limits:
memory: 200Mi
cpu: 200m
rspamd:
resources:
requests:
memory: 10Mi
cpu: 10m
limits:
memory: 2000Mi
cpu: 600m
certmanager:
issuerType: ClusterIssuer
issuerName: "letsencrypt-prod"
apiVersion: cert-manager.io/v1
persistence:
storageClass: openebs-hostpath

View File

@ -1,25 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: restore
name: restore
spec:
containers:
- image: iptizer/swiss
name: restore
resources: {}
volumeMounts:
- mountPath: /data/
name: data
command:
- "sleep"
- "3600"
dnsPolicy: ClusterFirst
restartPolicy: Never
volumes:
- name: data
persistentVolumeClaim:
claimName: mailu-storage
status: {}

Binary file not shown.

164
k8s/monitoring/AGENTS.md Normal file
View File

@ -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:<password>" 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-<name>
namespace: monitoring
labels:
grafana_dashboard: "1"
annotations:
grafana_dashboard_folder: "🏠 Home"
data:
<name>.json: |
{
"uid": "<unique-uid>",
"title": "<Dashboard Title>",
...
}
```
## InfluxDB Data Schema
Home Assistant writes sensor data to InfluxDB with this structure:
- **Measurement name** = unit of measurement (e.g., `°C`, `%`, `W`, `kWh`, `m³`, `hPa`, `km/h`, `mm/h`)
- **Fields**: `value` (numeric), plus metadata fields (`friendly_name_str`, `device_class_str`, `id_str`, etc.)
- **Tags**: `entity_id` (e.g., `home_wohnzimmer_temperature`**NOTE: no `sensor.` prefix!**), `domain` (e.g., `sensor`)
### Flux Query Pattern (MUST follow this exactly)
**Exact match on entity_id:**
```flux
from(bucket: "default")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r["_measurement"] == "°C")
|> filter(fn: (r) => r["_field"] == "value")
|> filter(fn: (r) => r.entity_id == "home_wandthermostat_bad_temperature")
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|> yield(name: "mean")
```
**Regex match on entity_id** (use `=~` operator with `/pattern/`):
```flux
from(bucket: "default")
|> range(start: v.timeRangeStart, stop: v.timeRangeStop)
|> filter(fn: (r) => r["_measurement"] == "°C")
|> filter(fn: (r) => r["_field"] == "value")
|> filter(fn: (r) => r.entity_id =~ /^home_/)
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|> yield(name: "mean")
```
**Critical rules:**
- Always use bracket notation: `r["_measurement"]`, `r["_field"]` (fields starting with `_`)
- Always use dynamic time: `range(start: v.timeRangeStart, stop: v.timeRangeStop)` and `aggregateWindow(every: v.windowPeriod, ...)`
- Always end with `|> yield(name: "mean")`
- **entity_id in InfluxDB does NOT have the `sensor.` prefix** — use `home_wohnzimmer_temperature`, NOT `sensor.home_wohnzimmer_temperature`
- For "current value" stat panels, use `range(start: -15m)` with `last()` instead of aggregateWindow
- Regex operator is `=~` with `/pattern/` syntax (e.g., `=~ /^home_/` matches all entities starting with `home_`)
## Common Pitfalls (Lessons Learned)
1. **Entity IDs have no `sensor.` prefix in InfluxDB** — HA stores domain separately. Use `home_wohnzimmer_temperature`, not `sensor.home_wohnzimmer_temperature`. This is the #1 cause of "no data".
2. **Dashboard JSON must NOT be wrapped in `"dashboard"` key** — Grafana file provisioning expects top-level properties (`uid`, `title`, `panels`, etc.). The `"dashboard"` wrapper is only used in the HTTP API.
3. **Datasource reference must use actual UID** — Discover via `kubectl exec` + Grafana API. Current UID: `P2AB959DC95E5519F`.
4. **Don't add `uid` to helm datasource config** — Adding `uid` to existing datasources in `additionalDataSources` causes Grafana provisioning to fail with "data source not found". Let UIDs be auto-generated.
5. **Always use bracket notation for `_measurement` and `_field`** — Use `r["_measurement"]` and `r["_field"]`, not dot notation. Fields starting with underscore may not resolve with dot notation in Flux.
6. **Always use Grafana time variables** — Never hardcode `range(start: -24h)` or `aggregateWindow(every: 5m)`. Always use `v.timeRangeStart`, `v.timeRangeStop`, `v.windowPeriod`.
7. **Always add `yield(name: "mean")`** — Required for Grafana to properly parse Flux results.
8. **InfluxDB has multiple fields per measurement** — Always filter `r["_field"] == "value"` to get numeric data.
9. **Sidecar container name is `grafana-sc-dashboard`** (singular), not `grafana-sc-dashboards`.
10. **Use Wandthermostat entities for temperature/humidity** — Query HA to filter by device type. Wandthermostat entities have `wandthermostat` or `thermostat` in their entity_id.
## 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

View File

@ -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 <HA_TOKEN>" \
https://hass.moritzgraf.de/api/states | \
jq '[.[] | select(.attributes.unit_of_measurement != null) | {
entity_id,
unit: .attributes.unit_of_measurement,
friendly_name: (.attributes.friendly_name // ""),
device_class: (.attributes.device_class // "")
}] | group_by(.unit)'
```
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 <INFLUXDB_TOKEN>" \
-H "Content-Type: application/vnd.flux" \
-d 'from(bucket: "default")
|> range(start: -6mo)
|> keep(columns: ["_measurement"])
|> distinct()
|> sort()'
# For a specific measurement, get fields and tags
curl -s "http://localhost:8086/api/v2/query?org=influxdata" \
-H "Authorization: Token <INFLUXDB_TOKEN>" \
-H "Content-Type: application/vnd.flux" \
-d 'import "influxdata/influxdb/schema"
schema.measurementFieldKeys(bucket: "default", measurement: "°C")'
```
Or use: `./scripts/discover-influxdb.sh`
#### 1c. Verify Data Exists
Always test a sample query before building dashboards:
```bash
curl -s "http://localhost:8086/api/v2/query?org=influxdata" \
-H "Authorization: Token <INFLUXDB_TOKEN>" \
-H "Content-Type: application/vnd.flux" \
-d 'from(bucket: "default")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "°C" and r._field == "value")
|> limit(n: 5)'
```
### Step 2: Discover Grafana Datasource UIDs
Datasource UIDs change when Grafana is redeployed. Always discover the current UIDs:
```bash
kubectl -n monitoring exec deploy/prometheus-operator-grafana -c grafana -- \
curl -s -u "admin:<GRAFANA_PASSWORD>" \
http://localhost:3000/api/datasources | \
jq '.[] | {name, uid, type}'
```
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 <HA_TOKEN>" \
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-<name>
namespace: monitoring
labels:
grafana_dashboard: "1"
annotations:
grafana_dashboard_folder: "🏠 Home"
data:
<name>.json: |
{
"uid": "<unique-uid>",
"title": "<Dashboard Title>",
"tags": ["home", "<category>", "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"] == "<UNIT>" and r["_field"] == "value")
|> filter(fn: (r) => r.entity_id == "<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"] == "<UNIT>" and r["_field"] == "value")
|> filter(fn: (r) => r.entity_id =~ /^home_/)
|> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)
|> yield(name: "mean")
```
**Current value** (for stat panels showing last reading):
```flux
from(bucket: "default")
|> range(start: -15m)
|> filter(fn: (r) => r["_measurement"] == "<UNIT>" and r["_field"] == "value")
|> filter(fn: (r) => r.entity_id == "<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-<name>.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-<name>.yaml`
4. The sidecar will detect the change and reload within ~60 seconds
5. Verify in Grafana UI
**Never edit dashboards in the Grafana UI** — they are `editable: false`. All changes must go through the YAML files.
---
## Removing a Dashboard
```bash
kubectl delete configmap -n monitoring grafana-dashboard-<name>
```
The sidecar will remove it from Grafana within ~60 seconds.
---
## Pitfalls Encountered in This Conversation
### Pitfall 1: Entity IDs lack `sensor.` prefix in InfluxDB
**Problem**: Used `sensor.home_wohnzimmer_temperature` in queries — no data appeared. Home Assistant stores the domain (`sensor`) in a separate `domain` tag, not in `entity_id`.
**Fix**: Use `home_wohnzimmer_temperature` (without prefix). Always query InfluxDB directly to verify entity_id format before building queries.
### Pitfall 2: Hardcoded time ranges
**Problem**: Used `range(start: -24h)` and `aggregateWindow(every: 5m)` — dashboard time picker had no effect, data not loaded properly.
**Fix**: Always use `v.timeRangeStart`, `v.timeRangeStop`, `v.windowPeriod`. These Grafana variables adapt to the dashboard time picker.
### Pitfall 3: Missing `yield()`
**Problem**: Flux queries missing `yield(name: "mean")` — Grafana couldn't parse results.
**Fix**: Always end Flux queries with `|> yield(name: "mean")`.
### Pitfall 4: Dot notation for underscore fields
**Problem**: Used `r._measurement` and `r._field` — Flux may not resolve fields starting with `_` via dot notation.
**Fix**: Use bracket notation: `r["_measurement"]`, `r["_field"]`.
### Pitfall 5: Datasource UID vs Name
**Problem**: Used datasource name as UID (`"uid": "InfluxDB Home Assistant"`). Dashboards loaded but showed "no data".
**Fix**: Discover actual UID via `kubectl exec` + Grafana API. Current: `P2AB959DC95E5519F`.
### Pitfall 6: `"dashboard"` JSON Wrapper
**Problem**: Dashboard JSON wrapped in `{"dashboard": {...}}`. Grafana showed "Dashboard title cannot be empty".
**Fix**: Top-level properties only. The `"dashboard"` wrapper is for HTTP API, not file provisioning.
### Pitfall 7: Adding `uid` to Helm Datasource Config
**Problem**: Added `uid: influxdb-ha-flux` to helm values — Grafana pod crashed.
**Fix**: Let Grafana auto-generate UIDs. Never add `uid` to existing datasources.
### Pitfall 8: Multi-entity panels using `or` in filter
**Problem**: Used `filter(fn: (r) => r.entity_id == "A" or r.entity_id == "B")` — Flux `or` inside a single filter call doesn't reliably work.
**Fix**: Use regex: `filter(fn: (r) => r.entity_id =~ /^home_/)` for matching multiple entities.
### Pitfall 9: Wrong Sensor Selection
**Problem**: Used raw temperature sensors instead of Wandthermostat sensors.
**Fix**: Query HA to filter entities with `wandthermostat` or `thermostat` in entity_id.
### Pitfall 10: Sidecar Container Name
**Problem**: `kubectl logs -c grafana-sc-dashboards` (plural).
**Fix**: Container name is `grafana-sc-dashboard` (singular).
---
## Dashboard Design Guidelines
1. **Use semantic thresholds with colors**: Blue=cold, green=comfort, orange=warm, red=hot for temperature; reverse for grid import (green=low, red=high).
2. **Use Stat panels with sparklines** for current values — they show the 24h mini-trend.
3. **Use BarGauge for day comparisons** (today vs yesterday vs avg).
4. **Use Gauge for ranged values** (battery SoC 0-100%, cloud cover 0-100%).
5. **Color-code panel backgrounds** with `"colorMode": "background"` for at-a-glance status.
6. **Set `"editable": false`** for strict GitOps.
7. **Use emoji in dashboard and panel titles** for visual appeal (🏠 ⚡ 🔥 🌡️ 💧).
8. **Add `"auto-generated"` tag** to distinguish from manually created dashboards.
9. **Include calcs in legend** (`mean`, `max`, `min`, `lastNotNull`) for quick statistics.
10. **Use `"spanNulls": true`** in timeseries to handle gaps in sensor data.
---
## InfluxDB Measurement Reference
| Measurement | Unit | Typical Entities |
|-------------|------|------------------|
| `°C` | celsius | Wandthermostat temperatures, outdoor temp, battery temp |
| `%` | percent | Humidity, battery levels, heating valve position, cloud cover |
| `W` | watt | Solar production, self-consumption, grid import/export, house consumption |
| `kWh` | kilowatt-hour | Daily energy totals (production, consumption, import, export) |
| `m³` | cubic meter | Gas volume counter |
| `m³/h` | cubic meter/hour | Current gas flow rate |
| `hPa` | hectopascal | Barometric pressure |
| `km/h` | kilometer/hour | Wind speed |
| `mm/h` | millimeter/hour | Precipitation rate |
| `EUR` | euro | Gas cost, electricity cost |

90
k8s/monitoring/README.md Normal file
View File

@ -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-<name>
namespace: monitoring
labels:
grafana_dashboard: "1"
annotations:
grafana_dashboard_folder: "<Folder Name>"
data:
<name>.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-<name>.yaml`
4. Verify the dashboard appears in Grafana at https://grafana.haumdaucher.de
## Removing a Dashboard
```bash
kubectl delete configmap -n monitoring grafana-dashboard-<name>
```
The sidecar will automatically remove the dashboard from Grafana within ~60 seconds.

View File

@ -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"}]}]}'

View File

@ -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"}]}]}'

View File

@ -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"}]}]}'

View File

@ -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"}]}]}'

View File

@ -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

View File

@ -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 ==="

View File

@ -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 ==="

View File

@ -1,10 +0,0 @@
rootUser:
password: cah6eepai1Saepho8tho
forcePassword: true
db:
name: tt-rss
user: tt-rss
password: AhS5cai7oph3ahN4Aile
forcePassword: true
replication:
enabled: false

View File

@ -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"

View File

@ -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

Binary file not shown.

View File

@ -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

BIN
k8s/velero/gdrive_sa.json.secret Executable file

Binary file not shown.

View File

@ -0,0 +1,39 @@
apiVersion: batch/v1
kind: CronJob
metadata:
name: gdrive-sync
namespace: velero
spec:
schedule: "0 5 * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: default
containers:
- name: gdrive-sync
image: rclone/rclone:latest
volumeMounts:
- name: rclone-config
mountPath: /rclone.secret
subPath: "rclone.secret"
- name: gdrive-sa-key
mountPath: /gdrive_sa.json.secret
subPath: "gdrive_sa.json.secret"
command:
- "rclone"
args:
- "--config"
- "/rclone.secret"
- "-v"
- "sync"
- "minio:backup/"
- "gdrivemoritz:"
volumes:
- name: rclone-config
secret:
secretName: rclone-config
- name: gdrive-sa-key
secret:
secretName: gdrive-sa-key
restartPolicy: Never

View File

@ -0,0 +1,36 @@
apiVersion: batch/v1
kind: Job
metadata:
name: gdrive-sync-manual
namespace: velero
spec:
template:
spec:
serviceAccountName: default
containers:
- name: gdrive-sync
image: rclone/rclone:latest
volumeMounts:
- name: rclone-config
mountPath: /rclone.secret
subPath: "rclone.secret"
- name: gdrive-sa-key
mountPath: /gdrive_sa.json.secret
subPath: "gdrive_sa.json.secret"
command:
- "rclone"
args:
- "--config"
- "/rclone.secret"
- "-v"
- "sync"
- "minio:backup/"
- "gdrivemoritz:"
volumes:
- name: rclone-config
secret:
secretName: rclone-config
- name: gdrive-sa-key
secret:
secretName: gdrive-sa-key
restartPolicy: Never

Binary file not shown.

View File

@ -0,0 +1,828 @@
##
## Configuration settings related to Velero installation namespace
##
# Labels settings in namespace
namespace:
labels: {}
# Enforce Pod Security Standards with Namespace Labels
# https://kubernetes.io/docs/tasks/configure-pod-container/enforce-standards-namespace-labels/
# pod-security.kubernetes.io/enforce: privileged
# pod-security.kubernetes.io/enforce-version: latest
# pod-security.kubernetes.io/audit: privileged
# pod-security.kubernetes.io/audit-version: latest
# pod-security.kubernetes.io/warn: privileged
# pod-security.kubernetes.io/warn-version: latest
##
## End of namespace-related settings.
##
##
## Configuration settings that directly affect the Velero deployment YAML.
##
# Details of the container image to use in the Velero deployment & daemonset (if
# enabling node-agent). Required.
image:
repository: docker.io/velero/velero
tag: v1.18.1
# Digest value example: sha256:73266e6bd7e2fe3f65fb20677d36c4a8df76d417d9ba76bd9932e0d983a776ec.
# If used, it will take precedence over the image.tag.
# digest:
pullPolicy: IfNotPresent
# One or more secrets to be used when pulling images
imagePullSecrets: []
# - registrySecretName
nameOverride: ""
fullnameOverride: ""
# Annotations to add to the Velero deployment's. Optional.
#
# If you are using reloader use the following annotation with your VELERO_SECRET_NAME
annotations: {}
# secret.reloader.stakater.com/reload: "<VELERO_SECRET_NAME>"
# 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::<AWS_ACCOUNT_ID>:role/<VELERO_ROLE_NAME>"
# 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 clusters 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 clusters 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=<REDACTED>
# aws_secret_access_key=<REDACTED>
# additional key/value pairs to be used as environment variables such as "DIGITALOCEAN_TOKEN: <your-key>". 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.
##

Binary file not shown.

View File

@ -1,4 +1,4 @@
# This file is maintained automatically by "tofu init".
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/ovh/ovh" {
@ -21,3 +21,84 @@ provider "registry.opentofu.org/ovh/ovh" {
"zh:f953ef37e4a1b3cac22f851c3269e7376704018ffa642b4768869cc227ffccc9",
]
}
provider "registry.terraform.io/hashicorp/google" {
version = "5.45.2"
constraints = "~> 5.0"
hashes = [
"h1:y2hf6zus1eKA5vpAfoyYkNBKDBQTVqmx4OVh3iRBaRo=",
"zh:0d09c8f20b556305192cdbe0efa6d333ceebba963a8ba91f9f1714b5a20c4b7a",
"zh:117143fc91be407874568df416b938a6896f94cb873f26bba279cedab646a804",
"zh:16ccf77d18dd2c5ef9c0625f9cf546ebdf3213c0a452f432204c69feed55081e",
"zh:3e555cf22a570a4bd247964671f421ed7517970cd9765ceb46f335edc2c6f392",
"zh:688bd5b05a75124da7ae6e885b2b92bd29f4261808b2b78bd5f51f525c1052ca",
"zh:6db3ef37a05010d82900bfffb3261c59a0c247e0692049cb3eb8c2ef16c9d7bf",
"zh:70316fde75f6a15d72749f66d994ccbdde5f5ed4311b6d06b99850f698c9bbf9",
"zh:84b8e583771a4f2bd514e519d98ed7fd28dce5efe0634e973170e1cfb5556fb4",
"zh:9d4b8ef0a9b6677935c604d94495042e68ff5489932cfd1ec41052e094a279d3",
"zh:a2089dd9bd825c107b148dd12d6b286f71aa37dfd4ca9c35157f2dcba7bc19d8",
"zh:f03d795c0fd9721e59839255ee7ba7414173017dc530b4ce566daf3802a0d6dd",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
]
}
provider "registry.terraform.io/hashicorp/local" {
version = "2.9.0"
constraints = "~> 2.0"
hashes = [
"h1:oF4vw1rikPMqQtMMBoUjc/pERp50DlWwO+C2I6/sNI8=",
"zh:0baa4566cf77f1ff52f4293d1c8536202dd23edc197c3196413a28343c3ac3a0",
"zh:16b5559c3c07088ddad11a9bb9e9c0799999363c2958e9a5be2bcbbf2cd9ca64",
"zh:197c79015a10d1cce904a8ea722cbc750c42aeae2da53f44a6a0751d9fd1aa90",
"zh:29d0b03e5343a80677ebfeb2e2c31cbe4b1f65e736e53417454a4277fec2544c",
"zh:4896bfa6cf1d2fd562b47ef2e87f47862ae92a04f8ad5d764380f0c6653473b8",
"zh:531f8529cbca49f681883e57761a05a8398afaef6d1ab0d205d26bf12f4428e8",
"zh:6aaf5011d83161c86d2bfb80c0923ec934e578288758da2f37acb7aec129004b",
"zh:7430275253d3d3c40aa6179e0ec0d63212874dbbc06c5a51b9d07ec590f9756c",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:be17dc611e95e26cdf6cad79dfccf1064f0e32032a2efeb939a9bbe7fb1cbfe9",
"zh:f0e3b0aa644202e1d79d2000dca91f6019425da71e9800fa23f27e51c034f195",
"zh:f62bae4519e4ead49182ddc8afe8cf61e2a4c3ba3973b0fbba967736a2696aa3",
"zh:fcafa360a5b0b96244f26f4e3a6d642b716a376557142c2442ff2fb12d11da18",
]
}
provider "registry.terraform.io/hashicorp/random" {
version = "3.9.0"
hashes = [
"h1:o0s5Mk9NXMP60nlheO1r0LsDGGratFb3oL0t7bD2QnM=",
"zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1",
"zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea",
"zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f",
"zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0",
"zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61",
"zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc",
"zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e",
"zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef",
"zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b",
"zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257",
"zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04",
]
}
provider "registry.terraform.io/ovh/ovh" {
version = "2.15.0"
hashes = [
"h1:Y6pQdKjn1OR4wNlxRpy+nI7Dvap/bKzFb68fynMxTmI=",
"zh:22d361f99caa70b07532fce13208fce8b84072cf83eaf6ddfcd9096df88b57ea",
"zh:497f88936649d3c352fdb639ba5ebdabfec8ec7a59d67bf619013e3afbbaefb4",
"zh:62e49208e2950daf366b396694dc7de5bba31a8b7a10aac7c021f2c78fd64f70",
"zh:97e06f7494706df30c136f4809314d92a25129738cf080e22c5c9e47ec70c4cf",
"zh:be23cf1ecb2eb76c60dad72eb4f9d005c6184cae6e2cf9f13b0462030133644c",
"zh:bf60c1af6db5ca51ff4372fee8e64a21462134d281ae9cb3968ca5b209338c4b",
"zh:c9e5ad5c063249786595c5b6c3f59791bd21f5157ff0572a4ed2d52ca8141c50",
"zh:d2bf2603741df6fc29a88384f7026fb8b0e982b8a270796e672037d287b41e83",
"zh:d944de8032fc01d8488087755bf7fb813e7b8c259e23a7b68ed654905ad19b56",
"zh:dcf2a7620abac38e9d8f0a88f2a79de0fddd917ba0facba1108f6581eb1b7f6f",
"zh:e7c7905954f69f0d5b6cf9bd87572101924e128aa33311856dbbfedd711361d2",
"zh:f6a9977d744ed879a0b3c28a5a800b6a1f49aabe8d71e87ca1d02fe6a77a31ba",
"zh:fd0e48de826df04e18489d33b0ff35bd776cb6e7704188f67dad66cbb041e5af",
]
}

56
terraform/gcp_backups.tf Normal file
View File

@ -0,0 +1,56 @@
variable "gcp_project_id" {
type = string
description = "The GCP Project ID where the Service Account should be created. If empty, a new project named 'velero-backup-xxxx' will be created."
default = ""
}
resource "random_id" "project_suffix" {
byte_length = 4
}
resource "google_project" "backup_project" {
count = var.gcp_project_id == "" ? 1 : 0
name = "velero-backup"
project_id = "velero-backup-${random_id.project_suffix.hex}"
}
locals {
project_id = var.gcp_project_id == "" ? google_project.backup_project[0].project_id : var.gcp_project_id
}
resource "google_project_service" "drive_api" {
project = local.project_id
service = "drive.googleapis.com"
disable_on_destroy = false
}
resource "google_project_organization_policy" "allow_key_creation" {
project = local.project_id
constraint = "iam.disableServiceAccountKeyCreation"
boolean_policy {
enforced = false
}
}
resource "google_service_account" "velero_backup_sa" {
project = local.project_id
account_id = "velero-backup-sa"
display_name = "Velero GDrive Backup Service Account"
depends_on = [google_project_service.drive_api]
}
resource "google_service_account_key" "velero_backup_sa_key" {
service_account_id = google_service_account.velero_backup_sa.name
depends_on = [google_project_organization_policy.allow_key_creation]
}
resource "local_file" "gdrive_sa_key_file" {
content = base64decode(google_service_account_key.velero_backup_sa_key.private_key)
filename = "${path.module}/../k8s/velero/gdrive_sa.json.secret"
}
output "velero_backup_sa_email" {
value = google_service_account.velero_backup_sa.email
description = "The email address of the Google Cloud Service Account. Share your Google Drive backup folder with this email address and grant it Editor access."
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,14 @@ terraform {
ovh = {
source = "ovh/ovh"
}
google = {
source = "hashicorp/google"
version = "~> 5.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.0"
}
}
required_version = ">= 0.13"
}