diff --git a/dev/README.md b/dev/README.md index 3346aaf..ba53856 100644 --- a/dev/README.md +++ b/dev/README.md @@ -70,6 +70,49 @@ OMNI_PCA_FIXTURE_KEY=0xC1A280B2 # or --pca-key on the command line buttons, programs, model byte, and firmware version from the file — everything the HA integration reads at discovery time. +## Time-series & dashboards + +`docker compose up -d` also brings up **InfluxDB v2** (port 8086) and +**Grafana** (port 3000). Open Grafana at +(login: `admin` / `$GRAFANA_PASSWORD` from `.env`) — the **Omni Pro II +— Panel Overview** dashboard loads automatically, pre-provisioned from +[`../grafana/`](../grafana/), the shipping bundle. + +To wire HA → InfluxDB, append this block to `ha-config/configuration.yaml` +(the directory is gitignored because it contains HA auth/state; the +block lives in `../grafana/ha-snippet.yaml` for production users): + +```yaml +influxdb: + api_version: 2 + host: influxdb + port: 8086 + ssl: false + verify_ssl: false + token: dev-token-omnipca-9472-fixed-for-dev-stack + organization: omni-pca + bucket: ha + precision: s + tags_attributes: [event_type, event_class] + include: + domains: [alarm_control_panel, binary_sensor, climate, event, light, sensor, switch] + entity_globs: ["*omni*"] +``` + +Restart HA (`docker compose restart homeassistant`) after editing. +Within 30 seconds, panels start populating with live data. + +The dashboard JSON in `../grafana/provisioning/dashboards/` is the +source of truth; edits in the Grafana UI don't persist (provisioned +dashboards are read-only). Iterate by editing the JSON and running +`docker compose restart grafana` — the provisioner picks up changes +within ~30s. + +To exercise dashboard panels against the mock, trigger HA actions +(arm an area, toggle a light): the mock pushes the resulting +`SystemEvent` back to HA, which ships it to InfluxDB, which Grafana +queries. Each step takes <1s. + ## Notes - The HA container mounts `../custom_components/omni_pca/` read-only, so diff --git a/dev/artifacts/screenshots/2026-05-17/grafana-iter-final.png b/dev/artifacts/screenshots/2026-05-17/grafana-iter-final.png new file mode 100644 index 0000000..b653872 Binary files /dev/null and b/dev/artifacts/screenshots/2026-05-17/grafana-iter-final.png differ diff --git a/dev/artifacts/screenshots/2026-05-17/grafana-real-panel-final.png b/dev/artifacts/screenshots/2026-05-17/grafana-real-panel-final.png new file mode 100644 index 0000000..4d40573 Binary files /dev/null and b/dev/artifacts/screenshots/2026-05-17/grafana-real-panel-final.png differ diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index dd2b681..29bacce 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -102,6 +102,74 @@ services: pip install --quiet --no-deps --upgrade /opt/omni-pca-src exec /init + # InfluxDB v2 + Grafana stack — kept inline rather than `extends:`-ing + # ../grafana/docker-compose.yml so this file stays self-contained and + # the named volumes get scoped to this compose project. The bundle + # compose stays the canonical ship-to-users version; we share its + # provisioning files via the volume mount on the grafana service. + influxdb: + image: influxdb:2.7-alpine + container_name: omni-pca-dev-influxdb + restart: unless-stopped + environment: + DOCKER_INFLUXDB_INIT_MODE: setup + DOCKER_INFLUXDB_INIT_USERNAME: ${INFLUX_USERNAME:-admin} + DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUX_PASSWORD} + DOCKER_INFLUXDB_INIT_ORG: omni-pca + DOCKER_INFLUXDB_INIT_BUCKET: ha + DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUX_TOKEN} + DOCKER_INFLUXDB_INIT_RETENTION: 30d + volumes: + - influxdb-data:/var/lib/influxdb2 + - influxdb-config:/etc/influxdb2 + ports: + - "8086:8086" + networks: + - default + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8086/health"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 10s + + grafana: + image: grafana/grafana:11.4.0 + container_name: omni-pca-dev-grafana + restart: unless-stopped + depends_on: + influxdb: + condition: service_healthy + environment: + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD} + GF_AUTH_ANONYMOUS_ENABLED: "false" + GF_USERS_ALLOW_SIGN_UP: "false" + GF_LOG_LEVEL: warn + INFLUX_URL: http://influxdb:8086 + INFLUX_TOKEN: ${INFLUX_TOKEN} + volumes: + - grafana-data:/var/lib/grafana + - ../grafana/provisioning:/etc/grafana/provisioning:ro + ports: + - "3000:3000" + networks: + - default + - caddy + labels: + caddy: grafana-omni.juliet.warehack.ing + caddy.reverse_proxy: "{{upstreams 3000}}" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/health || exit 1"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 15s + +volumes: + influxdb-data: + influxdb-config: + grafana-data: + networks: caddy: external: true diff --git a/grafana/.env.example b/grafana/.env.example new file mode 100644 index 0000000..5d657d0 --- /dev/null +++ b/grafana/.env.example @@ -0,0 +1,19 @@ +# Copy to .env and fill in. Both files in this directory load .env +# automatically via docker compose; ./env.example is committed, .env +# is gitignored. + +# InfluxDB v2 admin user (created on first boot). +INFLUX_USERNAME=admin +INFLUX_PASSWORD=change-me-strong-password-here + +# Admin token used by Home Assistant (writes) and Grafana (reads). +# Generate one with: openssl rand -hex 32 +INFLUX_TOKEN=replace-with-a-real-token-from-openssl-rand-hex-32 + +# Grafana admin password (UI login as "admin"/this value). +GRAFANA_PASSWORD=change-me-too + +# Public hostnames if you're putting either service behind a reverse +# proxy. Leave blank for localhost-only access. +INFLUX_PUBLIC_HOST= +GRAFANA_PUBLIC_HOST= diff --git a/grafana/README.md b/grafana/README.md new file mode 100644 index 0000000..7a960d2 --- /dev/null +++ b/grafana/README.md @@ -0,0 +1,129 @@ +# Grafana dashboard for omni_pca + +InfluxDB v2 + Grafana stack pre-provisioned to visualise an HAI/Leviton +Omni Pro II panel via the `omni_pca` Home Assistant integration. +Drop-in for any existing HA install — no integration changes required. + +![Dashboard overview](../dev/artifacts/screenshots/2026-05-17/grafana-dashboard-final.png) + +## What you get + +One dashboard, four rows: + +- **System health** — AC power, backup battery, system trouble, event count (24h). +- **Security** — area arming state timeline, recent push-event log, zone trip timeline. +- **Climate** — per-thermostat current temperatures + setpoints, HVAC mode timeline. +- **Activity** — event rate by typed event class, unit brightness heatmap. + +Data flows: HA entity state → HA's `influxdb:` integration → InfluxDB +v2 bucket → Grafana Flux queries → dashboard panels. + +## Quick start (~5 minutes) + +```bash +cd grafana/ +cp .env.example .env +# Edit .env — set strong INFLUX_PASSWORD, INFLUX_TOKEN, GRAFANA_PASSWORD. +# Generate the token with: openssl rand -hex 32 + +docker compose up -d +``` + +Wait ~30 seconds. InfluxDB does first-boot setup (creates the +`omni-pca` org, `ha` bucket, admin token); Grafana then auto-provisions +the InfluxDB datasource and the dashboard. + +Then add the influxdb integration to your Home Assistant config: + +```bash +# Paste the contents of ha-snippet.yaml into your configuration.yaml. +# Add `influxdb_token: ` to your secrets.yaml. +# Restart HA. +``` + +Within ~30 seconds you should see real-time data populating the +dashboard at (login: `admin` / your +`GRAFANA_PASSWORD`). + +## Networking notes + +The default `ha-snippet.yaml` assumes HA and InfluxDB sit on the same +docker network and HA can reach `influxdb:8086` by container name. +Three common variants: + +| HA layout | `host:` value | +|---|---| +| Same compose stack as this bundle | `influxdb` | +| HA on the host, InfluxDB in docker | `host.docker.internal` or your LAN IP | +| Different machine entirely | the InfluxDB host's IP / FQDN | + +If you put either service behind a reverse proxy with TLS, set `ssl: +true` in the HA snippet and supply the public hostname. + +## Iterating on the dashboard + +The dashboard JSON at `provisioning/dashboards/omni-pro-ii.json` is +loaded read-only by the provisioner. To change it: + +1. Edit the JSON directly, then `docker compose restart grafana` + (provisioner picks up changes within ~30s). +2. Or use the Grafana UI to experiment, then **Dashboard settings → + JSON Model → Save to file** and overwrite the file in this repo. + +Provisioned dashboards can't be saved from the UI by design — this is +intentional, so the file on disk stays the source of truth. + +## Extending coverage + +The bundle is scoped to the `omni_pca` entity surface via the +`entity_globs: ["*omni*"]` filter in `ha-snippet.yaml`. Drop that +filter (or add a second `include:` block) if you want to graph other +HA entities alongside omni data — Grafana's datasource is general +InfluxDB v2, nothing in the dashboard JSON hard-codes omni-specific +field names beyond what you'd want to scope to anyway. + +A few panel ideas not yet shipped: + +- Alarm activation drill-down — filter the event log to + `event_type == "alarm_activated"` and show the `alarm_type` + (Burglary / Fire / Auxiliary / …) distribution. +- Zone trip rate histogram — `binary_sensor` zone changes per zone + per hour, useful for spotting flaky sensors. +- Comm health — track integration coordinator state via the panel + device's "Comm error" attribute. + +## Files in this bundle + +| File | Purpose | +|---|---| +| `docker-compose.yml` | InfluxDB v2 + Grafana services | +| `.env.example` | Required environment template | +| `ha-snippet.yaml` | HA configuration.yaml additions | +| `provisioning/datasources/influxdb.yml` | Auto-wires the datasource | +| `provisioning/dashboards/dashboards.yml` | Provisioner config | +| `provisioning/dashboards/omni-pro-ii.json` | The dashboard JSON | + +## Troubleshooting + +**"No data" in panels.** Most panels need either continuous state +updates (climate, security) or push events (event-driven panels). +Verify HA is shipping data: + +```bash +docker exec -it omni-pca-influxdb influx query \ + 'from(bucket:"ha") |> range(start:-5m) |> limit(n:5)' \ + --token "$INFLUX_TOKEN" --org omni-pca +``` + +If this returns rows, the pipeline is healthy and panels will fill in +as the panel does interesting things. If it's empty, check HA logs for +`[homeassistant.components.influxdb]` errors. + +**Dashboard didn't auto-load.** Check `docker logs omni-pca-grafana +2>&1 | grep -i provision` — provisioner errors show up there. + +**Stat panels show duplicate values.** Your HA has multiple entities +matching the regex (e.g. `omni_pro_ii_ac_power` AND +`omni_pro_ii_ac_power_2` from prior integration reloads). Clean up the +duplicates in HA's entity registry, or tighten the filter in the +dashboard JSON. diff --git a/grafana/docker-compose.yml b/grafana/docker-compose.yml new file mode 100644 index 0000000..7325b95 --- /dev/null +++ b/grafana/docker-compose.yml @@ -0,0 +1,69 @@ +# Self-contained InfluxDB v2 + Grafana stack for the omni_pca +# integration. Pre-provisioned with the InfluxDB datasource and the +# "Omni Pro II — Panel Overview" dashboard. +# +# Usage: +# cp .env.example .env && edit the secrets && docker compose up -d +# open http://localhost:3000 (admin / $GRAFANA_PASSWORD) +# +# Then paste the contents of ha-snippet.yaml into your HA +# configuration.yaml (and add `influxdb_token: $INFLUX_TOKEN` to +# secrets.yaml). Restart HA. Within 30s the dashboard's panels start +# filling in. + +services: + influxdb: + image: influxdb:2.7-alpine + container_name: omni-pca-influxdb + restart: unless-stopped + environment: + DOCKER_INFLUXDB_INIT_MODE: setup + DOCKER_INFLUXDB_INIT_USERNAME: ${INFLUX_USERNAME:-admin} + DOCKER_INFLUXDB_INIT_PASSWORD: ${INFLUX_PASSWORD} + DOCKER_INFLUXDB_INIT_ORG: omni-pca + DOCKER_INFLUXDB_INIT_BUCKET: ha + DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${INFLUX_TOKEN} + DOCKER_INFLUXDB_INIT_RETENTION: 30d + volumes: + - influxdb-data:/var/lib/influxdb2 + - influxdb-config:/etc/influxdb2 + ports: + - "8086:8086" + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8086/health"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 10s + + grafana: + image: grafana/grafana:11.4.0 + container_name: omni-pca-grafana + restart: unless-stopped + depends_on: + influxdb: + condition: service_healthy + environment: + GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD} + GF_AUTH_ANONYMOUS_ENABLED: "false" + GF_USERS_ALLOW_SIGN_UP: "false" + GF_LOG_LEVEL: warn + # Consumed by ./provisioning/datasources/influxdb.yml + INFLUX_URL: http://influxdb:8086 + INFLUX_TOKEN: ${INFLUX_TOKEN} + volumes: + - grafana-data:/var/lib/grafana + - ./provisioning:/etc/grafana/provisioning:ro + ports: + - "3000:3000" + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/health || exit 1"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 15s + +volumes: + influxdb-data: + influxdb-config: + grafana-data: diff --git a/grafana/ha-snippet.yaml b/grafana/ha-snippet.yaml new file mode 100644 index 0000000..aaeef31 --- /dev/null +++ b/grafana/ha-snippet.yaml @@ -0,0 +1,51 @@ +# Paste this block into your Home Assistant configuration.yaml. +# +# Prerequisites: +# 1. The grafana stack from this directory is running: +# cd grafana/ && cp .env.example .env && docker compose up -d +# 2. Your HA instance can reach the influxdb container on port 8086. +# Common patterns: +# - HA and InfluxDB on the same compose stack: use host=influxdb +# - HA and InfluxDB on different hosts: use host= +# - HA on the host network, InfluxDB in docker: use +# host=host.docker.internal or the host's LAN IP +# 3. Add `influxdb_token: ` to your +# secrets.yaml. Restart HA after editing both files. +# +# What this ships: +# - All state changes from omni_pca entities (alarm_control_panel, +# binary_sensor, climate, event, light, sensor, switch). +# - Event entity attributes carried as fields, including the typed +# event_class and event_data payload — so Flux queries can filter +# by alarm_type, zone_index, etc. +# +# Adjust the entity_globs filter if you also want non-omni entities in +# the dashboard, or tighten it further to scope by area / device. + +influxdb: + api_version: 2 + host: influxdb # change to match your network layout + port: 8086 + ssl: false + verify_ssl: false + token: !secret influxdb_token + organization: omni-pca + bucket: ha + precision: s + + # Tag the typed event kind so Flux queries can filter by it cheaply. + tags_attributes: + - event_type + - event_class + + include: + domains: + - alarm_control_panel + - binary_sensor + - climate + - event + - light + - sensor + - switch + entity_globs: + - "*omni*" # scope to omni_pca entities only diff --git a/grafana/provisioning/dashboards/dashboards.yml b/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 0000000..47166e9 --- /dev/null +++ b/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,19 @@ +# Tells Grafana to scan /etc/grafana/provisioning/dashboards for +# *.json dashboard files at boot. Picks up omni-pro-ii.json +# automatically. Dashboards loaded this way are read-only in the UI; +# the source of truth is the JSON in this directory. + +apiVersion: 1 + +providers: + - name: omni-pca + orgId: 1 + folder: '' + folderUid: '' + type: file + disableDeletion: false + updateIntervalSeconds: 30 + allowUiUpdates: false + options: + path: /etc/grafana/provisioning/dashboards + foldersFromFilesStructure: false diff --git a/grafana/provisioning/dashboards/omni-pro-ii.json b/grafana/provisioning/dashboards/omni-pro-ii.json new file mode 100644 index 0000000..536d9e2 --- /dev/null +++ b/grafana/provisioning/dashboards/omni-pro-ii.json @@ -0,0 +1,682 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": {"type": "grafana", "uid": "-- Grafana --"}, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Live view of an HAI/Leviton Omni Pro II panel surfaced by the omni_pca Home Assistant integration. System health, security activity, climate trends, and the typed push-event stream — all sourced from InfluxDB writes shipped by HA's influxdb integration.", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 0}, + "id": 100, + "panels": [], + "title": "System health", + "type": "row" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "fieldConfig": { + "defaults": { + "color": {"mode": "thresholds"}, + "mappings": [ + {"options": {"0": {"color": "red", "index": 0, "text": "LOST"}}, "type": "value"}, + {"options": {"1": {"color": "green", "index": 1, "text": "OK"}}, "type": "value"} + ], + "thresholds": {"mode": "absolute", "steps": [{"color": "red"}, {"color": "green", "value": 1}]}, + "unit": "none" + } + }, + "gridPos": {"h": 5, "w": 6, "x": 0, "y": 1}, + "id": 101, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, + "textMode": "auto" + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: -24h)\n |> filter(fn: (r) => r.domain == \"binary_sensor\")\n |> filter(fn: (r) => r.entity_id =~ /ac_power/ and not (r.entity_id =~ /_[0-9]+$/))\n |> filter(fn: (r) => r._field == \"value\")\n |> last()\n |> group()", + "refId": "A" + } + ], + "title": "AC power", + "type": "stat" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "fieldConfig": { + "defaults": { + "color": {"mode": "thresholds"}, + "mappings": [ + {"options": {"0": {"color": "green", "index": 0, "text": "OK"}}, "type": "value"}, + {"options": {"1": {"color": "red", "index": 1, "text": "LOW"}}, "type": "value"} + ], + "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "red", "value": 1}]} + } + }, + "gridPos": {"h": 5, "w": 6, "x": 6, "y": 1}, + "id": 102, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, + "textMode": "auto" + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: -24h)\n |> filter(fn: (r) => r.domain == \"binary_sensor\")\n |> filter(fn: (r) => r.entity_id =~ /battery/ and not (r.entity_id =~ /_[0-9]+$/))\n |> filter(fn: (r) => r._field == \"value\")\n |> last()\n |> group()", + "refId": "A" + } + ], + "title": "Backup battery", + "type": "stat" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "fieldConfig": { + "defaults": { + "color": {"mode": "thresholds"}, + "mappings": [ + {"options": {"0": {"color": "green", "index": 0, "text": "Clear"}}, "type": "value"}, + {"options": {"1": {"color": "red", "index": 1, "text": "Trouble"}}, "type": "value"} + ], + "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "red", "value": 1}]} + } + }, + "gridPos": {"h": 5, "w": 6, "x": 12, "y": 1}, + "id": 103, + "options": { + "colorMode": "background", + "graphMode": "none", + "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, + "textMode": "auto" + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: -24h)\n |> filter(fn: (r) => r.domain == \"binary_sensor\")\n |> filter(fn: (r) => r.entity_id =~ /trouble/ and not (r.entity_id =~ /_[0-9]+$/))\n |> filter(fn: (r) => r._field == \"value\")\n |> last()\n |> group()", + "refId": "A" + } + ], + "title": "System trouble", + "type": "stat" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "description": "Count of panel push events in the last 24 hours. Empty until the panel pushes its first event (the mock fires events when HA actions trigger panel state changes; a real panel pushes continuously).", + "fieldConfig": { + "defaults": { + "color": {"mode": "thresholds"}, + "thresholds": {"mode": "absolute", "steps": [{"color": "blue"}, {"color": "green", "value": 1}]}, + "unit": "short" + } + }, + "gridPos": {"h": 5, "w": 6, "x": 18, "y": 1}, + "id": 104, + "options": { + "colorMode": "background", + "graphMode": "area", + "reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}, + "textMode": "auto" + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: -24h)\n |> filter(fn: (r) => r.domain == \"event\")\n |> filter(fn: (r) => r._field == \"state\")\n |> group()\n |> count()", + "refId": "A" + } + ], + "title": "Events (24h)", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 6}, + "id": 200, + "panels": [], + "title": "Security", + "type": "row" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "description": "Arming state per area. Disarmed = green, day = teal, night = blue, away = orange, vacation = magenta, triggered = red, arming/pending = yellow.", + "fieldConfig": { + "defaults": { + "color": {"mode": "thresholds"}, + "custom": {"fillOpacity": 80, "lineWidth": 0}, + "mappings": [ + {"options": {"disarmed": {"color": "#43aa8b", "text": "disarmed"}}, "type": "value"}, + {"options": {"armed_home": {"color": "#577590", "text": "armed home"}}, "type": "value"}, + {"options": {"armed_night": {"color": "#277da1", "text": "armed night"}}, "type": "value"}, + {"options": {"armed_away": {"color": "#f8961e", "text": "armed away"}}, "type": "value"}, + {"options": {"armed_vacation": {"color": "#a663cc", "text": "armed vacation"}}, "type": "value"}, + {"options": {"armed_custom_bypass": {"color": "#90be6d", "text": "armed custom"}}, "type": "value"}, + {"options": {"arming": {"color": "#f9c74f", "text": "arming"}}, "type": "value"}, + {"options": {"pending": {"color": "#f9c74f", "text": "pending"}}, "type": "value"}, + {"options": {"triggered": {"color": "#d62828", "text": "TRIGGERED"}}, "type": "value"} + ], + "thresholds": {"mode": "absolute", "steps": [{"color": "#6c757d"}]} + } + }, + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 7}, + "id": 201, + "options": { + "legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": {"mode": "single"} + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r.domain == \"alarm_control_panel\")\n |> filter(fn: (r) => r._field == \"state\")\n |> filter(fn: (r) => not (r.entity_id =~ /_[0-9]+$/))\n |> keep(columns: [\"_time\", \"_value\", \"entity_id\"])\n |> group(columns: [\"entity_id\"])", + "refId": "A" + } + ], + "title": "Area arming state", + "type": "state-timeline" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "description": "Push events the panel sent in the selected window. Columns: time, typed event_type, object index (zone / unit / area / user), and new_state for state-changed events.", + "fieldConfig": { + "defaults": { + "color": {"mode": "thresholds"}, + "custom": { + "align": "auto", + "cellOptions": {"type": "auto"}, + "inspect": false + }, + "thresholds": {"mode": "absolute", "steps": [{"color": "transparent"}]} + }, + "overrides": [ + { + "matcher": {"id": "byName", "options": "event_type"}, + "properties": [ + {"id": "custom.cellOptions", "value": {"type": "color-background", "mode": "basic"}}, + {"id": "mappings", "value": [ + {"options": {"alarm_activated": {"color": "#d62828", "text": "alarm_activated"}}, "type": "value"}, + {"options": {"alarm_cleared": {"color": "#43aa8b", "text": "alarm_cleared"}}, "type": "value"}, + {"options": {"ac_lost": {"color": "#d62828", "text": "ac_lost"}}, "type": "value"}, + {"options": {"ac_restored": {"color": "#43aa8b", "text": "ac_restored"}}, "type": "value"}, + {"options": {"battery_low": {"color": "#f8961e", "text": "battery_low"}}, "type": "value"}, + {"options": {"battery_restored": {"color": "#43aa8b", "text": "battery_restored"}}, "type": "value"}, + {"options": {"zone_state_changed": {"color": "#577590", "text": "zone_state_changed"}}, "type": "value"}, + {"options": {"unit_state_changed": {"color": "#90be6d", "text": "unit_state_changed"}}, "type": "value"}, + {"options": {"arming_changed": {"color": "#f9c74f", "text": "arming_changed"}}, "type": "value"}, + {"options": {"user_macro_button": {"color": "#277da1", "text": "user_macro_button"}}, "type": "value"}, + {"options": {"phone_line_dead": {"color": "#f8961e", "text": "phone_line_dead"}}, "type": "value"}, + {"options": {"phone_line_restored": {"color": "#43aa8b", "text": "phone_line_restored"}}, "type": "value"} + ]} + ] + }, + { + "matcher": {"id": "byName", "options": "_time"}, + "properties": [ + {"id": "custom.width", "value": 175}, + {"id": "displayName", "value": "time"} + ] + } + ] + }, + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 7}, + "id": 202, + "options": { + "cellHeight": "sm", + "footer": {"countRows": false, "fields": "", "reducer": ["sum"], "show": false}, + "showHeader": true, + "sortBy": [{"desc": true, "displayName": "time"}] + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r.domain == \"event\")\n |> filter(fn: (r) => r._field == \"new_state\" or r._field == \"unit_index\" or r._field == \"zone_index\" or r._field == \"area_index\" or r._field == \"user_index\" or r._field == \"alarm_type\" or r._field == \"button_index\")\n |> pivot(rowKey: [\"_time\", \"event_type\"], columnKey: [\"_field\"], valueColumn: \"_value\")\n |> drop(columns: [\"_start\", \"_stop\", \"_measurement\", \"domain\", \"entity_id\", \"event_class\"])\n |> group()\n |> sort(columns: [\"_time\"], desc: true)\n |> limit(n: 50)", + "refId": "A" + } + ], + "title": "Recent panel events", + "type": "table" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "description": "Zone open/closed timeline. Painted segments = zone is_on (open / tripped).", + "fieldConfig": { + "defaults": { + "color": {"mode": "thresholds"}, + "custom": {"fillOpacity": 70, "lineWidth": 0}, + "mappings": [ + {"options": {"0": {"color": "green", "index": 0, "text": "secure"}}, "type": "value"}, + {"options": {"1": {"color": "orange", "index": 1, "text": "open"}}, "type": "value"} + ], + "thresholds": {"mode": "absolute", "steps": [{"color": "green"}, {"color": "orange", "value": 1}]} + } + }, + "gridPos": {"h": 8, "w": 24, "x": 0, "y": 15}, + "id": 203, + "options": { + "legend": {"displayMode": "list", "placement": "bottom", "showLegend": false}, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "never", + "tooltip": {"mode": "single"} + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r.domain == \"binary_sensor\")\n |> filter(fn: (r) => not (r.entity_id =~ /ac_power|battery|trouble|bypass|_[0-9]+$/))\n |> filter(fn: (r) => r._field == \"value\")\n |> keep(columns: [\"_time\", \"_value\", \"entity_id\"])\n |> group(columns: [\"entity_id\"])", + "refId": "A" + } + ], + "title": "Zone trip timeline", + "type": "state-timeline" + }, + { + "collapsed": false, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 23}, + "id": 300, + "panels": [], + "title": "Climate", + "type": "row" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "description": "Current temperature per thermostat. Mock fixture values are raw panel format; a real panel reports °F.", + "fieldConfig": { + "defaults": { + "color": {"mode": "fixed", "fixedColor": "#f1faee"}, + "custom": { + "axisPlacement": "auto", + "drawStyle": "line", + "fillOpacity": 8, + "gradientMode": "opacity", + "lineInterpolation": "stepBefore", + "lineWidth": 2, + "pointSize": 4, + "showPoints": "auto", + "spanNulls": true + }, + "unit": "celsius" + }, + "overrides": [ + { + "matcher": {"id": "byFrameRefID", "options": "A"}, + "properties": [{"id": "color", "value": {"mode": "palette-classic-by-name"}}] + } + ] + }, + "gridPos": {"h": 9, "w": 16, "x": 0, "y": 24}, + "id": 301, + "options": { + "legend": {"calcs": ["mean", "lastNotNull"], "displayMode": "table", "placement": "right", "showLegend": true}, + "tooltip": {"mode": "multi", "sort": "desc"} + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r.domain == \"climate\")\n |> filter(fn: (r) => not (r.entity_id =~ /_[0-9]+$/))\n |> filter(fn: (r) => r._field == \"current_temperature\")\n |> keep(columns: [\"_time\", \"_value\", \"entity_id\"])\n |> group(columns: [\"entity_id\"])\n |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)", + "refId": "A" + } + ], + "title": "Thermostat temperatures", + "type": "timeseries" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "description": "HVAC system mode per thermostat over the selected window. Off = grey, Heat = orange, Cool = blue, Auto = green, Dry = teal, Fan only = yellow.", + "fieldConfig": { + "defaults": { + "color": {"mode": "thresholds"}, + "custom": {"fillOpacity": 80, "lineWidth": 0}, + "mappings": [ + {"options": {"off": {"color": "#adb5bd", "text": "off"}}, "type": "value"}, + {"options": {"heat": {"color": "#f3722c", "text": "heat"}}, "type": "value"}, + {"options": {"cool": {"color": "#277da1", "text": "cool"}}, "type": "value"}, + {"options": {"heat_cool":{"color": "#43aa8b", "text": "auto"}}, "type": "value"}, + {"options": {"auto": {"color": "#43aa8b", "text": "auto"}}, "type": "value"}, + {"options": {"dry": {"color": "#577590", "text": "dry"}}, "type": "value"}, + {"options": {"fan_only": {"color": "#f9c74f", "text": "fan only"}}, "type": "value"} + ], + "thresholds": {"mode": "absolute", "steps": [{"color": "#adb5bd"}]} + } + }, + "gridPos": {"h": 9, "w": 8, "x": 16, "y": 24}, + "id": 302, + "options": { + "legend": {"displayMode": "list", "placement": "bottom", "showLegend": true}, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": {"mode": "single"} + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r.domain == \"climate\")\n |> filter(fn: (r) => r._field == \"state\")\n |> filter(fn: (r) => not (r.entity_id =~ /_[0-9]+$/))\n |> keep(columns: [\"_time\", \"_value\", \"entity_id\"])\n |> group(columns: [\"entity_id\"])", + "refId": "A" + } + ], + "title": "HVAC mode", + "type": "state-timeline" + }, + { + "collapsed": false, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 33}, + "id": 400, + "panels": [], + "title": "Activity", + "type": "row" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "description": "Panel event rate, bucketed by event_type. Tracks zone state changes, button presses, alarm activation, AC/battery events, etc. Each event_type has its own color matching the events table.", + "fieldConfig": { + "defaults": { + "color": {"mode": "palette-classic"}, + "custom": { + "drawStyle": "bars", + "fillOpacity": 80, + "lineWidth": 0, + "showPoints": "never", + "stacking": {"mode": "normal"} + }, + "unit": "short" + }, + "overrides": [ + {"matcher": {"id": "byName", "options": "alarm_activated"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#d62828"}}]}, + {"matcher": {"id": "byName", "options": "alarm_cleared"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#43aa8b"}}]}, + {"matcher": {"id": "byName", "options": "ac_lost"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#d62828"}}]}, + {"matcher": {"id": "byName", "options": "ac_restored"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#43aa8b"}}]}, + {"matcher": {"id": "byName", "options": "battery_low"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#f8961e"}}]}, + {"matcher": {"id": "byName", "options": "battery_restored"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#43aa8b"}}]}, + {"matcher": {"id": "byName", "options": "zone_state_changed"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#577590"}}]}, + {"matcher": {"id": "byName", "options": "unit_state_changed"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#90be6d"}}]}, + {"matcher": {"id": "byName", "options": "arming_changed"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#f9c74f"}}]}, + {"matcher": {"id": "byName", "options": "user_macro_button"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#277da1"}}]}, + {"matcher": {"id": "byName", "options": "phone_line_dead"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#f8961e"}}]}, + {"matcher": {"id": "byName", "options": "phone_line_restored"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#43aa8b"}}]} + ] + }, + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 34}, + "id": 401, + "options": { + "legend": {"displayMode": "table", "placement": "right", "showLegend": true, "calcs": ["sum"]}, + "tooltip": {"mode": "multi", "sort": "desc"} + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r.domain == \"event\")\n |> filter(fn: (r) => r._field == \"state\")\n |> group(columns: [\"event_type\"])\n |> aggregateWindow(every: v.windowPeriod, fn: count, createEmpty: true)", + "refId": "A" + } + ], + "title": "Event rate by type", + "type": "timeseries" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "description": "Top 15 most-toggled units in the selected window — bar length = number of state changes. Reveals which lights/relays get used most.", + "fieldConfig": { + "defaults": { + "color": {"mode": "thresholds"}, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + {"color": "#f9c74f", "value": null}, + {"color": "#f8961e", "value": 5}, + {"color": "#f3722c", "value": 15}, + {"color": "#d62828", "value": 30} + ] + }, + "min": 0, + "unit": "short" + } + }, + "gridPos": {"h": 10, "w": 12, "x": 12, "y": 34}, + "id": 402, + "options": { + "displayMode": "gradient", + "valueMode": "color", + "showUnfilled": true, + "orientation": "horizontal", + "reduceOptions": {"calcs": ["lastNotNull"], "fields": "/^_value$/", "values": true}, + "minVizHeight": 10, + "minVizWidth": 0, + "namePlacement": "left" + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r.domain == \"light\")\n |> filter(fn: (r) => not (r.entity_id =~ /_[0-9]+$/))\n |> filter(fn: (r) => r._field == \"value\")\n |> toFloat()\n |> keep(columns: [\"_value\", \"entity_id\"])\n |> group(columns: [\"entity_id\"])\n |> count()\n |> group()\n |> sort(columns: [\"_value\"], desc: true)\n |> limit(n: 15)", + "refId": "A" + } + ], + "title": "Top toggled units (24h)", + "type": "bargauge" + }, + { + "collapsed": false, + "gridPos": {"h": 1, "w": 24, "x": 0, "y": 44}, + "id": 500, + "panels": [], + "title": "Insights", + "type": "row" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "description": "Zones currently bypassed. Bypass = the panel ignores this zone for arming/alarm purposes. Empty when nothing is bypassed; rows accrue when a switch is flipped.", + "fieldConfig": { + "defaults": { + "color": {"mode": "thresholds"}, + "custom": { + "align": "auto", + "cellOptions": {"type": "auto"}, + "inspect": false + }, + "thresholds": {"mode": "absolute", "steps": [{"color": "transparent"}]} + }, + "overrides": [ + { + "matcher": {"id": "byName", "options": "entity_id"}, + "properties": [ + {"id": "displayName", "value": "zone bypass switch"}, + {"id": "custom.cellOptions", "value": {"type": "color-text", "wrapText": false}}, + {"id": "color", "value": {"mode": "fixed", "fixedColor": "#f9c74f"}} + ] + }, + { + "matcher": {"id": "byName", "options": "_time"}, + "properties": [ + {"id": "displayName", "value": "since"}, + {"id": "custom.width", "value": 175} + ] + }, + { + "matcher": {"id": "byName", "options": "_value"}, + "properties": [{"id": "custom.hidden", "value": true}] + } + ] + }, + "gridPos": {"h": 8, "w": 8, "x": 0, "y": 45}, + "id": 501, + "options": { + "cellHeight": "sm", + "footer": {"countRows": true, "fields": "", "reducer": ["sum"], "show": true}, + "showHeader": true, + "sortBy": [{"desc": true, "displayName": "since"}] + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: -24h)\n |> filter(fn: (r) => r.domain == \"switch\")\n |> filter(fn: (r) => not (r.entity_id =~ /_[0-9]+$/))\n |> filter(fn: (r) => r._field == \"value\")\n |> last()\n |> filter(fn: (r) => r._value > 0.0)\n |> keep(columns: [\"_time\", \"_value\", \"entity_id\"])\n |> group()\n |> sort(columns: [\"_time\"], desc: true)", + "refId": "A" + } + ], + "title": "Active zone bypasses", + "type": "table" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "description": "User-macro button press events from the panel. Each row = one press; button_index identifies which scene/macro fired.", + "fieldConfig": { + "defaults": { + "color": {"mode": "thresholds"}, + "custom": { + "align": "auto", + "cellOptions": {"type": "auto"}, + "inspect": false + }, + "thresholds": {"mode": "absolute", "steps": [{"color": "transparent"}]} + }, + "overrides": [ + { + "matcher": {"id": "byName", "options": "button_index"}, + "properties": [ + {"id": "displayName", "value": "button #"}, + {"id": "custom.cellOptions", "value": {"type": "color-background", "mode": "basic"}}, + {"id": "color", "value": {"mode": "fixed", "fixedColor": "#277da1"}} + ] + }, + { + "matcher": {"id": "byName", "options": "_time"}, + "properties": [ + {"id": "displayName", "value": "time"}, + {"id": "custom.width", "value": 175} + ] + } + ] + }, + "gridPos": {"h": 8, "w": 8, "x": 8, "y": 45}, + "id": 502, + "options": { + "cellHeight": "sm", + "footer": {"countRows": true, "fields": "", "reducer": ["sum"], "show": true}, + "showHeader": true, + "sortBy": [{"desc": true, "displayName": "time"}] + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r.domain == \"event\" and r.event_type == \"user_macro_button\")\n |> filter(fn: (r) => r._field == \"button_index\")\n |> keep(columns: [\"_time\", \"_value\"])\n |> group()\n |> sort(columns: [\"_time\"], desc: true)\n |> limit(n: 25)\n |> rename(columns: {_value: \"button_index\"})", + "refId": "A" + } + ], + "title": "Button press log", + "type": "table" + }, + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "description": "Distribution of panel push events by typed kind across the selected window. Matches the colors used in the event rate and events table panels.", + "fieldConfig": { + "defaults": { + "color": {"mode": "palette-classic"}, + "custom": { + "hideFrom": {"legend": false, "tooltip": false, "viz": false} + }, + "mappings": [] + }, + "overrides": [ + {"matcher": {"id": "byName", "options": "alarm_activated"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#d62828"}}]}, + {"matcher": {"id": "byName", "options": "alarm_cleared"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#43aa8b"}}]}, + {"matcher": {"id": "byName", "options": "ac_lost"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#d62828"}}]}, + {"matcher": {"id": "byName", "options": "ac_restored"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#43aa8b"}}]}, + {"matcher": {"id": "byName", "options": "battery_low"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#f8961e"}}]}, + {"matcher": {"id": "byName", "options": "battery_restored"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#43aa8b"}}]}, + {"matcher": {"id": "byName", "options": "zone_state_changed"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#577590"}}]}, + {"matcher": {"id": "byName", "options": "unit_state_changed"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#90be6d"}}]}, + {"matcher": {"id": "byName", "options": "arming_changed"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#f9c74f"}}]}, + {"matcher": {"id": "byName", "options": "user_macro_button"}, "properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "#277da1"}}]} + ] + }, + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 45}, + "id": 503, + "options": { + "displayLabels": ["percent", "name"], + "legend": {"displayMode": "table", "placement": "right", "showLegend": true, "values": ["value"]}, + "pieType": "donut", + "reduceOptions": {"calcs": ["sum"], "fields": "", "values": false}, + "tooltip": {"mode": "single", "sort": "none"} + }, + "pluginVersion": "11.4.0", + "targets": [ + { + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "query": "from(bucket: \"ha\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r.domain == \"event\" and r._field == \"state\")\n |> keep(columns: [\"_time\", \"_value\", \"event_type\"])\n |> group(columns: [\"event_type\"])\n |> count(column: \"_value\")\n |> map(fn: (r) => ({_time: now(), _value: r._value, event_type: r.event_type}))\n |> pivot(rowKey: [\"_time\"], columnKey: [\"event_type\"], valueColumn: \"_value\")", + "refId": "A" + } + ], + "title": "Event distribution", + "type": "piechart" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": ["omni-pca", "hai", "omni-pro-ii", "home-assistant"], + "templating": { + "list": [ + { + "current": {"selected": true, "text": "All", "value": "$__all"}, + "datasource": {"type": "influxdb", "uid": "InfluxDB"}, + "definition": "from(bucket: \"ha\") |> range(start: -7d) |> filter(fn: (r) => r.domain == \"event\" and r._field == \"state\") |> keep(columns: [\"event_type\"]) |> group() |> distinct(column: \"event_type\")", + "hide": 0, + "includeAll": true, + "label": "Event type", + "multi": true, + "name": "event_type", + "options": [], + "query": "from(bucket: \"ha\") |> range(start: -7d) |> filter(fn: (r) => r.domain == \"event\" and r._field == \"state\") |> keep(columns: [\"event_type\"]) |> group() |> distinct(column: \"event_type\")", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": {"from": "now-24h", "to": "now"}, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h"], + "time_options": ["1h", "6h", "24h", "2d", "7d", "30d"] + }, + "timezone": "browser", + "title": "Omni Pro II — Panel Overview", + "uid": "omni-pro-ii-overview", + "version": 1, + "weekStart": "" +} diff --git a/grafana/provisioning/datasources/influxdb.yml b/grafana/provisioning/datasources/influxdb.yml new file mode 100644 index 0000000..65d2758 --- /dev/null +++ b/grafana/provisioning/datasources/influxdb.yml @@ -0,0 +1,21 @@ +# Auto-wires the InfluxDB v2 datasource at Grafana boot. Picks up +# INFLUX_URL and INFLUX_TOKEN from the grafana container's environment +# (set in docker-compose.yml from .env). No manual datasource config +# needed. + +apiVersion: 1 + +datasources: + - name: InfluxDB + type: influxdb + access: proxy + url: ${INFLUX_URL} + isDefault: true + editable: false + jsonData: + version: Flux + organization: omni-pca + defaultBucket: ha + tlsSkipVerify: true + secureJsonData: + token: ${INFLUX_TOKEN}