Add TeslaMate deployment for Tesla Model Y data logging (#47)

## Summary
- Add TeslaMate k8s deployment with Tailscale ingress at tesla.tail8d86e.ts.net
- Add teslamate user to CloudNativePG blumeops-pg cluster
- Add TeslaMate PostgreSQL datasource to Grafana
- Import 18 TeslaMate Grafana dashboards for charging, drives, efficiency, etc.
- Add teslamate database to borgmatic backup configuration

## Deployment and Testing
- [ ] Create 1Password items: "TeslaMate DB Password" and "TeslaMate Encryption Key"
- [ ] Apply database user secret: `op inject -i argocd/manifests/databases/secret-teslamate.yaml.tpl | kubectl apply -f -`
- [ ] Sync blumeops-pg: `argocd app sync blumeops-pg`
- [ ] Create teslamate database
- [ ] Apply teslamate secrets (encryption key, db connection)
- [ ] Apply Grafana datasource secret: `op inject -i argocd/manifests/grafana-config/secret-teslamate-datasource.yaml.tpl | kubectl apply -f -`
- [ ] Sync apps and teslamate: `argocd app sync apps teslamate grafana grafana-config`
- [ ] Complete Tesla API OAuth flow at https://tesla.tail8d86e.ts.net
- [ ] Verify data collection starts
- [ ] Verify Grafana dashboards show data

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/47
This commit is contained in:
Erich Blume 2026-01-22 21:25:44 -08:00
commit 272ddb213b
41 changed files with 22366 additions and 10 deletions

View file

@ -46,3 +46,7 @@ borgmatic_postgresql_databases:
hostname: pg.tail8d86e.ts.net
port: 5432
username: borgmatic
- name: teslamate
hostname: pg.tail8d86e.ts.net
port: 5432
username: borgmatic

View file

@ -17,5 +17,4 @@ spec:
syncPolicy:
syncOptions:
- CreateNamespace=true
# Manual sync only - no automated sync on git push
# To pick up new apps: argocd app sync apps

View file

@ -17,4 +17,3 @@ spec:
syncPolicy:
syncOptions:
- CreateNamespace=true
# Manual sync only - no automated sync on git push

View file

@ -21,4 +21,3 @@ spec:
syncPolicy:
syncOptions:
- CreateNamespace=true
# Manual sync only - no automated sync on git push

View file

@ -12,7 +12,7 @@ spec:
sources:
# Helm chart from forge mirror (SSH via egress)
- repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/cloudnative-pg-charts.git
targetRevision: cloudnative-pg-v0.23.0
targetRevision: cloudnative-pg-v0.27.0
path: charts/cloudnative-pg
helm:
releaseName: cloudnative-pg
@ -29,4 +29,3 @@ spec:
syncOptions:
- CreateNamespace=true
- ServerSideApply=true # Required for large CRDs that exceed annotation size limit
# Manual sync only - no automated sync on git push

View file

@ -27,4 +27,3 @@ spec:
syncPolicy:
syncOptions:
- CreateNamespace=true
# Manual sync only - no automated sync on git push

View file

@ -22,4 +22,3 @@ spec:
syncPolicy:
syncOptions:
- CreateNamespace=true
# Manual sync only - no automated sync on git push

View file

@ -31,4 +31,3 @@ spec:
syncPolicy:
syncOptions:
- CreateNamespace=true
# Manual sync only - no automated sync on git push

View file

@ -25,4 +25,3 @@ spec:
syncPolicy:
syncOptions:
- CreateNamespace=true
# Manual sync only - no automated sync on git push

View file

@ -23,4 +23,3 @@ spec:
syncPolicy:
syncOptions:
- CreateNamespace=true
# Manual sync only - no automated sync on git push

View file

@ -0,0 +1,32 @@
# TeslaMate Tesla Data Logger
# Requires: CloudNativePG PostgreSQL cluster and manual secret setup
#
# Before syncing, create the namespace and secrets:
# kubectl create namespace teslamate
# op inject -i argocd/manifests/databases/secret-teslamate.yaml.tpl | kubectl apply -f -
# op inject -i argocd/manifests/teslamate/secret-encryption-key.yaml.tpl | kubectl apply -f -
# op inject -i argocd/manifests/teslamate/secret-db.yaml.tpl | kubectl apply -f -
#
# Then create the database:
# PGPASSWORD=$(op --vault blumeops item get <eblume-item-id> --fields password --reveal) \
# psql -h pg.tail8d86e.ts.net -U eblume -c "CREATE DATABASE teslamate OWNER teslamate;"
#
# After syncing, access the TeslaMate UI at https://tesla.tail8d86e.ts.net to complete
# Tesla API authentication via OAuth flow.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: teslamate
namespace: argocd
spec:
project: default
source:
repoURL: ssh://forgejo@indri.tail8d86e.ts.net:2200/eblume/blumeops.git
targetRevision: main
path: argocd/manifests/teslamate
destination:
server: https://kubernetes.default.svc
namespace: teslamate
syncPolicy:
syncOptions:
- CreateNamespace=true

View file

@ -7,6 +7,7 @@ metadata:
namespace: databases
spec:
instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:18
storage:
size: 10Gi
@ -43,6 +44,17 @@ spec:
- pg_read_all_data
passwordSecret:
name: blumeops-pg-borgmatic
# teslamate user for TeslaMate Tesla data logger
# Note: superuser required for extension management during migrations
- name: teslamate
login: true
superuser: true
connectionLimit: -1
ensure: present
inherit: true
createdb: true
passwordSecret:
name: blumeops-pg-teslamate
# Resource limits for minikube environment
resources:

View file

@ -0,0 +1,11 @@
# Template for TeslaMate database user password
# Apply with: op inject -i argocd/manifests/databases/secret-teslamate.yaml.tpl | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: blumeops-pg-teslamate
namespace: databases
type: kubernetes.io/basic-auth
stringData:
username: teslamate
password: {{ op://blumeops/TeslaMate/db_password }}

View file

@ -0,0 +1,419 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard-teslamate-charge-level
namespace: monitoring
labels:
grafana_dashboard: "1"
annotations:
grafana_folder: "TeslaMate"
data:
teslamate-charge-level.json: |
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [
{
"icon": "dashboard",
"tags": [],
"title": "TeslaMate",
"tooltip": "",
"type": "link",
"url": "${base_url:raw}"
},
{
"asDropdown": true,
"icon": "external link",
"tags": [
"tesla"
],
"title": "Dashboards",
"type": "dashboards"
}
],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 4,
"panels": [],
"repeat": "car_id",
"title": "$car_id",
"type": "row"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Charge Level",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "stepAfter",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "line"
}
},
"decimals": 0,
"links": [],
"mappings": [],
"max": 100,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "transparent",
"value": 0
}
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 21,
"w": 24,
"x": 0,
"y": 1
},
"id": 2,
"options": {
"legend": {
"calcs": [
"mean",
"max",
"min"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"maxHeight": 600,
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "time_series",
"rawQuery": true,
"rawSql": "SELECT\n\tdate_bin('2 minutes'::interval, timezone('UTC', date), to_timestamp(${__from:date:seconds})) as time,\n\tavg(battery_level) AS \"Battery Level\",\n\tavg(usable_battery_level) AS \"Usable Battery Level\"\nfrom positions\n\tWHERE $__timeFilter(date) AND car_id = $car_id and ideal_battery_range_km is not null\n\tgroup by time\n\tORDER BY time ASC\n;",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT\r\n 20 as lower,\r\n CASE WHEN lfp_battery THEN 100 ELSE 80 END as upper\r\nfrom cars inner join car_settings on cars.settings_id = car_settings.id\r\nwhere cars.id = $car_id",
"refId": "B",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "-- To be able to calculate percentiles for unevenly sampled values we are bucketing & gapfilling values before running calculations\r\nwith positions_filtered as (\r\n select\r\n date,\r\n battery_level\r\n from\r\n positions p\r\n where\r\n p.car_id = $car_id\r\n -- p.ideal_battery_range_km condition is added to reduce overall amount of data and avoid data biases while driving (unevenly sampled data)\r\n and p.ideal_battery_range_km is not null\r\n and 1 = $include_average_percentiles\r\n),\r\ngen_date_series as (\r\n select\r\n -- series is used to bucket data and avoid gaps in series used to determine percentiles\r\n generate_series(to_timestamp(${__from:date:seconds} - (86400 * $days_moving_average_percentiles / 2)), to_timestamp(${__to:date:seconds}), concat($bucket_width, ' seconds')::INTERVAL) as series_id\r\n),\r\ndate_series as (\r\n select\r\n timezone('UTC', series_id) as series_id,\r\n -- before joining, get beginning of next series to be able to left join `positions_filtered`\r\n timezone('UTC', lead(series_id) over (order by series_id asc)) as next_series_id\r\n from\r\n gen_date_series\r\n),\r\npositions_bucketed as (\r\n select\r\n series_id,\r\n -- simple average can result in loss of accuracy, see https://www.timescale.com/blog/what-time-weighted-averages-are-and-why-you-should-care/ for details\r\n avg(battery_level) as battery_level,\r\n min(positions_filtered.date) as series_min_date\r\n from\r\n date_series\r\n left join positions_filtered on\r\n positions_filtered.date >= date_series.series_id\r\n and positions_filtered.date < date_series.next_series_id\r\n group by\r\n series_id\r\n),\r\n-- PostgreSQL cannot IGNORE NULLS via Window Functions LAST_VALUE - therefore use natural behavior of COUNT & MAX, see https://www.reddit.com/r/SQL/comments/wb949v/comment/ii5mmmi/ for details\r\npositions_bucketed_gapfilling_locf_intermediate as (\r\n select\r\n series_id,\r\n battery_level,\r\n series_min_date,\r\n count(battery_level) over (order by series_id) as i\r\n from\r\n positions_bucketed\r\n\r\n),\r\npositions_bucketed_gapfilled_locf as (\r\n select\r\n series_id,\r\n series_min_date,\r\n max(battery_level) over (partition by i) as battery_level_locf\r\n from\r\n positions_bucketed_gapfilling_locf_intermediate\r\n),\r\n-- PostgreSQL cannot use PERCENTILE_DISC as Window Function - therefore use ARRAY_AGG and UNNEST, see https://stackoverflow.com/a/72718604 for details\r\npositions_bucketed_gapfilled_locf_percentile_intermediate as (\r\n select\r\n series_id,\r\n series_min_date,\r\n min(series_min_date) over () as min_date,\r\n array_agg(battery_level_locf) over w as arr,\r\n avg(battery_level_locf) over w as battery_level_avg\r\n from\r\n positions_bucketed_gapfilled_locf\r\n window w as (rows between (86400 / $bucket_width) * ($days_moving_average_percentiles / 2) preceding and (86400 / $bucket_width) * ($days_moving_average_percentiles / 2) following)\r\n)\r\n\r\nselect\r\n series_id::timestamptz,\r\n (select percentile_cont(0.075) within group (order by s) from unnest(arr) trick(s)) as \"$days_moving_average_percentiles Day Moving 7.5% Percentile (${bucket_width:text} buckets)\",\r\n battery_level_avg as \"$days_moving_average_percentiles Day Moving Average (${bucket_width:text} buckets)\",\r\n (select percentile_cont(0.5) within group (order by s) from unnest(arr) trick(s)) as \"$days_moving_average_percentiles Day Moving Median (${bucket_width:text} buckets)\",\r\n (select percentile_cont(0.925) within group (order by s) from unnest(arr) trick(s)) as \"$days_moving_average_percentiles Day Moving 92.5% Percentile (${bucket_width:text} buckets)\"\r\nfrom\r\n positions_bucketed_gapfilled_locf_percentile_intermediate where $__timeFilter(series_id) and series_min_date >= min_date",
"refId": "C",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Charge Level",
"transformations": [
{
"id": "configFromData",
"options": {
"applyTo": {
"id": "byFrameRefID",
"options": "A"
},
"configRefId": "B",
"mappings": [
{
"fieldName": "lower",
"handlerArguments": {
"threshold": {
"color": "green"
}
},
"handlerKey": "threshold1"
},
{
"fieldName": "upper",
"handlerArguments": {
"threshold": {
"color": "green"
}
},
"handlerKey": "threshold1"
}
]
}
}
],
"type": "timeseries"
}
],
"preload": false,
"refresh": "",
"schemaVersion": 41,
"tags": [
"tesla"
],
"templating": {
"list": [
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"hide": 2,
"includeAll": true,
"label": "Car",
"name": "car_id",
"options": [],
"query": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select base_url from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "base_url",
"options": [],
"query": "select base_url from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {
"text": "2h",
"value": "7200"
},
"description": "Data used to calculate Moving Average / Percentiles is unevenly sampled in TeslaMate. To avoid biases towards more frequently sampled values, the data is bucketed. For buckets without sampled values, the last observed value is carried forward. Bucketing is not time-weighted but is a simple average. Increasing the bucket width results in a loss of accuracy.",
"includeAll": false,
"label": "Bucket Width",
"name": "bucket_width",
"options": [
{
"selected": false,
"text": "1h",
"value": "3600"
},
{
"selected": true,
"text": "2h",
"value": "7200"
},
{
"selected": false,
"text": "4h",
"value": "14400"
}
],
"query": "1h : 3600, 2h : 7200, 4h : 14400",
"type": "custom"
},
{
"current": {
"text": "yes",
"value": "1"
},
"includeAll": false,
"label": "Include Moving Average / Percentiles",
"name": "include_average_percentiles",
"options": [
{
"selected": false,
"text": "no",
"value": "0"
},
{
"selected": true,
"text": "yes",
"value": "1"
}
],
"query": "no : 0, yes : 1",
"type": "custom"
},
{
"current": {
"text": "1/6 of interval",
"value": "6"
},
"description": "",
"includeAll": false,
"label": "Moving Average / Percentiles Width",
"name": "intervals_moving_average_percentiles",
"options": [
{
"selected": true,
"text": "1/6 of interval",
"value": "6"
},
{
"selected": false,
"text": "1/12 of interval",
"value": "12"
}
],
"query": "1/6 of interval : 6, 1/12 of interval : 12",
"type": "custom"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select ((${__to:date:seconds} - ${__from:date:seconds}) / 86400 / $intervals_moving_average_percentiles)",
"hide": 2,
"includeAll": false,
"name": "days_moving_average_percentiles",
"options": [],
"query": "select ((${__to:date:seconds} - ${__from:date:seconds}) / 86400 / $intervals_moving_average_percentiles)",
"refresh": 2,
"regex": "",
"type": "query"
}
]
},
"time": {
"from": "now-6M",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Charge Level",
"uid": "WopVO_mgz",
"version": 1
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,293 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard-teslamate-mileage
namespace: monitoring
labels:
grafana_dashboard: "1"
annotations:
grafana_folder: "TeslaMate"
data:
teslamate-mileage.json: |
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [
{
"icon": "dashboard",
"tags": [],
"title": "TeslaMate",
"tooltip": "",
"type": "link",
"url": "${base_url:raw}"
},
{
"asDropdown": true,
"icon": "external link",
"tags": [
"tesla"
],
"title": "Dashboards",
"type": "dashboards"
}
],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 4,
"panels": [],
"repeat": "car_id",
"title": "$car_id",
"type": "row"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "stepAfter",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"decimals": 0,
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": [
{
"matcher": {
"id": "byRegexp",
"options": ".*_km$"
},
"properties": [
{
"id": "unit",
"value": "km"
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": ".*_mi$"
},
"properties": [
{
"id": "unit",
"value": "mi"
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": "mileage_.*"
},
"properties": [
{
"id": "displayName",
"value": "Mileage"
}
]
}
]
},
"gridPos": {
"h": 21,
"w": 24,
"x": 0,
"y": 1
},
"id": 2,
"options": {
"legend": {
"calcs": [
"min",
"max"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"maxHeight": 600,
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "time_series",
"rawQuery": true,
"rawSql": "WITH o AS (SELECT\n start_date AS time,\n car_id,\n start_km AS \"odometer\"\nFROM drives\nUNION ALL\nSELECT\n end_date,\n car_id,\n end_km AS \"odometer\"\nFROM drives)\n\nSELECT\n time, \n convert_km(odometer::numeric, '$length_unit') as mileage_$length_unit\nFROM o\nWHERE\n\tcar_id = $car_id AND\n\t$__timeFilter(time)\norder by 1;",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Mileage",
"type": "timeseries"
}
],
"preload": false,
"refresh": "",
"schemaVersion": 41,
"tags": [
"tesla"
],
"templating": {
"list": [
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"hide": 2,
"includeAll": true,
"label": "Car",
"name": "car_id",
"options": [],
"query": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select unit_of_length from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "length_unit",
"options": [],
"query": "select unit_of_length from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select base_url from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "base_url",
"options": [],
"query": "select base_url from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
}
]
},
"time": {
"from": "now-6M",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Mileage",
"uid": "NjtMTFggz",
"version": 1
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,772 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard-teslamate-projected-range
namespace: monitoring
labels:
grafana_dashboard: "1"
annotations:
grafana_folder: "TeslaMate"
data:
teslamate-projected-range.json: |
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"enable": false,
"hide": false,
"iconColor": "rgba(255, 96, 96, 1)",
"limit": 100,
"name": "Charged",
"rawQuery": "SELECT\n$__time(start_date),\nend_date as timeend,\nconcat('Charged: ',round(cast(charge_energy_added as numeric),2),' kWh') AS text\nFROM charging_processes\nWHERE\n$__timeFilter(start_date) AND duration_min > 5\nORDER BY start_date DESC",
"showIn": 0,
"tags": [],
"type": "tags"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"links": [
{
"icon": "dashboard",
"tags": [],
"title": "TeslaMate",
"tooltip": "",
"type": "link",
"url": "${base_url:raw}"
},
{
"asDropdown": true,
"icon": "external link",
"tags": [
"tesla"
],
"title": "Dashboards",
"type": "dashboards"
}
],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 4,
"panels": [],
"repeat": "car_id",
"title": "$car_id",
"type": "row"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Projected Range",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 30,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "stepAfter",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"decimals": 0,
"links": [],
"mappings": [],
"min": 200,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
},
"unit": "none"
},
"overrides": [
{
"matcher": {
"id": "byRegexp",
"options": "/Mileage.*/"
},
"properties": [
{
"id": "custom.fillOpacity",
"value": 0
},
{
"id": "custom.axisLabel",
"value": "Mileage"
},
{
"id": "min"
}
]
}
]
},
"gridPos": {
"h": 21,
"w": 24,
"x": 0,
"y": 1
},
"id": 2,
"options": {
"legend": {
"calcs": [
"mean",
"max",
"min"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"maxHeight": 600,
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "time_series",
"rawQuery": true,
"rawSql": "SELECT\n\t$__timeGroup(date, $interval) AS time,\n\tconvert_km((sum(${preferred_range}_battery_range_km) / nullif(sum(coalesce(usable_battery_level,battery_level)),0) * 100)::numeric, '$length_unit') AS \"Projected ${preferred_range} range [$length_unit]\"\nFROM\n\t(\n select battery_level, usable_battery_level, date,\n rated_battery_range_km, ideal_battery_range_km, outside_temp\n from positions\n where\n car_id = $car_id and $__timeFilter(date) and ideal_battery_range_km is not null\n union all\n select battery_level, coalesce(usable_battery_level,battery_level) as usable_battery_level, date,\n rated_battery_range_km, ideal_battery_range_km, outside_temp\n from charges c\n join\n charging_processes p ON p.id = c.charging_process_id \n where\n $__timeFilter(date) and p.car_id = $car_id\n ) as data\n\nGROUP BY\n\t1\nhaving convert_km((sum(${preferred_range}_battery_range_km) / nullif(sum(coalesce(usable_battery_level,battery_level)),0) * 100)::numeric, '$length_unit') is not null\nORDER BY\n\t1,2 DESC",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "time_series",
"rawQuery": true,
"rawSql": "SELECT\n\t$__timeGroup(date, $interval) AS time,\n\tconvert_km(avg(odometer)::numeric, '$length_unit') AS \"Mileage [$length_unit]\"\nFROM\n\tpositions\nWHERE\n\t$__timeFilter(date) and\n\tcar_id = $car_id and ideal_battery_range_km is not null\nGROUP BY\n\t1\nORDER BY\n\t1,2 DESC;",
"refId": "B",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Projected Range - Mileage",
"type": "timeseries"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Projected Range",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 30,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "stepAfter",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"decimals": 0,
"links": [],
"mappings": [],
"min": 200,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
},
"unit": "none"
},
"overrides": [
{
"matcher": {
"id": "byRegexp",
"options": "/Battery.*/"
},
"properties": [
{
"id": "custom.fillOpacity",
"value": 0
},
{
"id": "max",
"value": 100
},
{
"id": "custom.axisLabel",
"value": "Battery Level"
}
]
}
]
},
"gridPos": {
"h": 21,
"w": 24,
"x": 0,
"y": 22
},
"id": 6,
"options": {
"legend": {
"calcs": [
"mean",
"max",
"min"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"maxHeight": 600,
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "time_series",
"rawQuery": true,
"rawSql": "SELECT\n\t$__timeGroup(date, $interval) AS time,\n\tconvert_km((sum(${preferred_range}_battery_range_km) / sum(battery_level) * 100 ) - (sum(${preferred_range}_battery_range_km) / sum(battery_level) * 100 * (avg(battery_level)-avg(coalesce(usable_battery_level,battery_level)))/100 ), '$length_unit') AS \"Projected Range (using usable_battery_level) [$length_unit]\",\n\tconvert_km(max(${preferred_range}_battery_range_km) / max(battery_level) * 100, '$length_unit') AS \"Projected Range (using battery_level)[$length_unit]\"\nFROM\n\t(\n select battery_level, usable_battery_level, date,\n rated_battery_range_km, ideal_battery_range_km, outside_temp\n from positions\n where\n car_id = $car_id and $__timeFilter(date) and ideal_battery_range_km is not null\n union all\n select battery_level, coalesce(usable_battery_level,battery_level) as usable_battery_level, date,\n rated_battery_range_km, ideal_battery_range_km, outside_temp\n from charges c\n join\n charging_processes p ON p.id = c.charging_process_id \n where\n $__timeFilter(date) and p.car_id = $car_id\n ) as data\n\nGROUP BY\n\t1\nORDER BY\n\t1,2 DESC",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "time_series",
"rawQuery": true,
"rawSql": "select \n\t$__timeGroup(date, $interval) AS time,\n avg(battery_level) AS \"Battery Level [%]\", avg(coalesce(usable_battery_level, battery_level)) as \"Usable Battery Level [%]\"\nfrom\n (SELECT\n battery_level, usable_battery_level\n , date\n FROM\n positions\n WHERE\n car_id = $car_id AND\n $__timeFilter(date) and ideal_battery_range_km is not null\n UNION ALL\n select\n battery_level, null as usable_battery_level\n , date\n from charges c\njoin\n charging_processes p ON p.id = c.charging_process_id \nWHERE\n $__timeFilter(date) and\n p.car_id = $car_id) as data\n\nGROUP BY\n 1\nORDER BY\n 1 ASC",
"refId": "B",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Projected Range - Battery Level",
"type": "timeseries"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Projected Range",
"axisPlacement": "auto",
"barAlignment": 0,
"barWidthFactor": 0.6,
"drawStyle": "line",
"fillOpacity": 30,
"gradientMode": "opacity",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "stepAfter",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": true,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"decimals": 0,
"links": [],
"mappings": [],
"min": 200,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
},
"unit": "none"
},
"overrides": [
{
"matcher": {
"id": "byRegexp",
"options": "/Temp.*/"
},
"properties": [
{
"id": "custom.fillOpacity",
"value": 0
},
{
"id": "custom.axisLabel",
"value": "Temp"
},
{
"id": "min"
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": "/.*using usable_battery_level.*/"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "#56A64B",
"mode": "fixed"
}
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": "/.*using battery_level.*/"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "#C8F2C2",
"mode": "fixed"
}
}
]
}
]
},
"gridPos": {
"h": 21,
"w": 24,
"x": 0,
"y": 43
},
"id": 5,
"options": {
"legend": {
"calcs": [
"mean",
"max",
"min"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"hideZeros": false,
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "time_series",
"rawQuery": true,
"rawSql": "\nSELECT\n\t$__timeGroup(date, $interval) AS time,\n\tconvert_km((sum(${preferred_range}_battery_range_km) / sum(battery_level) * 100 ) - (sum(${preferred_range}_battery_range_km) / sum(battery_level) * 100 * (avg(battery_level)-avg(coalesce(usable_battery_level,battery_level)))/100 ), '$length_unit') AS \"Projected Range (using usable_battery_level) [$length_unit]\",\n\tconvert_km(max(${preferred_range}_battery_range_km) / max(battery_level) * 100, '$length_unit') AS \"Projected Range (using battery_level)[$length_unit]\"\nFROM\n\t(\n select battery_level, usable_battery_level, date,\n rated_battery_range_km, ideal_battery_range_km, outside_temp\n from positions\n where\n car_id = $car_id and $__timeFilter(date) and ideal_battery_range_km is not null\n union all\n select battery_level, coalesce(usable_battery_level,battery_level) as usable_battery_level, date,\n rated_battery_range_km, ideal_battery_range_km, outside_temp\n from charges c\n join\n charging_processes p ON p.id = c.charging_process_id \n where\n $__timeFilter(date) and p.car_id = $car_id\n ) as data\n\nGROUP BY\n\t1\nORDER BY\n\t1,2 DESC",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "time_series",
"rawQuery": true,
"rawSql": "SELECT\n\t$__timeGroup(date, $interval) AS time,\n\tavg(convert_celsius(outside_temp, '$temp_unit')) as \"Outdoor Temperature [°$temp_unit]\"\n\nFROM\n\tpositions\nWHERE\n\t$__timeFilter(date) and\n\tcar_id = $car_id and ideal_battery_range_km is not null\nGROUP BY\n\t1\nORDER BY\n\t1,2 DESC",
"refId": "B",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Projected Range - Outdoor Temp",
"type": "timeseries"
}
],
"preload": false,
"refresh": "",
"schemaVersion": 41,
"tags": [
"tesla"
],
"templating": {
"list": [
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"hide": 2,
"includeAll": true,
"label": "Car",
"name": "car_id",
"options": [],
"query": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select unit_of_length from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "length_unit",
"options": [],
"query": "select unit_of_length from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select preferred_range from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "preferred_range",
"options": [],
"query": "select preferred_range from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select unit_of_temperature from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "temp_unit",
"options": [],
"query": "select unit_of_temperature from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select base_url from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "base_url",
"options": [],
"query": "select base_url from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"auto": false,
"auto_count": 30,
"auto_min": "10s",
"current": {
"text": "6h",
"value": "6h"
},
"hide": 1,
"label": "Time Resolution",
"name": "interval",
"options": [
{
"selected": false,
"text": "5m",
"value": "5m"
},
{
"selected": false,
"text": "15m",
"value": "15m"
},
{
"selected": false,
"text": "30m",
"value": "30m"
},
{
"selected": false,
"text": "1h",
"value": "1h"
},
{
"selected": false,
"text": "3h",
"value": "3h"
},
{
"selected": true,
"text": "6h",
"value": "6h"
}
],
"query": "5m,15m,30m,1h,3h,6h",
"refresh": 2,
"type": "interval"
}
]
},
"time": {
"from": "now-6M",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Projected Range",
"uid": "riqUfXgRz",
"version": 1
}

