Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
7da287b
fix: null safety for Charging page and API latest handlers
atulmgupta Mar 30, 2026
4ea1244
fix: null safety for Energy and Dashboard pages
atulmgupta Mar 30, 2026
b624ff2
fix: increase telemetry DB write timeout from 5s to 30s
atulmgupta Mar 30, 2026
ac85f3e
feat: add migration 17 for comprehensive telemetry tables
atulmgupta Mar 30, 2026
53e101c
fix: throttle telemetry DB writes to every 10s per vehicle
atulmgupta Mar 30, 2026
235e13c
fix: relax telemetry tracking gate conditions
atulmgupta Mar 30, 2026
101c7a8
fix: detect vehicle state from telemetry signals instead of hardcodin…
atulmgupta Mar 30, 2026
fb75ef7
fix: align alert read field name between backend and frontend
atulmgupta Mar 30, 2026
baa1669
fix: return 200 null instead of 404 for empty latest endpoints
atulmgupta Mar 30, 2026
cd2bc98
fix: phase 1 audit - frontend crashes and safety
atulmgupta Mar 30, 2026
def01cf
fix: phase 2-3 audit - backend, telemetry, router fixes
atulmgupta Mar 30, 2026
9bba5ab
fix: align TS interfaces with Go models - add 72 missing fields
atulmgupta Mar 30, 2026
ee3ccd4
fix: telemetry memory leak and mutex cleanup
atulmgupta Mar 30, 2026
ee1b9ef
fix: remaining audit items - TS fields, query keys, dashboard staleTime
atulmgupta Mar 30, 2026
80e1588
feat: telemetry-first data source for vehicle state
atulmgupta Mar 30, 2026
2a0dba9
feat: add PostgreSQL unit conversion functions for Grafana
atulmgupta Mar 30, 2026
7597af3
fix: restore missing Drive interface declaration in api.ts
atulmgupta Mar 30, 2026
20412cf
fix: resolve TS compile errors from interface changes
atulmgupta Mar 30, 2026
92fd2fe
fix: use charging telemetry for battery level and charging state dete…
atulmgupta Mar 30, 2026
07fff87
fix: fall back to Fleet API when telemetry data is sparse/stale
atulmgupta Mar 30, 2026
10783bb
fix: battery SOC, charging detection, voltage precision
atulmgupta Mar 30, 2026
f16f88c
fix: don't discard telemetry signals when GPS location is missing
atulmgupta Mar 30, 2026
8acaf80
fix: apply user unit preferences to sidebar and Dashboard
atulmgupta Mar 30, 2026
6df4e44
fix: writeJSON writes 'null' body instead of empty body for nil data
atulmgupta Mar 30, 2026
1aa9fe3
feat: add gas price settings for EV vs ICE cost comparison
atulmgupta Mar 30, 2026
be8bbfc
feat: gas price history tracking for period-accurate comparisons
atulmgupta Mar 30, 2026
aad7a8d
feat: add EIA gas price auto-poll feature
atulmgupta Mar 30, 2026
1622d17
fix: EIA API URL typo and context cancellation in gas price worker
atulmgupta Mar 30, 2026
a404949
fix: correct EIA API endpoint path and product facet
atulmgupta Mar 30, 2026
5f920a2
fix: correct EIA API facets - product=EPMR + duoarea=NUS
atulmgupta Mar 30, 2026
6af3f9e
fix: merge recent charging telemetry records for complete state
atulmgupta Mar 30, 2026
173eb61
fix: accumulate signals across batches within throttle window
atulmgupta Mar 30, 2026
b5742be
fix: remove test files with real VIN, add to gitignore
atulmgupta Mar 30, 2026
d17b8a2
chore: clean .gitignore, add test file exclusions
atulmgupta Mar 30, 2026
772f55a
fix: null safety for all .toFixed() calls in Energy page
atulmgupta Mar 30, 2026
975942b
fix: map motor temp/current fields to DrivetrainHealth cards
atulmgupta Mar 30, 2026
9c318fa
fix: parseDateRange end date includes full day (23:59:59)
atulmgupta Mar 30, 2026
bd7abf0
chore: add test seed SQL, update gitignore
atulmgupta Mar 31, 2026
3199e00
Update Grafana dashboards to use PostgreSQL unit conversion functions
atulmgupta Mar 31, 2026
eec9bc4
Add comprehensive TeslaSync seed SQL
atulmgupta Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 25 additions & 50 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,68 +1,43 @@
# Binaries
bin/
*.exe

