Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ All notable changes are listed here. `Unreleased` is what `main` and the Docker

### Added

- Vehicle status on the dashboard: connection and sleep state, battery and charging, climate, lock state, last contact and last error per vehicle, each with the time its data is from. It updates itself every 10 seconds from what the proxy already knows and never contacts the vehicle on its own; **Refresh** fetches current data without waking it, **Wake & refresh** wakes it first. New endpoints `GET /api/proxy/1/status` and `POST /api/proxy/1/status/refresh`. ([#168](https://github.com/wimaha/TeslaBleHttpProxy/issues/168))
- `trustedOrigins` environment variable for reverse proxies over plain HTTP that rewrite the `Host` header, and for web pages on other origins that call the API. See [environment variables](docs/environment_variables.md#trustedorigins). ([#170](https://github.com/wimaha/TeslaBleHttpProxy/pull/170))
- Docker image `:dev`, built from `main` on every push. ([#171](https://github.com/wimaha/TeslaBleHttpProxy/pull/171))
- Docker image `:pr-<number>` for every pull request from a branch of this repository, to test a change on a car before it is merged. See [Test images for pull requests](README.md#test-images-for-pull-requests).
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The program stores the received requests in a queue and processes them one by on
- [Vehicle Data](#vehicle-data)
- [Body Controller State](#body-controller-state)
- [Version of Proxy](#version-of-proxy)
- [Status of Proxy and Vehicles](#status-of-proxy-and-vehicles)
- [Troubleshooting](#troubleshooting)

## How to install
Expand Down Expand Up @@ -275,6 +276,20 @@ Get version of proxy:

The response will contain the version of the proxy.

### Status of Proxy and Vehicles

*(unreleased)*

The dashboard (`http://localhost:8080/dashboard`) shows the status of the proxy and of each vehicle it has talked to: connection, sleep state, battery and charging, climate, lock state, and the last successful contact and last error. Every block shows when its data was fetched. Opening or auto-updating the dashboard never contacts the vehicle; it only shows what the proxy already knows, for example from evcc polling `vehicle_data`. **Refresh** fetches current data without waking the vehicle; **Wake & refresh** wakes it first (after a confirmation).

The same information as JSON, without contacting the vehicle:
`http://localhost:8080/api/proxy/1/status`

To fetch current `charge_state` and `climate_state` for the dashboard (bypasses the cache, the result is cached like any `vehicle_data` response):
`POST http://localhost:8080/api/proxy/1/status/refresh` with form fields `vin` and `wakeup` (`true` wakes the vehicle if it is asleep, default `false`).

**No login:** like the rest of the API, the dashboard and these endpoints have no authentication. Anyone who can reach the proxy on your network can see the VIN, battery, charging and climate data, and can wake the vehicle or send commands. Only websites opened in a browser are blocked from doing this (see [Dashboard buttons return 403 Forbidden](#dashboard-buttons-return-403-forbidden)). Run the proxy only in a network you trust, and do not expose its port to the internet; if you need remote access, put it behind a VPN or a reverse proxy with authentication.

## Troubleshooting

### Vehicle Requirements
Expand Down
6 changes: 6 additions & 0 deletions html/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ <h1>TeslaBleHttpProxy</h1>
<a href="/logs" style="color: #007bff; text-decoration: none; font-size: 14px;">View Logs</a>
</div>
</div>
<h3>Vehicle Status</h3>
<p class="description-text">Shows what the proxy already knows; updating this page never contacts the vehicle. <strong>Refresh</strong> fetches current data without waking the vehicle.</p>
<div id="vehicle-status" aria-live="polite"></div>
</div>
<div class="container">
<div class="add-setting">
<h3>Key Management</h3>
<p class="description-text">You can generate multiple keys with different roles. The active key is used for all vehicle operations.</p>
Expand Down Expand Up @@ -120,4 +125,5 @@ <h2>Setup Vehicle</h2>
return confirm(message);
}
</script>
<script src="static/js/status.js"></script>
{{end}}
137 changes: 137 additions & 0 deletions internal/api/handlers/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package handlers

import (
"encoding/json"
"net/http"
"regexp"
"slices"
"strings"
"time"

"github.com/wimaha/TeslaBleHttpProxy/config"
"github.com/wimaha/TeslaBleHttpProxy/internal/api/models"
"github.com/wimaha/TeslaBleHttpProxy/internal/ble/control"
"github.com/wimaha/TeslaBleHttpProxy/internal/logging"
)

// The dashboard status is assembled from what the proxy already knows: the command loop's view of
// each connection and the cached responses. Reading it never contacts the vehicle; only the
// refresh endpoint does, through the command queue.

var startTime = time.Now()

// statusEndpoints are the cached vehicle_data endpoints the dashboard shows. Location and drive
// data are left out on purpose, even when cached.
var statusEndpoints = []string{"charge_state", "climate_state"}

type proxyStatus struct {
Version string `json:"version"`
StartedAt time.Time `json:"started_at"`
UptimeSeconds int64 `json:"uptime_seconds"`
ActiveKeyRole string `json:"active_key_role"`
BleControlRunning bool `json:"ble_control_running"`
QueueLength int `json:"queue_length"`
ConnectionTimeout int `json:"connection_timeout"`
ReleaseAdapterWhenIdle bool `json:"release_adapter_when_idle"`
VehicleDataCacheTime int `json:"vehicle_data_cache_time"`
}

type vehicleStatus struct {
control.VehicleConnection
ChargeState *cachedEntry `json:"charge_state"`
ClimateState *cachedEntry `json:"climate_state"`
BodyController *cachedEntry `json:"body_controller"`
}

type statusResponse struct {
Proxy proxyStatus `json:"proxy"`
Vehicles []vehicleStatus `json:"vehicles"`
Now time.Time `json:"now"`
}

func buildStatus() statusResponse {
bc := control.Instance()
proxy := proxyStatus{
Version: config.Version,
StartedAt: startTime,
UptimeSeconds: int64(time.Since(startTime).Seconds()),
ActiveKeyRole: control.GetActiveKeyRole(),
BleControlRunning: bc != nil,
}
if config.AppConfig != nil {
proxy.ConnectionTimeout = config.AppConfig.ConnectionTimeout
proxy.ReleaseAdapterWhenIdle = config.AppConfig.ReleaseAdapterWhenIdle
proxy.VehicleDataCacheTime = config.AppConfig.VehicleDataCacheTime
}

byVin := map[string]*vehicleStatus{}
vehicle := func(vin string) *vehicleStatus {
if v, ok := byVin[vin]; ok {
return v
}
v := &vehicleStatus{VehicleConnection: control.VehicleConnection{Vin: vin}}
byVin[vin] = v
return v
}
if bc != nil {
proxy.QueueLength = bc.QueueLength()
for _, c := range bc.Vehicles() {
vehicle(c.Vin).VehicleConnection = c
}
}
for _, vin := range cachedVins() {
v := vehicle(vin)
v.ChargeState = latestVehicleData(vin, statusEndpoints[0])
v.ClimateState = latestVehicleData(vin, statusEndpoints[1])
v.BodyController = latestBodyControllerState(vin)
}

vehicles := make([]vehicleStatus, 0, len(byVin))
for _, v := range byVin {
vehicles = append(vehicles, *v)
}
slices.SortFunc(vehicles, func(a, b vehicleStatus) int { return strings.Compare(a.Vin, b.Vin) })
return statusResponse{Proxy: proxy, Vehicles: vehicles, Now: time.Now()}
}

// Status answers GET /api/proxy/1/status for the dashboard. It never contacts the vehicle.
func Status(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(buildStatus())
response := models.Ret{Response: models.Response{Result: err == nil, Reason: "The request was successfully processed.", Response: data}}
if err != nil {
logging.Error("Failed to build status", "error", err)
response.Response.Reason = err.Error()
response.Response.Response = nil
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
if err := json.NewEncoder(w).Encode(response); err != nil {
logging.Error("failed to send response", "error", err)
}
}

var vinPattern = regexp.MustCompile(`^[A-HJ-NPR-Z0-9]{17}$`)

// RefreshStatus answers POST /api/proxy/1/status/refresh (form fields vin, wakeup). It fetches
// the dashboard's vehicle data over BLE through the command queue, bypassing the cache, and
// wakes the vehicle only if wakeup=true. The result is cached like any vehicle_data response.
func RefreshStatus(w http.ResponseWriter, r *http.Request) {
vin := strings.ToUpper(strings.TrimSpace(r.FormValue("vin")))
wakeup := r.FormValue("wakeup") == "true"

var response models.Response
response.Vin = vin
response.Command = "vehicle_data"
defer commonDefer(w, &response)
logging.Info("Dashboard refresh", "VIN", vin, "Wakeup", wakeup)

if !vinPattern.MatchString(vin) {
response.Reason = "A VIN has 17 characters (letters and digits, no I, O or Q)."
return
}
bc := bleControl(&response)
if bc == nil {
return
}
fetchVehicleData(r.Context(), bc, vin, statusEndpoints, 0, wakeup, &response)
}
Loading
Loading