View file

@ -0,0 +1,528 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard-teslamate-states
namespace: monitoring
labels:
grafana_dashboard: "1"
annotations:
grafana_folder: "TeslaMate"
data:
teslamate-states.json: |
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [
{
"icon": "dashboard",
"tags": [],
"title": "TeslaMate",
"tooltip": "",
"type": "link",
"url": "${base_url:raw}"
},
{
"asDropdown": true,
"icon": "external link",
"tags": [
"tesla"
],
"title": "Dashboards",
"type": "dashboards"
}
],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 16,
"panels": [],
"repeat": "car_id",
"title": "$car_id",
"type": "row"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"description": "Only distinguishes between online, offline and asleep.",
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#c7d0d9",
"value": 0
}
]
},
"unit": "dateTimeAsLocal"
},
"overrides": []
},
"gridPos": {
"h": 3,
"w": 12,
"x": 0,
"y": 1
},
"id": 2,
"maxDataPoints": 100,
"options": {
"colorMode": "value",
"fieldOptions": {
"calcs": [
"mean"
]
},
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "/^time$/",
"values": true
},
"showPercentChange": false,
"textMode": "value",
"wideLayout": true
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "select $__time(start_date), state from states where car_id = $car_id order by start_date desc limit 1;",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Last state change",
"type": "stat"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"description": "Only distinguishes between online, offline and asleep.",
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#c7d0d9",
"value": 0
}
]
},
"unit": "none"
},
"overrides": []
},
"gridPos": {
"h": 3,
"w": 6,
"x": 12,
"y": 1
},
"id": 6,
"maxDataPoints": 100,
"options": {
"colorMode": "value",
"fieldOptions": {
"calcs": [
"first"
]
},
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "/^state$/",
"values": true
},
"showPercentChange": false,
"textMode": "value",
"wideLayout": true
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "select $__time(start_date), state from states where car_id = $car_id order by start_date desc limit 1;",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Current State",
"type": "stat"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"description": "based on any data ever recorded.",
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"match": "null",
"result": {
"text": "N/A"
}
},
"type": "special"
}
],
"max": 1,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#c7d0d9",
"value": 0
}
]
},
"unit": "percentunit"
},
"overrides": []
},
"gridPos": {
"h": 3,
"w": 6,
"x": 18,
"y": 1
},
"id": 8,
"maxDataPoints": 100,
"options": {
"colorMode": "value",
"fieldOptions": {
"calcs": [
"lastNotNull"
]
},
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": true
},
"showPercentChange": false,
"textMode": "value",
"wideLayout": true
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "time_series",
"rawQuery": true,
"rawSql": "select 1 - sum(duration_min) / (EXTRACT(EPOCH FROM (max(end_date) - min(start_date))) / 60), 1 as time from drives where car_id = $car_id;",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "parked (%)",
"type": "stat"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"axisPlacement": "auto",
"fillOpacity": 100,
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineWidth": 0,
"spanNulls": false
},
"mappings": [
{
"options": {
"0": {
"color": "#6ED0E0",
"index": 0,
"text": "online"
},
"1": {
"color": "#8F3BB8",
"index": 1,
"text": "driving"
},
"2": {
"color": "#F2CC0C",
"index": 2,
"text": "charging"
},
"3": {
"color": "#FFB357",
"index": 3,
"text": "offline"
},
"4": {
"color": "#56A64B",
"index": 4,
"text": "asleep"
},
"5": {
"color": "#6ED0E0",
"index": 5,
"text": "online"
},
"6": {
"color": "#E02F44",
"index": 6,
"text": "updating"
},
"null": {
"index": 7,
"text": "N/A"
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 24,
"x": 0,
"y": 4
},
"id": 14,
"options": {
"alignValue": "center",
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"mergeValues": true,
"rowHeight": 0.9,
"showValue": "auto",
"tooltip": {
"hideZeros": false,
"maxHeight": 600,
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "time_series",
"rawQuery": true,
"rawSql": "WITH states AS (\n SELECT\n unnest(ARRAY [start_date + interval '1 second', end_date]) AS date,\n unnest(ARRAY [2, 0]) AS state\n FROM charging_processes\n WHERE\n car_id = $car_id AND \n ($__timeFrom() :: timestamp - interval '30 day') < start_date AND \n (end_date < ($__timeTo() :: timestamp + interval '30 day') OR end_date IS NULL)\n UNION\n SELECT\n unnest(ARRAY [start_date + interval '1 second', end_date]) AS date,\n unnest(ARRAY [1, 0]) AS state\n FROM drives\n WHERE\n car_id = $car_id AND \n ($__timeFrom() :: timestamp - interval '30 day') < start_date AND \n (end_date < ($__timeTo() :: timestamp + interval '30 day') OR end_date IS NULL)\n UNION\n SELECT\n start_date AS date,\n CASE\n WHEN state = 'offline' THEN 3\n WHEN state = 'asleep' THEN 4\n WHEN state = 'online' THEN 5\n END AS state\n FROM states\n WHERE\n car_id = $car_id AND \n ($__timeFrom() :: timestamp - interval '30 day') < start_date AND \n (end_date < ($__timeTo() :: timestamp + interval '30 day') OR end_date IS NULL)\n UNION\n SELECT\n unnest(ARRAY [start_date + interval '1 second', end_date]) AS date,\n unnest(ARRAY [6, 0]) AS state\n FROM updates\n WHERE\n car_id = $car_id AND \n ($__timeFrom() :: timestamp - interval '30 day') < start_date AND \n (end_date < ($__timeTo() :: timestamp + interval '30 day') OR end_date IS NULL)\n)\nSELECT date AS \"time\", state\nFROM states\nWHERE \n date IS NOT NULL AND\n ($__timeFrom() :: timestamp - interval '30 day') < date AND \n date < ($__timeTo() :: timestamp + interval '30 day') \nORDER BY date ASC, state ASC;",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "States",
"type": "state-timeline"
}
],
"preload": false,
"refresh": "",
"schemaVersion": 41,
"tags": [
"tesla"
],
"templating": {
"list": [
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"hide": 2,
"includeAll": true,
"label": "Car",
"name": "car_id",
"options": [],
"query": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select base_url from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "base_url",
"options": [],
"query": "select base_url from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
}
]
},
"time": {
"from": "now-2d",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "States",
"uid": "xo4BNRkZz",
"version": 1
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,619 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard-teslamate-updates
namespace: monitoring
labels:
grafana_dashboard: "1"
annotations:
grafana_folder: "TeslaMate"
data:
teslamate-updates.json: |
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [
{
"icon": "dashboard",
"tags": [],
"title": "TeslaMate",
"tooltip": "",
"type": "link",
"url": "${base_url:raw}"
},
{
"asDropdown": true,
"icon": "external link",
"tags": [
"tesla"
],
"title": "Dashboards",
"type": "dashboards"
}
],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 4,
"panels": [],
"repeat": "car_id",
"title": "$car_id",
"type": "row"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"fieldConfig": {
"defaults": {
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#c7d0d9",
"value": 0
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 3,
"w": 8,
"x": 0,
"y": 1
},
"id": 8,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"count"
],
"fields": "",
"values": true
},
"showPercentChange": false,
"textMode": "value",
"wideLayout": true
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT count(*)\nFROM updates\nWHERE $__timeFilter(start_date) AND car_id = $car_id",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Updates",
"type": "stat"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"fieldConfig": {
"defaults": {
"decimals": 1,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "#c7d0d9",
"value": 0
}
]
},
"unit": "dtdurations"
},
"overrides": []
},
"gridPos": {
"h": 3,
"w": 16,
"x": 8,
"y": 1
},
"id": 6,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": true
},
"showPercentChange": false,
"textMode": "value",
"wideLayout": true
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT percentile_disc(0.5) WITHIN GROUP (ORDER BY since_last_update) FROM (\n\tSELECT extract(EPOCH FROM start_date - lag(start_date) OVER (ORDER BY start_date)) AS since_last_update\n\tFROM updates\n\tWHERE $__timeFilter(start_date) AND car_id = $car_id\n) d;",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Median time between updates",
"type": "stat"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"fieldConfig": {
"defaults": {
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"filterable": false,
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "time"
},
"properties": [
{
"id": "custom.minWidth",
"value": 210
},
{
"id": "displayName",
"value": "Date"
},
{
"id": "unit",
"value": "dateTimeAsLocal"
}
]
},
{
"matcher": {
"id": "byName",
"options": "update_duration"
},
"properties": [
{
"id": "custom.minWidth",
"value": 120
},
{
"id": "displayName",
"value": "Duration"
},
{
"id": "unit",
"value": "dtdurations"
}
]
},
{
"matcher": {
"id": "byName",
"options": "since_last_update"
},
"properties": [
{
"id": "custom.minWidth",
"value": 180
},
{
"id": "displayName",
"value": "Since Previous Update"
}
]
},
{
"matcher": {
"id": "byName",
"options": "version"
},
"properties": [
{
"id": "displayName",
"value": "Installed Version"
},
{
"id": "custom.align",
"value": "right"
},
{
"id": "decimals",
"value": 2
},
{
"id": "links",
"value": [
{
"targetBlank": true,
"title": "${__data.fields[version]} release notes",
"url": "https://www.notateslaapp.com/software-updates/version/${__data.fields[version]}/release-notes"
}
]
},
{
"id": "unit",
"value": "string"
},
{
"id": "custom.minWidth",
"value": 150
}
]
},
{
"matcher": {
"id": "byName",
"options": "chg_ct"
},
"properties": [
{
"id": "custom.minWidth",
"value": 120
},
{
"id": "displayName",
"value": "# of Charges"
}
]
},
{
"matcher": {
"id": "byName",
"options": "avg_ideal_range_km"
},
"properties": [
{
"id": "custom.minWidth",
"value": 130
},
{
"id": "decimals",
"value": 1
},
{
"id": "unit",
"value": "lengthkm"
},
{
"id": "displayName",
"value": "Ø Ideal range"
}
]
},
{
"matcher": {
"id": "byName",
"options": "avg_rated_range_km"
},
"properties": [
{
"id": "custom.minWidth",
"value": 130
},
{
"id": "decimals",
"value": 1
},
{
"id": "unit",
"value": "lengthkm"
},
{
"id": "displayName",
"value": "Ø Rated range"
}
]
},
{
"matcher": {
"id": "byName",
"options": "avg_ideal_range_mi"
},
"properties": [
{
"id": "custom.minWidth",
"value": 130
},
{
"id": "decimals",
"value": 1
},
{
"id": "unit",
"value": "lengthmi"
},
{
"id": "displayName",
"value": "Ø Ideal range"
}
]
},
{
"matcher": {
"id": "byName",
"options": "avg_rated_range_mi"
},
"properties": [
{
"id": "custom.minWidth",
"value": 130
},
{
"id": "decimals",
"value": 1
},
{
"id": "unit",
"value": "lengthmi"
},
{
"id": "displayName",
"value": "Ø Rated range"
}
]
}
]
},
"gridPos": {
"h": 28,
"w": 24,
"x": 0,
"y": 4
},
"id": 2,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": [
{
"desc": true,
"displayName": "Date"
}
]
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "with u as (\r\n select *, coalesce(lag(start_date) over(order by start_date desc), now()) as next_start_date \r\n from updates\r\n where car_id = $car_id and $__timeFilter(start_date)\r\n),\r\nrng as (\r\n SELECT\r\n\t date_trunc('hour', timezone('UTC', date), '$__timezone') AS date,\r\n\t (sum(${preferred_range}_battery_range_km)/ nullif(sum(usable_battery_level),0) * 100 ) AS \"battery_rng\",\r\n\t sum(case when action = 'Charge' then 1 else 0 end) as chg_ct\r\n FROM (\r\n select usable_battery_level, start_date as date, start_rated_range_km as rated_battery_range_km, start_ideal_range_km as ideal_battery_range_km, 'Drive' as action\r\n from drives d\r\n inner join positions p on d.start_position_id = p.id \r\n where d.car_id = $car_id and $__timeFilter(start_date) and usable_battery_level > 0\r\n union all\r\n select end_battery_level as usable_battery_level, end_date, end_rated_range_km as rated_battery_range_km, end_ideal_range_km as ideal_battery_range_km, 'Charge' as action\r\n from charging_processes p\r\n where $__timeFilter(end_date) and p.car_id = $car_id\r\n ) as data\r\n GROUP BY 1\r\n)\r\n\r\nselect\t\r\n u.start_date as time,\r\n\textract(EPOCH FROM u.end_date - u.start_date) AS update_duration,\r\n\tage(date(u.start_date), date(lag(u.start_date) OVER (ORDER BY u.start_date))) AS since_last_update,\r\n\tsplit_part(u.version, ' ', 1) as version,\r\n\tsum(r.chg_ct) as chg_ct,\r\n\tconvert_km(avg(r.battery_rng), '$length_unit')::numeric(6,2) AS avg_${preferred_range}_range_${length_unit}\r\nfrom u u\r\nleft join rng r\r\n\tON r.date between u.start_date and u.next_start_date\r\ngroup by u.car_id,\r\n\tu.start_date,\r\n\tu.end_date,\r\n\tnext_start_date,\r\n\tsplit_part(u.version, ' ', 1)",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Updates",
"type": "table"
}
],
"preload": false,
"refresh": "",
"schemaVersion": 41,
"tags": [
"tesla"
],
"templating": {
"list": [
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"hide": 2,
"includeAll": true,
"label": "Car",
"name": "car_id",
"options": [],
"query": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select preferred_range from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "preferred_range",
"options": [],
"query": "select preferred_range from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select unit_of_length from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "length_unit",
"options": [],
"query": "select unit_of_length from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select base_url from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "base_url",
"options": [],
"query": "select base_url from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
}
]
},
"time": {
"from": "now-10y",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Updates",
"uid": "IiC07mgWz",
"version": 1
}