# Go
coverage.out
vendor/

# Frontend
web/node_modules/
web/dist/

# Environment
.env
.env.local
*.exe~
*.dll
*.so
*.dylib

# IDE
.vscode/
.idea/
.vscode/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Docker volumes
postgres_data/
grafana_data/
mosquitto_data/
redis_data/

# Temporary
tmp/
*.log

# Plan tracking
.plan/

# Helm
helm/teslasync/charts/
*.tgz

# Docs build output
docs/.vitepress/dist/
docs/.vitepress/cache/
docs/node_modules/

# Test coverage
*.out
# Go
vendor/
coverage/

# Go debug
__debug_bin*
dlv
# Node
node_modules/
dist/
.env.local
.env.*.local

# Python (for any scripts)
# TeslaSync
certs/
*.pem
*.key
.env

# Python
__pycache__/
*.pyc
.venv/

# Terraform (future)
.terraform/
*.tfstate*
# Test files
test_*.py
test_*.js
publish_test.sh
package-lock.json

# Secrets
*.pem
*.key
package.json
14 changes: 14 additions & 0 deletions cmd/teslasync/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,19 @@ func main() {
})
log.Info().Msg("maintenance worker started")

// Gas price worker — polls EIA API for US average gasoline price
var gasPriceWorker *worker.GasPriceWorker
if cfg.GasPrice.APIKey != "" {
gasPriceWorker = worker.NewGasPriceWorker(db, cfg.GasPrice)
resilience.SafeGoLoop(ctx, "gas-price-worker", func(loopCtx context.Context) {
gasPriceWorker.Start(loopCtx)
})
log.Info().
Bool("enabled", cfg.GasPrice.Enabled).
Str("poll_interval", cfg.GasPrice.PollInterval).
Msg("gas price worker started")
}

