Plan Big Pickle----Own plan
Artificial Intelligence

Plan Big Pickle----Own plan

To understand this blog post you need to read the post "Keeeping token cost down and evaluation test" and the other posts

Jan Petersen
July 4, 2026
8 min read
2 views

Tesla Cheap-Hour Charger — Implementation Plan

Overview

A single-container Docker app that controls Tesla charging during the cheapest hourly electricity prices (DK1, Denmark). Minimal API usage to stay within the $10/month Tesla Fleet API credit.

Architecture

┌─────────────────────────────────────────┐
│         Docker Container                │
│  ┌───────────┐    ┌────────────────┐    │
│  │  Express   │    │  VCP (Go)      │    │
│  │  ─ auth    │    │  signs cmds    │    │
│  │  ─ routes  │    └──────┬─────────┘    │
│  │  ─ cron    │           │              │
│  │  ─ UI      │           │              │
│  └─────┬─────┘           │              │
│        │                 │              │
│   ┌────▼─────────────────▼────┐         │
│   │    /app/data/             │         │
│   │  ├── config.json          │         │
│   │  ├── ec_private_key.pem   │         │
│   │  └── price_cache.json     │         │
│   └───────────────────────────┘         │
└─────────────────────────────────────────┘

Stack

  • Runtime: Node.js + TypeScript + Express 5
  • UI: htmx + Tailwind CSS (served directly from Express, no nginx)
  • Charts: Chart.js via CDN (24h price bar chart)
  • Config: JSON file on mounted volume
  • VCP: teslamotors/vehicle-command built from Go, bundled in-container
  • Container: Multi-stage Dockerfile (Go build stage → Node runtime stage)

Daily Cycle

TimeActionTesla API callsCost
14:00 CETFetch Energinet DK1 prices (next 24-36h)0$0
monitor_start (e.g. 18:00)vehicle_data?endpoints=charge_state1 Data ($0.002)$0.002
If plugged in → compute cheapest window
If not plugged in → re-check every 15 min~4-8 Data~$0.008-0.016
Window startwake_up (if asleep) + charge_start1 Wake ($0.02) + 1 Cmd ($0.001)$0.021
Window endcharge_stop1 Cmd ($0.001)$0.001
Total (best case)$0.024/night
Total (worst case)~$0.04/night
Monthly (30 days)$0.72-$1.20

Schedule Algorithm

  1. Fetch hourly spot prices from Energi Data Service (DK1)
  2. Add Danish tariffs (Konstant model):
    • 00:00-06:00: 0.20 DKK
    • 06:00-17:00: 0.45 DKK
    • 17:00-21:00: 1.10 DKK
    • 21:00-24:00: 0.45 DKK
  3. Get current_soc from vehicle_data
  4. kwh_needed = (target_soc - current_soc) / 100 * battery_kwh
  5. hours_needed = ceil(kwh_needed / charger_kw)
  6. Filter hours: [now, departure_time] excluding past hours
  7. Find cheapest contiguous window of hours_needed length
  8. If no suitable window exists → skip night (log reason)

Auth & Setup

Reuses the same OAuth PKCE flow as the existing Tesla Charger app:

  1. In-app setup wizard (served at /setup):

    • Enter Tesla Developer client_id + client_secret
    • Click "Connect to Tesla" → redirected to Tesla authorize URL
    • User logs in on Tesla, grants permissions
    • Tesla redirects to /api/auth/callback with auth code
    • App exchanges code for tokens (access + refresh), stores encrypted in config.json
    • App calls partner_accounts to register as Fleet API partner
    • App generates EC P-256 key pair, serves public key
    • VCP proxy starts with private key for command signing
  2. Token refresh: Auto-refresh 5 min before expiry. If refresh fails, re-auth required.

Config Fields (config.json)

FieldTypeDefaultEditable via UI
target_socnumber80Yes (slider 50-100)
departure_timestring"07:00"Yes (time picker)
monitor_startstring"18:00"Yes (time picker)
charger_kwnumber11Yes (Settings page)
battery_kwhnumber75Yes (Settings page)
active_daysbitmask62 (Mon-Fri)Yes (checkboxes)
tesla_vinstringAuto-detected
ntfy_topicstring""Yes (Settings page)
tesla_client_idstringSetup wizard
tesla_client_secretstringSetup wizard (encrypted)
access_tokenstringOAuth flow (encrypted)
refresh_tokenstringOAuth flow (encrypted)
ec_private_keystringGenerated (encrypted)
ec_public_keystringGenerated

UI Pages

Dashboard (/)

  • Large battery SOC percentage display
  • Charging status badge (Idle / Charging / Disconnected / Done / Planned)
  • Next planned charging window: "Charging 03:00-05:00"
  • Target SOC slider
  • Departure time picker
  • Monitor start time picker

Prices (/prices)

  • 24h bar chart via Chart.js
  • Cheapest hours highlighted in green
  • Planned charging window highlighted in blue
  • Current price indicator