View file

@ -0,0 +1,666 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard-teslamate-vampire-drain
namespace: monitoring
labels:
grafana_dashboard: "1"
annotations:
grafana_folder: "TeslaMate"
data:
teslamate-vampire-drain.json: |
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [
{
"icon": "dashboard",
"tags": [],
"title": "TeslaMate",
"tooltip": "",
"type": "link",
"url": "${base_url:raw}"
},
{
"asDropdown": true,
"icon": "external link",
"tags": [
"tesla"
],
"title": "Dashboards",
"type": "dashboards"
}
],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 4,
"panels": [],
"repeat": "car_id",
"title": "$car_id",
"type": "row"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "start_date"
},
"properties": [
{
"id": "displayName",
"value": "Start"
},
{
"id": "unit",
"value": "dateTimeAsLocal"
},
{
"id": "links",
"value": [
{
"targetBlank": false,
"title": "",
"url": "/d/zm7wN6Zgz/drive-details?from=${__data.fields.start_date_ts.numeric}&to=${__data.fields.end_date_ts.numeric}"
}
]
},
{
"id": "custom.minWidth",
"value": 210
}
]
},
{
"matcher": {
"id": "byName",
"options": "end_date"
},
"properties": [
{
"id": "displayName",
"value": "End"
},
{
"id": "unit",
"value": "dateTimeAsLocal"
},
{
"id": "custom.minWidth",
"value": 210
}
]
},
{
"matcher": {
"id": "byName",
"options": "range_diff_km"
},
"properties": [
{
"id": "displayName",
"value": "Range loss"
},
{
"id": "unit",
"value": "lengthkm"
},
{
"id": "decimals",
"value": 2
},
{
"id": "custom.minWidth",
"value": 95
}
]
},
{
"matcher": {
"id": "byName",
"options": "duration"
},
"properties": [
{
"id": "displayName",
"value": "Period"
},
{
"id": "unit",
"value": "s"
},
{
"id": "decimals",
"value": 1
},
{
"id": "custom.cellOptions",
"value": {
"type": "color-text"
}
},
{
"id": "thresholds",
"value": {
"mode": "absolute",
"steps": [
{
"color": "rgb(133, 142, 133)",
"value": 0
},
{
"color": "#56A64B",
"value": 43200
}
]
}
},
{
"id": "custom.minWidth",
"value": 100
}
]
},
{
"matcher": {
"id": "byName",
"options": "range_lost_per_hour_km"
},
"properties": [
{
"id": "displayName",
"value": "Ø Range loss / h"
},
{
"id": "unit",
"value": "lengthkm"
},
{
"id": "decimals",
"value": 2
},
{
"id": "custom.minWidth",
"value": 135
}
]
},
{
"matcher": {
"id": "byRegexp",
"options": "/.*_ts/"
},
"properties": [
{
"id": "custom.hidden",
"value": true
}
]
},
{
"matcher": {
"id": "byName",
"options": "standby"
},
"properties": [
{
"id": "displayName",
"value": "Standby"
},
{
"id": "unit",
"value": "percentunit"
},
{
"id": "custom.cellOptions",
"value": {
"type": "color-text"
}
},
{
"id": "thresholds",
"value": {
"mode": "absolute",
"steps": [
{
"color": "#FF7383",
"value": 0
},
{
"color": "#FFB357",
"value": 0.3
},
{
"color": "#56A64B",
"value": 0.85
}
]
}
},
{
"id": "decimals",
"value": 0
},
{
"id": "custom.minWidth",
"value": 75
}
]
},
{
"matcher": {
"id": "byName",
"options": "consumption"
},
"properties": [
{
"id": "displayName",
"value": "Energy drained"
},
{
"id": "unit",
"value": "kwatth"
},
{
"id": "decimals",
"value": 2
},
{
"id": "custom.minWidth",
"value": 125
}
]
},
{
"matcher": {
"id": "byName",
"options": "avg_power"
},
"properties": [
{
"id": "displayName",
"value": "Ø Power"
},
{
"id": "unit",
"value": "watt"
},
{
"id": "decimals",
"value": 1
},
{
"id": "custom.minWidth",
"value": 80
}
]
},
{
"matcher": {
"id": "byName",
"options": "range_lost_per_hour_mi"
},
"properties": [
{
"id": "displayName",
"value": "Ø Range loss / h"
},
{
"id": "unit",
"value": "lengthmi"
},
{
"id": "decimals",
"value": 2
},
{
"id": "custom.minWidth",
"value": 135
}
]
},
{
"matcher": {
"id": "byName",
"options": "range_diff_mi"
},
"properties": [
{
"id": "displayName",
"value": "Range loss"
},
{
"id": "unit",
"value": "lengthmi"
},
{
"id": "decimals",
"value": 2
},
{
"id": "custom.minWidth",
"value": 95
}
]
},
{
"matcher": {
"id": "byName",
"options": "soc_diff"
},
"properties": [
{
"id": "displayName",
"value": "SoC Diff"
},
{
"id": "unit",
"value": "percent"
},
{
"id": "custom.minWidth",
"value": 80
}
]
},
{
"matcher": {
"id": "byName",
"options": "has_reduced_range"
},
"properties": [
{
"id": "displayName",
"value": " "
},
{
"id": "custom.align",
"value": "center"
},
{
"id": "mappings",
"value": [
{
"options": {
"0": {
"color": "transparent",
"index": 1,
"text": " "
},
"1": {
"color": "dark-blue",
"index": 0,
"text": "❄"
}
},
"type": "value"
}
]
},
{
"id": "links",
"value": [
{
"title": "In cold weather, the estimated range loss cannot be estimated correctly and is therefore hidden.",
"url": ""
}
]
},
{
"id": "custom.width",
"value": 50
}
]
}
]
},
"gridPos": {
"h": 23,
"w": 24,
"x": 0,
"y": 1
},
"id": 2,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "with merge as (\n SELECT \n c.start_date AS start_date,\n c.end_date AS end_date,\n c.start_ideal_range_km AS start_ideal_range_km,\n c.end_ideal_range_km AS end_ideal_range_km,\n c.start_rated_range_km AS start_rated_range_km,\n c.end_rated_range_km AS end_rated_range_km,\n start_battery_level,\n end_battery_level,\n p.usable_battery_level AS start_usable_battery_level,\n NULL AS end_usable_battery_level,\n p.odometer AS start_km,\n p.odometer AS end_km\n FROM charging_processes c\n JOIN positions p ON c.position_id = p.id\n WHERE c.car_id = $car_id AND $__timeFilter(start_date)\n UNION\n SELECT \n d.start_date AS start_date,\n d.end_date AS end_date,\n d.start_ideal_range_km AS start_ideal_range_km,\n d.end_ideal_range_km AS end_ideal_range_km,\n d.start_rated_range_km AS start_rated_range_km,\n d.end_rated_range_km AS end_rated_range_km,\n start_position.battery_level AS start_battery_level,\n end_position.battery_level AS end_battery_level,\n start_position.usable_battery_level AS start_usable_battery_level,\n end_position.usable_battery_level AS end_usable_battery_level,\n d.start_km AS start_km,\n d.end_km AS end_km\n FROM drives d\n JOIN positions start_position ON d.start_position_id = start_position.id\n JOIN positions end_position ON d.end_position_id = end_position.id\n WHERE d.car_id = $car_id AND $__timeFilter(start_date)\n), \nv as (\n SELECT\n lag(t.end_date) OVER w AS start_date,\n t.start_date AS end_date,\n lag(t.end_${preferred_range}_range_km) OVER w AS start_range,\n t.start_${preferred_range}_range_km AS end_range,\n lag(t.end_km) OVER w AS start_km,\n t.start_km AS end_km,\n EXTRACT(EPOCH FROM age(t.start_date, lag(t.end_date) OVER w)) AS duration,\n lag(t.end_battery_level) OVER w AS start_battery_level,\n lag(t.end_usable_battery_level) OVER w AS start_usable_battery_level,\n\t\tstart_battery_level AS end_battery_level,\n\t\tstart_usable_battery_level AS end_usable_battery_level,\n\t\tstart_battery_level > COALESCE(start_usable_battery_level, start_battery_level) AS has_reduced_range\n FROM merge t\n WINDOW w AS (ORDER BY t.start_date ASC)\n ORDER BY start_date DESC\n)\n\nSELECT\n round(extract(epoch FROM v.start_date)) * 1000 AS start_date_ts,\n round(extract(epoch FROM v.end_date)) * 1000 AS end_date_ts,\n -- Columns\n v.start_date,\n v.end_date,\n v.duration,\n (coalesce(s_asleep.sleep, 0) + coalesce(s_offline.sleep, 0)) / v.duration as standby,\n\t-greatest(v.start_battery_level - v.end_battery_level, 0) as soc_diff,\n\tCASE WHEN has_reduced_range THEN 1 ELSE 0 END as has_reduced_range,\n\tconvert_km(CASE WHEN has_reduced_range THEN NULL ELSE (v.start_range - v.end_range)::numeric END, '$length_unit') AS range_diff_$length_unit,\n CASE WHEN has_reduced_range THEN NULL ELSE (v.start_range - v.end_range) * c.efficiency END AS consumption,\n CASE WHEN has_reduced_range THEN NULL ELSE ((v.start_range - v.end_range) * c.efficiency) / (v.duration / 3600) * 1000 END as avg_power,\n convert_km(CASE WHEN has_reduced_range THEN NULL ELSE ((v.start_range - v.end_range) / (v.duration / 3600))::numeric END, '$length_unit') AS range_lost_per_hour_${length_unit}\nFROM v,\n LATERAL (\n SELECT EXTRACT(EPOCH FROM sum(age(s.end_date, s.start_date))) as sleep\n FROM states s\n WHERE\n state = 'asleep' AND\n v.start_date <= s.start_date AND s.end_date <= v.end_date AND\n s.car_id = $car_id\n ) s_asleep,\n LATERAL (\n SELECT EXTRACT(EPOCH FROM sum(age(s.end_date, s.start_date))) as sleep\n FROM states s\n WHERE\n state = 'offline' AND\n v.start_date <= s.start_date AND s.end_date <= v.end_date AND\n s.car_id = $car_id\n ) s_offline\nJOIN cars c ON c.id = $car_id\nWHERE\n v.duration > ($duration * 60 * 60)\n AND v.start_range - v.end_range >= 0\n AND v.end_km - v.start_km < 1;",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "Vampire Drain",
"transformations": [
{
"id": "merge",
"options": {
"reducers": []
}
}
],
"type": "table"
}
],
"preload": false,
"refresh": "",
"schemaVersion": 41,
"tags": [
"tesla"
],
"templating": {
"list": [
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"hide": 2,
"includeAll": true,
"label": "Car",
"name": "car_id",
"options": [],
"query": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {
"text": "6",
"value": "6"
},
"includeAll": false,
"label": "min. Idle Time (h)",
"name": "duration",
"options": [
{
"selected": false,
"text": "0",
"value": "0"
},
{
"selected": false,
"text": "1",
"value": "1"
},
{
"selected": false,
"text": "3",
"value": "3"
},
{
"selected": true,
"text": "6",
"value": "6"
},
{
"selected": false,
"text": "12",
"value": "12"
},
{
"selected": false,
"text": "18",
"value": "18"
},
{
"selected": false,
"text": "24",
"value": "24"
}
],
"query": "0,1,3,6,12,18,24",
"type": "custom"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select unit_of_length from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "length_unit",
"options": [],
"query": "select unit_of_length from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select preferred_range from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "preferred_range",
"options": [],
"query": "select preferred_range from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select base_url from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "base_url",
"options": [],
"query": "select base_url from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
}
]
},
"time": {
"from": "now-90d",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Vampire Drain",
"uid": "zhHx2Fggk",
"version": 1
}