// Periodic component health checker — creates system alerts on state changes
alertRepo := database.NewAlertRepo(db)
notifRepo := database.NewNotificationRepo(db)
Expand Down Expand Up @@ -301,6 +314,7 @@ func main() {
AppVersion: Version,
Encryptor: encryptor,
TelemetryHandler: telemetryHandler,
GasPriceWorker: gasPriceWorker,
})
server := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Port),
Expand Down
8 changes: 4 additions & 4 deletions grafana/dashboards/battery-health.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@
}
]
},
"unit": "km"
"unit": "none"
}
},
"gridPos": {
Expand Down Expand Up @@ -363,7 +363,7 @@
}
]
},
"unit": "celsius"
"unit": "none"
}
},
"gridPos": {
Expand Down Expand Up @@ -394,7 +394,7 @@
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT ROUND(avg_cell_temp_c::numeric, 1) AS \"°C\" FROM battery_snapshots WHERE vehicle_id = ${vehicle_id} ORDER BY created_at DESC LIMIT 1",
"rawSql": "SELECT ROUND(avg_cell_temp_c::numeric, 1) AS \"Temp\" FROM battery_snapshots WHERE vehicle_id = ${vehicle_id} ORDER BY created_at DESC LIMIT 1",
"refId": "A"
}
],
Expand Down Expand Up @@ -805,4 +805,4 @@
"uid": "teslasync-battery-health",
"version": 1,
"weekStart": ""
}
}
4 changes: 2 additions & 2 deletions grafana/dashboards/charging-sessions.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT\n cs.id AS \"🔗 ID\",\n cs.start_date AS \"📅 Date\",\n CASE\n WHEN cs.fast_charger_type IS NOT NULL AND cs.fast_charger_type != '' THEN '⚡ ' || cs.fast_charger_type\n WHEN cs.charger_power > 20 THEN '🔌 DC Fast'\n ELSE '🏠 AC Level 2'\n END AS \"🏷️ Type\",\n cs.start_battery_level AS \"🔋 Start\",\n cs.end_battery_level AS \"🔋 End\",\n (cs.end_battery_level - cs.start_battery_level) AS \"📈 Gained\",\n ROUND(cs.charge_energy_added::numeric, 1) AS \"⚡ kWh\",\n ROUND(cs.charger_power::numeric, 1) AS \"🔌 kW\",\n cs.charger_voltage AS \"⚡ V\",\n cs.charger_phases AS \"🔌 Ph\",\n COALESCE(cs.conn_charge_cable, '-') AS \"🔌 Cable\",\n ROUND(cs.duration_min::numeric) AS \"⏱️ Min\",\n ROUND(CASE WHEN cs.cost > 0 THEN cs.cost ELSE cs.charge_energy_added * (SELECT COALESCE(base_cost_per_kwh, 0.12) FROM settings WHERE id = 1) END::numeric, 2) AS \"💰 $\",\n ROUND(CASE WHEN '${unit_length}' = 'mi' THEN (cs.end_range_km - cs.start_range_km) * 0.621371 ELSE cs.end_range_km - cs.start_range_km END::numeric, 1) AS \"📏 Range+\",\n COALESCE(a.display_name, a.city, '-') AS \"📍 Location\"\nFROM charging_sessions cs\nLEFT JOIN addresses a ON cs.address_id = a.id\nWHERE cs.vehicle_id = ${vehicle_id} AND $__timeFilter(cs.start_date)\nORDER BY cs.start_date DESC\nLIMIT 200",
"rawSql": "SELECT\n cs.id AS \"🔗 ID\",\n cs.start_date AS \"📅 Date\",\n CASE\n WHEN cs.fast_charger_type IS NOT NULL AND cs.fast_charger_type != '' THEN '⚡ ' || cs.fast_charger_type\n WHEN cs.charger_power > 20 THEN '🔌 DC Fast'\n ELSE '🏠 AC Level 2'\n END AS \"🏷️ Type\",\n cs.start_battery_level AS \"🔋 Start\",\n cs.end_battery_level AS \"🔋 End\",\n (cs.end_battery_level - cs.start_battery_level) AS \"📈 Gained\",\n ROUND(cs.charge_energy_added::numeric, 1) AS \"⚡ kWh\",\n ROUND(cs.charger_power::numeric, 1) AS \"🔌 kW\",\n cs.charger_voltage AS \"⚡ V\",\n cs.charger_phases AS \"🔌 Ph\",\n COALESCE(cs.conn_charge_cable, '-') AS \"🔌 Cable\",\n ROUND(cs.duration_min::numeric) AS \"⏱️ Min\",\n ROUND(CASE WHEN cs.cost > 0 THEN cs.cost ELSE cs.charge_energy_added * (SELECT COALESCE(base_cost_per_kwh, 0.12) FROM settings WHERE id = 1) END::numeric, 2) AS \"💰 $\",\n ROUND(convert_distance(cs.end_range_km - cs.start_range_km)::numeric, 1) AS \"📏 Range+\",\n COALESCE(a.display_name, a.city, '-') AS \"📍 Location\"\nFROM charging_sessions cs\nLEFT JOIN addresses a ON cs.address_id = a.id\nWHERE cs.vehicle_id = ${vehicle_id} AND $__timeFilter(cs.start_date)\nORDER BY cs.start_date DESC\nLIMIT 200",
"refId": "A"
}
],
Expand Down Expand Up @@ -778,4 +778,4 @@
"uid": "teslasync-charging-sessions",
"version": 1,
"weekStart": ""
}
}
2 changes: 1 addition & 1 deletion grafana/dashboards/charging-stats.json
Original file line number Diff line number Diff line change
Expand Up @@ -827,4 +827,4 @@
"uid": "teslasync-charging-stats",
"version": 1,
"weekStart": ""
}
}
2 changes: 1 addition & 1 deletion grafana/dashboards/charging.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,4 @@
"uid": "teslasync-charging",
"version": 1,
"weekStart": ""
}
}
14 changes: 7 additions & 7 deletions grafana/dashboards/climate-hvac.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
}
]
},
"unit": "celsius"
"unit": "none"
},
"overrides": [
{
Expand Down Expand Up @@ -138,7 +138,7 @@
"editorMode": "code",
"format": "time_series",
"rawQuery": true,
"rawSql": "SELECT\n created_at AS time,\n inside_temp AS \"Cabin Temp (°C)\",\n outside_temp AS \"Outside Temp (°C)\"\nFROM climate_snapshots\nWHERE vehicle_id = ${vehicle_id}\n AND $__timeFilter(created_at)\nORDER BY created_at",
"rawSql": "SELECT\n created_at AS time,\n convert_temp(inside_temp) AS \"Cabin Temp\",\n convert_temp(outside_temp) AS \"Outside Temp\"\nFROM climate_snapshots\nWHERE vehicle_id = ${vehicle_id}\n AND $__timeFilter(created_at)\nORDER BY created_at",
"refId": "A"
}
],
Expand Down Expand Up @@ -347,7 +347,7 @@
}
]
},
"unit": "celsius"
"unit": "none"
},
"overrides": []
},
Expand Down Expand Up @@ -381,7 +381,7 @@
"editorMode": "code",
"format": "time_series",
"rawQuery": true,
"rawSql": "SELECT\n created_at AS time,\n inside_temp AS \"Actual Cabin\",\n hvac_left_temp_request AS \"Left Target\",\n hvac_right_temp_request AS \"Right Target\"\nFROM climate_snapshots\nWHERE vehicle_id = ${vehicle_id}\n AND $__timeFilter(created_at)\nORDER BY created_at",
"rawSql": "SELECT\n created_at AS time,\n convert_temp(inside_temp) AS \"Actual Cabin\",\n convert_temp(hvac_left_temp_request) AS \"Left Target\",\n convert_temp(hvac_right_temp_request) AS \"Right Target\"\nFROM climate_snapshots\nWHERE vehicle_id = ${vehicle_id}\n AND $__timeFilter(created_at)\nORDER BY created_at",
"refId": "A"
}
],
Expand Down Expand Up @@ -476,7 +476,7 @@
}
]
},
"unit": "celsius",
"unit": "none",
"min": -10,
"max": 50
},
Expand Down Expand Up @@ -510,7 +510,7 @@
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT\n AVG(inside_temp) AS \"Avg Cabin Temp\"\nFROM climate_snapshots\nWHERE vehicle_id = ${vehicle_id}\n AND $__timeFilter(created_at)",
"rawSql": "SELECT\n convert_temp(AVG(inside_temp)) AS \"Avg Cabin Temp\"\nFROM climate_snapshots\nWHERE vehicle_id = ${vehicle_id}\n AND $__timeFilter(created_at)",
"refId": "A"
}
],
Expand Down Expand Up @@ -732,4 +732,4 @@
"uid": "teslasync-climate-hvac",
"version": 1,
"weekStart": ""
}
}
2 changes: 1 addition & 1 deletion grafana/dashboards/comfort-media.json
Original file line number Diff line number Diff line change
Expand Up @@ -694,4 +694,4 @@
"uid": "teslasync-comfort-media",
"version": 1,
"weekStart": ""
}
}
2 changes: 1 addition & 1 deletion grafana/dashboards/cost-analysis.json
Original file line number Diff line number Diff line change
Expand Up @@ -630,4 +630,4 @@
"uid": "teslasync-cost-analysis",
"version": 1,
"weekStart": ""
}
}
16 changes: 8 additions & 8 deletions grafana/dashboards/drive-detail.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT\n ROUND(distance::numeric, 1) AS \"🛣️ Distance (km)\",\n ROUND(duration_min::numeric) AS \"⏱️ Duration (min)\",\n ROUND(speed_max::numeric) AS \"🏎️ Max Speed (km/h)\",\n start_battery_level AS \"🔋 Start %\",\n end_battery_level AS \"🔋 End %\",\n ROUND(outside_temp_avg::numeric, 1) AS \"🌡️ Temp (°C)\"\nFROM drives WHERE id = ${drive_id}",
"rawSql": "SELECT\n ROUND(convert_distance(distance)::numeric, 1) AS \"🛣️ Distance\",\n ROUND(duration_min::numeric) AS \"⏱️ Duration (min)\",\n ROUND(convert_speed(speed_max)::numeric) AS \"🏎️ Max Speed\",\n start_battery_level AS \"🔋 Start %\",\n end_battery_level AS \"🔋 End %\",\n ROUND(convert_temp(outside_temp_avg)::numeric, 1) AS \"🌡️ Temp\"\nFROM drives WHERE id = ${drive_id}",
"refId": "A"
}
],
Expand Down Expand Up @@ -127,7 +127,7 @@
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT p.latitude, p.longitude, p.speed, p.battery_level, p.created_at AS time FROM positions p JOIN drives d ON p.vehicle_id = d.vehicle_id AND p.created_at >= d.start_date AND p.created_at <= COALESCE(d.end_date, NOW()) WHERE d.id = ${drive_id} ORDER BY p.created_at",
"rawSql": "SELECT p.latitude, p.longitude, convert_speed(p.speed) AS speed, p.battery_level, p.created_at AS time FROM positions p JOIN drives d ON p.vehicle_id = d.vehicle_id AND p.created_at >= d.start_date AND p.created_at <= COALESCE(d.end_date, NOW()) WHERE d.id = ${drive_id} ORDER BY p.created_at",
"refId": "A"
}
],
Expand Down Expand Up @@ -202,7 +202,7 @@
"color": {
"mode": "palette-classic"
},
"unit": "velocitykmh"
"unit": "none"
},
"overrides": []
},
Expand Down Expand Up @@ -233,7 +233,7 @@
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT p.created_at AS time, p.speed AS \"Speed (km/h)\"\nFROM positions p\nJOIN drives d ON p.vehicle_id = d.vehicle_id AND p.created_at >= d.start_date AND p.created_at <= COALESCE(d.end_date, NOW())\nWHERE d.id = ${drive_id}\nORDER BY p.created_at",
"rawSql": "SELECT p.created_at AS time, convert_speed(p.speed) AS \"Speed\"\nFROM positions p\nJOIN drives d ON p.vehicle_id = d.vehicle_id AND p.created_at >= d.start_date AND p.created_at <= COALESCE(d.end_date, NOW())\nWHERE d.id = ${drive_id}\nORDER BY p.created_at",
"refId": "A"
}
],
Expand Down Expand Up @@ -300,7 +300,7 @@
"color": {
"mode": "palette-classic"
},
"unit": "celsius"
"unit": "none"
},
"overrides": []
},
Expand Down Expand Up @@ -331,7 +331,7 @@
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT p.created_at AS time, p.inside_temp AS \"Cabin (°C)\", p.outside_temp AS \"Outside (°C)\"\nFROM positions p\nJOIN drives d ON p.vehicle_id = d.vehicle_id AND p.created_at >= d.start_date AND p.created_at <= COALESCE(d.end_date, NOW())\nWHERE d.id = ${drive_id}\nORDER BY p.created_at",
"rawSql": "SELECT p.created_at AS time, convert_temp(p.inside_temp) AS \"Cabin\", convert_temp(p.outside_temp) AS \"Outside\"\nFROM positions p\nJOIN drives d ON p.vehicle_id = d.vehicle_id AND p.created_at >= d.start_date AND p.created_at <= COALESCE(d.end_date, NOW())\nWHERE d.id = ${drive_id}\nORDER BY p.created_at",
"refId": "A"
}
],
Expand Down Expand Up @@ -428,7 +428,7 @@
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT m.created_at AS time, m.di_torque AS \"Torque (Nm)\", m.vehicle_speed AS \"Speed (km/h)\"\nFROM motor_snapshots m\nJOIN drives d ON m.vehicle_id = d.vehicle_id AND m.created_at >= d.start_date AND m.created_at <= COALESCE(d.end_date, NOW())\nWHERE d.id = ${drive_id}\nORDER BY m.created_at",
"rawSql": "SELECT m.created_at AS time, m.di_torque AS \"Torque (Nm)\", convert_speed(m.vehicle_speed) AS \"Speed\"\nFROM motor_snapshots m\nJOIN drives d ON m.vehicle_id = d.vehicle_id AND m.created_at >= d.start_date AND m.created_at <= COALESCE(d.end_date, NOW())\nWHERE d.id = ${drive_id}\nORDER BY m.created_at",
"refId": "A"
}
],
Expand Down Expand Up @@ -573,4 +573,4 @@
"uid": "teslasync-drive-detail",
"version": 1,
"weekStart": ""
}
}
6 changes: 3 additions & 3 deletions grafana/dashboards/drives.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
{
"matcher": {
"id": "byName",
"options": "\ud83d\udcc5 Date"
"options": "📅 Date"
},
"properties": [
{
Expand Down Expand Up @@ -153,7 +153,7 @@
"editorMode": "code",
"format": "table",
"rawQuery": true,
"rawSql": "SELECT\n d.id AS \"🔗 ID\",\n d.start_date AS \"📅 Date\",\n CASE\n WHEN d.distance > 100 THEN '🛣️ Road Trip'\n WHEN d.distance > 30 THEN '🚗 Drive'\n WHEN d.distance > 0 THEN '🏙️ Short'\n ELSE '🅿️ Parked'\n END AS \"🏷️ Type\",\n ROUND(d.distance::numeric, 1) AS \"📏 Distance\",\n ROUND(d.duration_min::numeric) AS \"⏱️ Duration\",\n ROUND((d.distance / NULLIF(d.duration_min, 0) * 60)::numeric, 1) AS \"🚗 Avg Speed\",\n ROUND(d.speed_max::numeric) AS \"🏎️ Max Speed\",\n CASE\n WHEN d.speed_max > 130 THEN '🔴'\n WHEN d.speed_max > 100 THEN '🟡'\n ELSE '🟢'\n END AS \"🚦 Speed\",\n d.start_battery_level AS \"🔋 Start %\",\n d.end_battery_level AS \"🔋 End %\",\n CASE\n WHEN (d.start_battery_level - COALESCE(d.end_battery_level, d.start_battery_level)) > 20 THEN '⚠️ High'\n WHEN (d.start_battery_level - COALESCE(d.end_battery_level, d.start_battery_level)) > 10 THEN '📊 Medium'\n ELSE '✅ Low'\n END AS \"📊 Usage\",\n ROUND(d.start_range_km::numeric) AS \"📏 Range Start\",\n ROUND(d.end_range_km::numeric) AS \"📏 Range End\",\n ROUND(d.outside_temp_avg::numeric, 1) AS \"🌡️ Temp °C\"\nFROM drives d\nWHERE d.vehicle_id = ${vehicle_id}\n AND $__timeFilter(d.start_date)\nORDER BY d.start_date DESC\nLIMIT 200",
"rawSql": "SELECT\n d.id AS \"🔗 ID\",\n d.start_date AS \"📅 Date\",\n CASE\n WHEN d.distance > 100 THEN '🛣️ Road Trip'\n WHEN d.distance > 30 THEN '🚗 Drive'\n WHEN d.distance > 0 THEN '🏙️ Short'\n ELSE '🅿️ Parked'\n END AS \"🏷️ Type\",\n ROUND(convert_distance(d.distance)::numeric, 1) AS \"📏 Distance\",\n ROUND(d.duration_min::numeric) AS \"⏱️ Duration\",\n ROUND(convert_speed(d.distance / NULLIF(d.duration_min, 0) * 60)::numeric, 1) AS \"🚗 Avg Speed\",\n ROUND(convert_speed(d.speed_max)::numeric) AS \"🏎️ Max Speed\",\n CASE\n WHEN d.speed_max > 130 THEN '🔴'\n WHEN d.speed_max > 100 THEN '🟡'\n ELSE '🟢'\n END AS \"🚦 Speed\",\n d.start_battery_level AS \"🔋 Start %\",\n d.end_battery_level AS \"🔋 End %\",\n CASE\n WHEN (d.start_battery_level - COALESCE(d.end_battery_level, d.start_battery_level)) > 20 THEN '⚠️ High'\n WHEN (d.start_battery_level - COALESCE(d.end_battery_level, d.start_battery_level)) > 10 THEN '📊 Medium'\n ELSE '✅ Low'\n END AS \"📊 Usage\",\n ROUND(convert_distance(d.start_range_km)::numeric) AS \"📏 Range Start\",\n ROUND(convert_distance(d.end_range_km)::numeric) AS \"📏 Range End\",\n ROUND(convert_temp(d.outside_temp_avg)::numeric, 1) AS \"🌡️ Temp\"\nFROM drives d\nWHERE d.vehicle_id = ${vehicle_id}\n AND $__timeFilter(d.start_date)\nORDER BY d.start_date DESC\nLIMIT 200",
"refId": "A"
}
],
Expand Down Expand Up @@ -255,4 +255,4 @@
"uid": "teslasync-drives",
"version": 1,
"weekStart": ""
}
}
Loading
Loading