Settings (/settings)

  • Charger power input (kW)
  • Battery capacity input (kWh)
  • Active days checkboxes (Mon-Sun)
  • ntfy.sh topic input (optional)

Setup (/setup)

  • 3-step OAuth wizard (hidden once setup is complete)
  • Step 1: Enter Developer credentials
  • Step 2: Redirect to Tesla for authorization
  • Step 3: Confirm success, show VIN

API Endpoints

MethodPathAuthPurpose
GET/api/configYesRead config
POST/api/configYesUpdate config
GET/api/statusYesVehicle SOC, charging state, schedule
GET/api/pricesYesToday + tomorrow prices
POST/api/prices/refreshYesForce price refresh
GET/api/scheduleYesPlanned charging window
GET/api/auth/statusNoTesla auth status
GET/api/auth/startNoInitiate OAuth flow
GET/api/auth/callbackNoOAuth callback
POST/api/auth/registerYesRegister partner account
GET/healthNoContainer health
GET/.well-known/appspecific/com.tesla.3p.public-key.pemNoTesla public key

Error Handling

ScenarioBehavior
Car already at/above target SOCSkip night. Log: "Already charged."
Not plugged in at checkRe-check every 15 min. If still unplugged 2h before departure, abort.
Price API (Energinet) downUse last cached prices. If no cache, skip night.
Tesla 429 rate limitExponential backoff: 1min -> 5min -> 15min -> abort night.
charge_start failsRetry once after 30s. If still fails, log + ntfy + abort.
charge_stop failsRetry once after 30s. Log + ntfy. Car stops on its own at target SOC.
Container restart mid-chargeOn boot: check vehicle_data. If charging and past window, stop.
Token refresh failsClear tokens. Re-auth required flag in UI.
VCP proxy failsRestart VCP. If fails twice, abort.

Project Structure

tesla-cheap-charger/
├── Dockerfile                  # Multi-stage build (Go VCP + Node)
├── docker-compose.yml          # Single service
├── .env.example                # Env vars template
├── .gitignore
├── src/
│   ├── index.ts                # Express entry point
│   ├── config.ts               # JSON config reader/writer
│   ├── scheduler.ts            # Cron/setInterval schedule manager
│   ├── tesla/
│   │   ├── client.ts           # Fleet API + VCP proxy caller
│   │   ├── auth.ts             # OAuth PKCE flow
│   │   └── keys.ts             # EC P-256 key management
│   ├── prices/
│   │   ├── energinet.ts        # Energi Data Service client (DK1)
│   │   └── tariffs.ts          # Konstant tariff model
│   ├── charging/
│   │   ├── optimizer.ts        # Cheapest contiguous window algorithm
│   │   └── executor.ts         # Start/stop charge loop
│   ├── routes/
│   │   ├── api.ts              # REST API routes
│   │   ├── auth.ts             # OAuth callback routes
│   │   └── pages.ts            # HTML page routes (htmx)
│   ├── views/
│   │   ├── layout.html         # Base layout (Tailwind)
│   │   ├── dashboard.html      # Main dashboard
│   │   ├── prices.html         # Price chart page
│   │   ├── settings.html       # Settings page
│   │   └── setup.html          # OAuth setup wizard
│   ├── notifications/
│   │   └── ntfy.ts             # Push notification sender
│   └── utils/
│       ├── time.ts             # Denmark timezone helpers
│       └── crypto.ts           # AES encryption for tokens
├── package.json
├── tsconfig.json
└── data/                       # Mounted volume (gitignored)
    ├── config.json
    ├── ec_private_key.pem
    └── price_cache.json

Docker Compose

yaml
services:
  app:
    build: .
    ports:
      - "3001:3001"
    volumes:
      - ./data:/app/data
    environment:
      - PORT=3001
      - TZ=Europe/Copenhagen
      - NODE_ENV=production
    restart: unless-stopped

Multi-stage Dockerfile:

  1. Stage 1 (golang): Clone teslamotors/vehicle-command, build tesla-http-proxy
  2. Stage 2 (node:22-alpine): Copy Go binary, install npm deps, copy source
  3. Runtime: VCP runs as child process on port 4040, Express on port 3001

Budget

ItemMonthlyNotes
Tesla Fleet API~$0.72-1.20Covered by $10 monthly credit
VPS hosting$0Docker on existing home server
Energinet API$0Free, open data
ntfy.sh$0Free, open source
Total out-of-pocket$0

Dependencies

  • express — HTTP framework
  • axios — HTTP client (Tesla, Energinet, ntfy)
  • node-cron — In-process scheduling
  • crypto-js — AES token encryption
  • uuid — OAuth state nonce
  • dotenv — Env file loading
  • Dev: typescript, tsx, @types/*
  • No native modules needed (pure JS/TS)

Share this post

Related Posts

Comments

Be the first to leave a comment.

Leave a comment

Comments are reviewed before they appear.