View file

@ -0,0 +1,571 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard-teslamate-visited
namespace: monitoring
labels:
grafana_dashboard: "1"
annotations:
grafana_folder: "TeslaMate"
data:
teslamate-visited.json: |
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [
{
"icon": "dashboard",
"tags": [],
"title": "TeslaMate",
"tooltip": "",
"type": "link",
"url": "${base_url:raw}"
},
{
"asDropdown": true,
"icon": "external link",
"tags": [
"tesla"
],
"title": "Dashboards",
"type": "dashboards"
}
],
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 4,
"panels": [],
"repeat": "car_id",
"title": "$car_id",
"type": "row"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "dark-red",
"value": 0
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 21,
"w": 24,
"x": 0,
"y": 1
},
"id": 2,
"maxDataPoints": 10000000,
"options": {
"basemap": {
"config": {},
"name": "Layer 0",
"type": "osm-standard"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"arrow": 0,
"style": {
"color": {
"fixed": "dark-blue"
},
"lineWidth": 2,
"opacity": 1,
"rotation": {
"fixed": 0,
"max": 360,
"min": -360,
"mode": "mod"
},
"size": {
"fixed": 3,
"max": 15,
"min": 2
},
"symbol": {
"fixed": "img/icons/marker/circle.svg",
"mode": "fixed"
},
"symbolAlign": {
"horizontal": "center",
"vertical": "center"
},
"textConfig": {
"fontSize": 12,
"offsetX": 0,
"offsetY": 0,
"textAlign": "center",
"textBaseline": "middle"
}
}
},
"location": {
"latitude": "lat",
"longitude": "long",
"mode": "auto"
},
"name": "Layer 1",
"tooltip": true,
"type": "route"
}
],
"tooltip": {
"mode": "none"
},
"view": {
"allLayers": true,
"id": "fit",
"lat": 0,
"lon": 0,
"zoom": 15
}
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT\n date_trunc('minute', timezone('UTC', date), '$__timezone') as time,\n avg(latitude) as latitude,\n avg(longitude) as longitude\nFROM\n positions\nWHERE\n car_id = $car_id AND $__timeFilter(date) and ideal_battery_range_km is not null\nGROUP BY 1\nORDER BY 1",
"refId": "Positions",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "",
"type": "geomap"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "super-light-blue",
"value": 0
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 2,
"w": 5,
"x": 0,
"y": 22
},
"id": 5,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "/.*/",
"values": true
},
"showPercentChange": false,
"textMode": "value_and_name",
"wideLayout": true
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT ROUND(convert_km((max(end_km) - min(start_km))::numeric, '$length_unit'),0)|| ' $length_unit' as \"Mileage\"\nFROM drives WHERE car_id = $car_id AND $__timeFilter(start_date)",
"refId": "distance traveled",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "",
"type": "stat"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Total Energy added"
},
"properties": [
{
"id": "unit",
"value": "kwatth"
}
]
},
{
"matcher": {
"id": "byName",
"options": "Total Energy used"
},
"properties": [
{
"id": "unit",
"value": "kwatth"
}
]
},
{
"matcher": {
"id": "byName",
"options": "Charging Efficiency"
},
"properties": [
{
"id": "unit",
"value": "percent"
}
]
}
]
},
"gridPos": {
"h": 2,
"w": 14,
"x": 5,
"y": 22
},
"id": 6,
"maxDataPoints": 100,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "center",
"orientation": "vertical",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"mean"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "value_and_name",
"wideLayout": true
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT\n\tsum(charge_energy_added) as \"Total Energy added\",\n\tSUM(greatest(charge_energy_added, charge_energy_used)) AS \"Total Energy used\",\n\tSUM(charge_energy_added) * 100 / SUM(greatest(charge_energy_added, charge_energy_used)) AS \"Charging Efficiency\"\nFROM\n\tcharging_processes\nWHERE\n\tcar_id = $car_id AND $__timeFilter(start_date) AND charge_energy_added > 0.01",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "",
"type": "stat"
},
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"description": "",
"fieldConfig": {
"defaults": {
"decimals": 2,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": 0
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 2,
"w": 5,
"x": 19,
"y": 22
},
"id": 7,
"maxDataPoints": 100,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"percentChangeColorMode": "standard",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showPercentChange": false,
"textMode": "value_and_name",
"wideLayout": true
},
"pluginVersion": "12.1.1",
"targets": [
{
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "select sum(cost) as \"Total Charging Cost\" from charging_processes where $__timeFilter(start_date) AND car_id = $car_id;",
"refId": "A",
"sql": {
"columns": [
{
"parameters": [],
"type": "function"
}
],
"groupBy": [
{
"property": {
"type": "string"
},
"type": "groupBy"
}
],
"limit": 50
}
}
],
"title": "",
"type": "stat"
}
],
"preload": false,
"refresh": "",
"schemaVersion": 41,
"tags": [
"tesla"
],
"templating": {
"list": [
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"hide": 2,
"includeAll": true,
"label": "Car",
"name": "car_id",
"options": [],
"query": "SELECT\n id as __value,\n CASE WHEN COUNT(id) OVER (PARTITION BY name) > 1 AND name IS NOT NULL THEN CONCAT(name, ' - ', RIGHT(vin, 6)) ELSE COALESCE(name, CONCAT('VIN ', vin)) end as __text \nFROM cars\nORDER BY display_priority ASC, name ASC, vin ASC;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "select base_url from settings limit 1;",
"hide": 2,
"includeAll": false,
"name": "base_url",
"options": [],
"query": "select base_url from settings limit 1;",
"refresh": 1,
"regex": "",
"type": "query"
},
{
"current": {},
"datasource": {
"type": "grafana-postgresql-datasource",
"uid": "TeslaMate"
},
"definition": "SELECT unit_of_length FROM settings LIMIT 1",
"hide": 2,
"includeAll": false,
"name": "length_unit",
"options": [],
"query": "SELECT unit_of_length FROM settings LIMIT 1",
"refresh": 1,
"regex": "",
"type": "query"
}
]
},
"time": {
"from": "now-90d",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Visited",
"uid": "RG_DxSmgk",
"version": 1
}

