Une application pour adapter la charge de la tesla selon le surplus de production des panneaux solaires renvoyé sur le réseau. Permet d'optimiser la charge sans importer de courant supplémentaire. Application installée sur une très vieille raspberry pi (Model B + 512 Mb) qui trainait dans un coin. Ne pouvant pas utiliser l'application evcc qui nécessite une carte plus récente, j'ai tout redéveloppé avec codex en une dizaine d'heures.
Dépôt unique pour deux composants complémentaires :
raspberry/: service Python léger qui tourne en continu sur une Raspberry Pi et ajuste la charge Tesla selon le surplus solaire ;tesla.opcoach.com/: mini site PHP pour Tesla Fleet API, utilisé pour l’autorisation OAuth, le callback et le key pairing.tesla-refresh-token.json: fichier local généré après l’autorisation OAuth, à copier sur la Raspberry hors du webroot.
tesla-charge/
├── tesla-refresh-token.json
├── deploy/
│ └── systemd/
│ ├── README.md
│ ├── tesla-charge.env.example
│ ├── tesla-charge.service
│ └── tesla-command-proxy.service.example
├── raspberry/
│ ├── app.py
│ ├── api_server.py
│ ├── config.py
│ ├── control_loop.py
│ ├── proxy/
│ │ └── README.md
│ ├── requirements.txt
│ ├── solar_monitor.py
│ └── tesla_controller.py
└── tesla.opcoach.com/
├── .well-known/
│ └── appspecific/
│ └── com.tesla.3p.public-key.pem
├── auth/
├── lib/
├── logout/
├── index.php
└── tesla-oauth-config.php.example
Le composant raspberry/ est une application Python 3.11 prévue pour Raspberry Pi OS Lite, avec un minimum de dépendances.
requestsflask
Le service :
- lit les données de production et de réseau via l’Envoy Enphase ;
- calcule le surplus exporté ;
- calcule une consigne Tesla incrémentale à partir du courant déjà demandé par la voiture et du flux réseau signé, avec la formule
current_amps + floor((export_watts - import_watts) / tension_nominale), puis borne le résultat entreTESLA_MIN_AMPSetTESLA_MAX_AMPS; - lit l’état du véhicule via Tesla Fleet API à partir d’un
refresh_token; - borne la consigne entre
6 Aet32 A; - n’envoie pas de commande si l’ampérage ne change pas ;
- expose une API REST ;
- affiche une page web locale de résumé avec un historique roulant, trois graphes séparés, des indicateurs d’âge des mesures, un compteur de prochaine mise à jour et une fenêtre de courbe sélectionnable côté navigateur.
La boucle solaire tourne toutes les 5 secondes par défaut.
La lecture Tesla est mise en cache 30 secondes par défaut pour éviter de solliciter inutilement le véhicule.
Si le proxy local de commandes n’est pas disponible, un nouvel essai n’est tenté qu’après 60 secondes par défaut.
La boucle passe en veille hors plage de jour, avec un réveil plus rare toutes les 900 secondes par défaut.
ENPHASE_TOKEN: obligatoireTESLA_CLIENT_ID: recommandé, sinon lu depuistesla-refresh-token.jsons’il contient déjàclient_idENVOY_URL: défauthttps://192.168.68.57/ivp/meters/readingsTESLA_REFRESH_TOKEN_FILE: défaut../tesla-refresh-token.jsonTESLA_API_BASE_URL: défauthttps://fleet-api.prd.eu.vn.cloud.tesla.comTESLA_AUTH_URL: défauthttps://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/tokenTESLA_PROXY_URL: défauthttps://localhost:4443TESLA_PROXY_VERIFY_SSL: défautfalseTESLA_PROXY_CA_FILE: optionnel, chemin vers un certificat local si tu veux vérifier TLSTESLA_VEHICLE_NAME: optionnelTESLA_VEHICLE_INDEX: défaut0APP_TIMEZONE: défautEurope/ParisCONTROL_INTERVAL_SEC: défaut5CONTROL_IDLE_INTERVAL_SEC: défaut900TESLA_STATUS_INTERVAL_SEC: défaut900TESLA_IDLE_STATUS_INTERVAL_SEC: défaut3600TESLA_DETAIL_INTERVAL_SEC: défaut3600TESLA_PROXY_RETRY_SEC: défaut60TESLA_NOMINAL_VOLTAGE: défaut220TESLA_CHARGE_START_AMPS: défaut6TESLA_CHARGE_STOP_AMPS: défaut5TESLA_CHARGE_START_CONFIRM_SEC: défaut60TESLA_CHARGE_STOP_CONFIRM_SEC: défaut90TIMELINE_WINDOW_SEC: défaut3600DAY_ACTIVE_START: défaut07:00DAY_ACTIVE_END: défaut22:00TESLA_MIN_AMPS: défaut6TESLA_MAX_AMPS: défaut32APP_HOST: défaut0.0.0.0APP_PORT: défaut8080REQUEST_TIMEOUT_SEC: défaut10LOG_LEVEL: défautINFOENVOY_VERIFY_SSL: défautfalse
cd "$HOME/git"
git clone <URL_DU_DEPOT> tesla-charge
cd "$HOME/git/tesla-charge/raspberry"
sudo apt update
sudo apt install -y python3-venv
python3 -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txtCopier ensuite le fichier tesla-refresh-token.json généré par le callback web vers la racine du dépôt sur la Raspberry :
scp /chemin/local/vers/tesla-refresh-token.json pi@raspberrypi:git/tesla-charge/tesla-refresh-token.jsonSur une installation Raspberry Pi OS Lite récente, les composants suivants ont été nécessaires :
python3-venvpour créer l’environnement virtuel Pythonopensslpour générer le certificat TLS local du proxygouniquement si tu compilestesla-http-proxydirectement sur la Raspberry
Exemple :
sudo apt update
sudo apt install -y python3-venv openssl golangcd "$HOME/git/tesla-charge/raspberry"
. .venv/bin/activate
export ENPHASE_TOKEN='...'
export TESLA_CLIENT_ID='...'
python app.pyL’interface locale sera disponible sur :
http://<ip-de-la-raspberry>:8080/
Tant que rien n’écoute sur TESLA_PROXY_URL (par défaut https://localhost:4443), l’application reste utile pour la supervision solaire et Tesla, mais passe de fait en lecture seule pour les commandes de charge.
Par défaut :
- mode actif en journée ;
- mode veille hors plage de jour.
Le comportement par défaut correspond à :
APP_TIMEZONE=Europe/Paris
CONTROL_INTERVAL_SEC=5
CONTROL_IDLE_INTERVAL_SEC=900
DAY_ACTIVE_START=07:00
DAY_ACTIVE_END=22:00
Conséquence :
- entre
07:00et22:00, la régulation fonctionne normalement ; - entre
22:00et07:00, la boucle ralentit fortement ; - aucune logique spécifique heures creuses n’est appliquée.
- la charge ne démarre que si le surplus reste suffisant pendant
TESLA_CHARGE_START_CONFIRM_SEC; - la charge s’arrête si le surplus passe sous le seuil bas pendant
TESLA_CHARGE_STOP_CONFIRM_SEC.
Vérifier l’application web :
curl http://127.0.0.1:8080/statusVérifier l’historique de régulation :
curl http://127.0.0.1:8080/timelineVérifier le proxy local Tesla :
ss -ltnp | grep 4443Le calcul de la consigne Tesla est volontairement incrémental pour pousser un maximum de surplus vers la voiture sans oublier le courant déjà demandé par la Tesla :
ampères = courant_Tesla_actuel + floor((export_watts - import_watts) / tension_nominale)
Le résultat est ensuite limité entre TESLA_MIN_AMPS et TESLA_MAX_AMPS.
Exemple :
- courant Tesla actuel :
8 A - surplus exporté :
2068 W - tension nominale utilisée :
220 V - calcul brut :
2068 / 220 = 9,40 floor(9,40)donne9 A- consigne finale :
17 A
Avec 25 W d’import réseau et 8 A déjà demandés par la voiture, la consigne passe à 7 A. C’est volontaire : dès qu’on importe un peu, on baisse d’un cran pour revenir vers un léger export. Si tu veux encore plus de finesse, la prochaine étape serait d’ajouter une marge de sécurité configurable.
Réglages liés :
TESLA_CHARGE_START_AMPS=6
TESLA_CHARGE_STOP_AMPS=5
TESLA_CHARGE_START_CONFIRM_SEC=60
TESLA_CHARGE_STOP_CONFIRM_SEC=90
TESLA_NOMINAL_VOLTAGE=220
TESLA_STATUS_INTERVAL_SEC=900
TESLA_IDLE_STATUS_INTERVAL_SEC=3600
TESLA_DETAIL_INTERVAL_SEC=3600
Vérifier le service principal :
sudo systemctl status tesla-charge --no-pagerVérifier le proxy :
sudo systemctl status tesla-command-proxy --no-pagerGET /solarGET /teslaGET /statusGET /timelinePOST /tesla/amps
Le tableau de bord affiche désormais :
- le temps restant avant la prochaine régulation ;
- le temps restant avant la prochaine lecture Tesla ;
- deux indicateurs d'usage Tesla, séparant les requêtes de données et les commandes avec coût estimé ;
- ces compteurs sont persistés localement dans
tesla-usage.jsonpour conserver le mois en cours après un redémarrage ; - en mode automatique, la Tesla est interrogée de façon beaucoup plus parcimonieuse hors charge, avec des lectures détaillées réservées aux rafraîchissements manuels ;
- un graphe puissance ;
- un graphe réseau séparé ;
- un graphe Tesla avec zoom ;
- des tooltips explicatifs sur les cartes de graphe.
Exemple :
curl -X POST http://raspberrypi:8080/tesla/amps \
-H 'Content-Type: application/json' \
-d '{"amps": 10}'Le dépôt contient maintenant les fichiers prêts à copier dans deploy/systemd/ :
deploy/systemd/tesla-charge.servicedeploy/systemd/tesla-charge.env.exampledeploy/systemd/tesla-command-proxy.service.exampledeploy/systemd/README.md
Les unités systemd sont à installer dans :
/etc/systemd/system/
Les fichiers versionnés supposent un utilisateur Raspberry nommé olivier. Si besoin, adapte User= et les chemins /home/olivier/....
L’application Python ne doit pas démarrer elle-même le proxy local. Le proxy de commandes Tesla doit rester un service séparé, plus simple à superviser et redémarrer.
Le proxy local Tesla officiel est tesla-http-proxy. Il écoute en HTTPS, pas en HTTP.
Pour simplifier les mises à jour sur la Raspberry, tu peux ajouter ces fonctions dans ~/.bashrc ou ~/.bash_profile :
tesla-status() {
sudo systemctl status tesla-command-proxy --no-pager
sudo systemctl status tesla-charge --no-pager
}
tesla-restart() {
sudo systemctl daemon-reload
sudo systemctl restart tesla-command-proxy
sudo systemctl restart tesla-charge
tesla-status
}
tesla-restart-proxy() {
sudo systemctl daemon-reload
sudo systemctl restart tesla-command-proxy
sudo systemctl status tesla-command-proxy --no-pager
}
tesla-restart-charge() {
sudo systemctl restart tesla-charge
sudo systemctl status tesla-charge --no-pager
}Pour automatiser le rappel après un git pull, le dépôt fournit un hook post-merge dans deploy/git-hooks/post-merge. Active-le localement avec :
chmod +x deploy/git-hooks/post-merge
git config core.hooksPath deploy/git-hooksLe proxy local Tesla a besoin de trois fichiers :
- une clé privée Fleet API, correspondant à la clé publique publiée sur le sous-domaine web ;
- un certificat TLS local ;
- une clé privée TLS locale.
Exemple de préparation :
mkdir -p "$HOME/git/tesla-charge/raspberry/proxy"
cp /chemin/vers/la_vraie_cle_privee_fleet.pem \
"$HOME/git/tesla-charge/raspberry/proxy/fleet-key.pem"
chmod 600 "$HOME/git/tesla-charge/raspberry/proxy/fleet-key.pem"
openssl req -x509 -nodes -newkey rsa:2048 \
-keyout "$HOME/git/tesla-charge/raspberry/proxy/tls-key.pem" \
-out "$HOME/git/tesla-charge/raspberry/proxy/tls-cert.pem" \
-days 3650 \
-subj "/CN=localhost"
chmod 600 "$HOME/git/tesla-charge/raspberry/proxy/tls-key.pem"Important :
fleet-key.pemdoit contenir une clé privée, pas une clé publique ;- si
openssl pkey -in .../fleet-key.pem -nooutéchoue, le fichier n’est pas le bon ; - si tu perds la vraie clé privée Fleet, il faut régénérer une paire, republier la clé publique et refaire le pairing Tesla.
Le composant tesla.opcoach.com/ est un petit site PHP sans framework pour :
- démarrer le flux OAuth Tesla ;
- recevoir le callback OAuth ;
- échanger le
codecontre un token ; - sauvegarder le
refresh_tokencôté serveur ; - exposer la clé publique à l’URL attendue par Tesla.
Déployer uniquement le contenu du répertoire tesla.opcoach.com/ sur le sous-domaine web.
Important :
- ne pas publier le répertoire
raspberry/sur le site public ; - créer le vrai
tesla-oauth-config.phpà la racine du dépôt sur le serveur, hors du dossier web ; - publier la clé publique ici :
https://tesla.opcoach.com/.well-known/appspecific/com.tesla.3p.public-key.pem
Dans l’interface Tesla :
URL d'origine autorisée:https://tesla.opcoach.comURI de redirection autorisée:https://tesla.opcoach.com/auth/callback/URL de renvoi autorisée:https://tesla.opcoach.com/logout/callback/
De manière générique, si le domaine change plus tard :
- origine :
https://<app-domain> - callback OAuth :
https://<app-domain>/auth/callback/ - logout callback :
https://<app-domain>/logout/callback/ - clé publique :
https://<app-domain>/.well-known/appspecific/com.tesla.3p.public-key.pem
Ne versionne pas tesla-oauth-config.php. Crée /home/compte/tesla-charge/tesla-oauth-config.php sur le serveur à partir de tesla.opcoach.com/tesla-oauth-config.php.example, puis remplace :
REMPLACER_PAR_LE_CLIENT_ID_TESLAREMPLACER_PAR_LE_CLIENT_SECRET_TESLA
Pour un compte Tesla utilisé en France, l’audience Fleet API à garder dans l’exemple est :
https://fleet-api.prd.eu.vn.cloud.tesla.com
Le refresh_token est sauvegardé par défaut hors de la racine web, dans le parent du dossier tesla.opcoach.com.
Exemple d’emplacement :
/home/compte/tesla-charge/
├── tesla-oauth-config.php
├── tesla-refresh-token.json
└── tesla.opcoach.com/
└── tesla-oauth-config.php.example
https://tesla.opcoach.com/auth/start/
Après consentement, le callback sauvegarde le refresh_token dans le fichier privé configuré.
Après création de l’application et publication de la clé publique, il faut enregistrer le domaine auprès de Fleet API.
Exemple pour la région Europe :
CLIENT_ID='...'
CLIENT_SECRET='...'
AUDIENCE='https://fleet-api.prd.eu.vn.cloud.tesla.com'
PARTNER_TOKEN=$(
curl -s --request POST \
--url 'https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode "client_id=${CLIENT_ID}" \
--data-urlencode "client_secret=${CLIENT_SECRET}" \
--data-urlencode "audience=${AUDIENCE}" \
--data-urlencode 'scope=openid vehicle_device_data vehicle_cmds vehicle_charging_cmds' \
| jq -r '.access_token'
)
curl --request POST \
--url "${AUDIENCE}/api/1/partner_accounts" \
--header "Authorization: Bearer ${PARTNER_TOKEN}" \
--header 'Content-Type: application/json' \
--data '{"domain":"tesla.opcoach.com"}'Une fois le domaine enregistré, il reste à faire le key pairing pour les commandes signées.
Validation syntaxique Python :
cd raspberry
python3 -m py_compile app.py api_server.py config.py control_loop.py solar_monitor.py tesla_controller.pyLe dépôt ignore notamment :
tesla-refresh-token.jsondeploy/systemd/*.env- les artefacts Python
- les environnements virtuels
- les fichiers
.env - les fichiers privés OAuth et les tokens de
tesla.opcoach.com
Le cache Tesla, les secrets OAuth et les tokens ne doivent pas être versionnés.
Si le dépôt est rendu public, il est recommandé de ne laisser dans Git que des valeurs d’exemple ou génériques.
Les vraies valeurs doivent rester uniquement :
- dans
/etc/default/tesla-chargepour le service principal ; - dans
/etc/systemd/system/tesla-charge.servicesi tu l’as adapté localement ; - dans
/etc/systemd/system/tesla-command-proxy.servicesi tu l’as adapté localement ; - dans
deploy/systemd/tesla-charge.enven local seulement, non versionné ; - dans
tesla-refresh-token.json, non versionné ; - dans
raspberry/proxy/*.pem, non versionnés ; - dans
tesla-oauth-config.php, hors Git et hors webroot.
Exemple de workflow :
cp deploy/systemd/tesla-charge.env.example deploy/systemd/tesla-charge.env
nano deploy/systemd/tesla-charge.env
sudo cp deploy/systemd/tesla-charge.env /etc/default/tesla-chargeDans ce modèle :
- le dépôt peut contenir des exemples génériques ;
git pullmet à jour le code et les exemples ;- les vraies valeurs restent en dehors des fichiers versionnés ;
- un
git pulln’écrase pas/etc/default/tesla-charge.
Si tu modifies un fichier versionné pour y mettre une vraie valeur, il faudra la remettre après un git pull. Il vaut donc mieux éviter ce mode de fonctionnement.