View file

@ -15,3 +15,22 @@ resources:
- dashboards/configmap-postgresql.yaml
- dashboards/configmap-services.yaml
- dashboards/configmap-zot.yaml
# TeslaMate dashboards
- dashboards/configmap-teslamate-overview.yaml
- dashboards/configmap-teslamate-charges.yaml
- dashboards/configmap-teslamate-drives.yaml
- dashboards/configmap-teslamate-efficiency.yaml
- dashboards/configmap-teslamate-states.yaml
- dashboards/configmap-teslamate-vampire-drain.yaml
- dashboards/configmap-teslamate-battery-health.yaml
- dashboards/configmap-teslamate-statistics.yaml
- dashboards/configmap-teslamate-charge-level.yaml
- dashboards/configmap-teslamate-updates.yaml
- dashboards/configmap-teslamate-trip.yaml
- dashboards/configmap-teslamate-locations.yaml
- dashboards/configmap-teslamate-mileage.yaml
- dashboards/configmap-teslamate-drive-stats.yaml
- dashboards/configmap-teslamate-charging-stats.yaml
- dashboards/configmap-teslamate-projected-range.yaml
- dashboards/configmap-teslamate-timeline.yaml
- dashboards/configmap-teslamate-visited.yaml

View file

@ -0,0 +1,13 @@
# TeslaMate PostgreSQL datasource password for Grafana
# Apply with: op inject -i argocd/manifests/grafana-config/secret-teslamate-datasource.yaml.tpl | kubectl apply -f -
#
# This secret is mounted as environment variables in Grafana
# The password is referenced in values.yaml datasource config as $TESLAMATE_DB_PASSWORD
apiVersion: v1
kind: Secret
metadata:
name: grafana-teslamate-datasource
namespace: monitoring
type: Opaque
stringData:
TESLAMATE_DB_PASSWORD: {{ op://blumeops/TeslaMate/db_password }}

View file

@ -8,6 +8,11 @@ admin:
userKey: admin-user
passwordKey: admin-password
# Environment variables from secrets (for datasource credentials)
envFromSecrets:
- name: grafana-teslamate-datasource
optional: true
# Persistence with PVC for SQLite database
persistence:
enabled: true
@ -44,6 +49,22 @@ datasources:
uid: loki
url: http://loki.monitoring.svc.cluster.local:3100
editable: false
- name: TeslaMate
type: postgres
access: proxy
orgId: 1
uid: TeslaMate
url: blumeops-pg-rw.databases.svc.cluster.local:5432
database: teslamate
user: teslamate
editable: false
jsonData:
sslmode: disable
maxOpenConns: 5
maxIdleConns: 2
connMaxLifetime: 14400
secureJsonData:
password: $TESLAMATE_DB_PASSWORD
# Dashboard provisioning - sidecar watches for ConfigMaps with label
sidecar:

View file

@ -0,0 +1,69 @@
# TeslaMate
TeslaMate is a self-hosted Tesla data logger that collects and visualizes vehicle data.
## Prerequisites
### 1. Create 1Password Secrets
Create two items in the blumeops 1Password vault:
1. **TeslaMate DB Password**
- Generate a secure password for the teslamate PostgreSQL user
- Add a field named `password` with the generated value
2. **TeslaMate Encryption Key**
- Generate with: `openssl rand -base64 32`
- Add a field named `key` with the generated value
- This encrypts Tesla API tokens at rest in the database
### 2. Apply Kubernetes Secrets
```bash
# Create namespace
kubectl create namespace teslamate
# Apply database user secret (for CNPG)
op inject -i argocd/manifests/databases/secret-teslamate.yaml.tpl | kubectl apply -f -
# Apply teslamate secrets
op inject -i argocd/manifests/teslamate/secret-encryption-key.yaml.tpl | kubectl apply -f -
op inject -i argocd/manifests/teslamate/secret-db.yaml.tpl | kubectl apply -f -
```
### 3. Create Database
After the teslamate user exists in PostgreSQL (sync blumeops-pg first):
```bash
PGPASSWORD=$(op --vault blumeops item get <eblume-item-id> --fields password --reveal) \
psql -h pg.tail8d86e.ts.net -U eblume -c "CREATE DATABASE teslamate OWNER teslamate;"
```
## Deployment
```bash
# Sync ArgoCD apps
argocd app sync apps
argocd app sync blumeops-pg teslamate grafana grafana-config
```
## Tesla API Setup
1. Access TeslaMate UI at https://tesla.tail8d86e.ts.net
2. Click "Sign in with Tesla"
3. Complete OAuth flow in browser
4. Tokens are encrypted and stored in database
5. Verify vehicle appears and data collection starts
## Grafana Dashboards
TeslaMate dashboards are available in Grafana at https://grafana.tail8d86e.ts.net
They use the "TeslaMate" PostgreSQL datasource (not Prometheus).
## Notes
- MQTT is disabled (can be enabled later for Home Assistant integration)
- Timezone is set to America/Los_Angeles
- Encryption key protects Tesla API tokens at rest

View file

@ -0,0 +1,62 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: teslamate
namespace: teslamate
spec:
replicas: 1
selector:
matchLabels:
app: teslamate
template:
metadata:
labels:
app: teslamate
spec:
containers:
- name: teslamate
image: teslamate/teslamate:2.2.0
ports:
- containerPort: 4000
env:
- name: DATABASE_USER
value: "teslamate"
- name: DATABASE_PASS
valueFrom:
secretKeyRef:
name: teslamate-db
key: password
- name: DATABASE_NAME
value: "teslamate"
- name: DATABASE_HOST
value: "blumeops-pg-rw.databases.svc.cluster.local"
- name: ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: teslamate-encryption
key: key
- name: DISABLE_MQTT
value: "true"
- name: CHECK_ORIGIN
value: "false"
- name: TZ
value: "America/Los_Angeles"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /
port: 4000
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /
port: 4000
initialDelaySeconds: 10
periodSeconds: 10

View file

@ -0,0 +1,17 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: teslamate-tailscale
namespace: teslamate
annotations:
tailscale.com/proxy-class: "default"
spec:
ingressClassName: tailscale
defaultBackend:
service:
name: teslamate
port:
number: 4000
tls:
- hosts:
- tesla

View file

@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: teslamate
resources:
- deployment.yaml
- service.yaml
- ingress-tailscale.yaml

View file

@ -0,0 +1,11 @@
# TeslaMate database password secret
#
# Apply with: op inject -i argocd/manifests/teslamate/secret-db.yaml.tpl | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: teslamate-db
namespace: teslamate
type: Opaque
stringData:
password: {{ op://blumeops/TeslaMate/db_password }}

View file

@ -0,0 +1,12 @@
# TeslaMate encryption key secret
# This key encrypts Tesla API tokens at rest in the database
#
# Apply with: op inject -i argocd/manifests/teslamate/secret-encryption-key.yaml.tpl | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: teslamate-encryption
namespace: teslamate
type: Opaque
stringData:
key: {{ op://blumeops/TeslaMate/api_enc_key }}

View file

@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: teslamate
namespace: teslamate
spec:
selector:
app: teslamate
ports:
- port: 4000
targetPort: 4000
type: ClusterIP