workspace: Jeep auto-identifiee, photo de tableau de bord, releves km, transfert Stoik

- concierge-live.js : variante (a), la JEEP identifiee par Uconnect recoit une
  ligne d'info sans question ; attente de 15 min avant de questionner une voiture
  inconnue ; releves km ; reponse sur une ligne ; simulation sans ecriture d'etat.
- concierge-write.js (EVCS) : niveau et releve km, tests.
- lecture-tableau.py : lecture d'une photo de tableau de bord par la Spark,
  confirmation avant envoi au concierge, % ignore si branchee depuis > 10 min.
- stoik-transfert.py : rapports Stoik de la boite AIRTRIA vers la Gmail, une fois.
- AGENTS.md et reference-concierge.md alignes.
- .gitignore : deux scripts ajoutes, DREAMS de l'agent bilans exclu.
This commit is contained in:
2026-10-04 15:50:25 +02:00
parent b6dccc0481
commit 4cd3615172
11 changed files with 564 additions and 40 deletions

View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
# lecture-tableau.py — lit le kilométrage et le niveau de batterie sur une PHOTO de tableau de bord ou une
# CAPTURE de l'application constructeur, envoyée par Thierry sur WhatsApp (04/10/2026, « GO photo Jeep »).
# Le modèle de la Spark (vision) lit ; ce script met la lecture EN ATTENTE ; rien n'est écrit tant que
# Thierry n'a pas confirmé. La confirmation passe le texte au concierge, seul chemin d'écriture.
# python3 lecture-tableau.py lire <chemin_image> [JEEP|VW|ZERO] → lit, imprime une ligne, met en attente
# python3 lecture-tableau.py confirmer → envoie la lecture en attente au concierge
# python3 lecture-tableau.py annuler → efface la lecture en attente
# Le véhicule vient de Thierry (légende de la photo), jamais de l'image.
import base64, json, os, re, subprocess, sys, time, urllib.request
WS = "/home/node/.openclaw/workspace"
PENDING = os.path.join(WS, "releve-photo-pending.json")
SPARK = os.environ.get("SPARK_URL", "http://10.10.10.8:8000/v1/chat/completions")
HA = os.environ.get("HA_BASE", "http://10.10.10.17:8123")
def minutes_branchee():
"""Minutes depuis le branchement si une voiture est branchée, sinon None (lecture HA seule)."""
try:
tok = open(os.path.join(WS, ".ha-token")).read().strip()
r = urllib.request.urlopen(urllib.request.Request(
f"{HA}/api/states/binary_sensor.victron_evcs_vehicle_connected",
headers={"Authorization": f"Bearer {tok}"}), timeout=10)
st = json.load(r)
if st.get("state") != "on": return None
from datetime import datetime, timezone
return (datetime.now(timezone.utc) - datetime.fromisoformat(st["last_changed"])).total_seconds() / 60
except Exception:
return None
VEHICULES = {"JEEP": "JEEP", "VW": "VW", "ZERO": "ZERO", "MOTO": "ZERO", "ZÉRO": "ZERO"}
PROMPT = ("Photo d'un tableau de bord de véhicule, ou capture d'une application constructeur. Réponds en JSON "
"strict, sans texte autour : {\"kilometrage_km\": entier ou null, \"batterie_pct\": entier ou null, "
"\"autonomie_electrique_km\": entier ou null}. kilometrage_km = l'ODOMÈTRE (kilométrage total du "
"véhicule, souvent 5 ou 6 chiffres), JAMAIS une autonomie ni un trajet. batterie_pct = le pourcentage "
"de la batterie de traction s'il est écrit en chiffres, sinon null (ne pas l'estimer d'après une jauge). "
"Valeur illisible ou absente = null.")
def lire(img, veh):
if not os.path.isfile(img): print(f"Lecture impossible : image introuvable ({img})."); return
ext = os.path.splitext(img)[1].lower().lstrip(".") or "jpeg"
data = base64.b64encode(open(img, "rb").read()).decode()
body = {"model": "qwen3-coder", "temperature": 0, "max_tokens": 200, "messages": [{"role": "user", "content": [
{"type": "text", "text": PROMPT},
{"type": "image_url", "image_url": {"url": f"data:image/{'jpeg' if ext == 'jpg' else ext};base64,{data}"}}]}]}
try:
r = urllib.request.urlopen(urllib.request.Request(SPARK, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"}), timeout=240)
txt = json.load(r)["choices"][0]["message"]["content"]
v = json.loads(re.search(r"\{.*\}", txt, re.S).group(0))
except Exception as e:
print(f"Lecture impossible : le modèle n'a pas répondu correctement ({e})."); return
km, pct, aut = v.get("kilometrage_km"), v.get("batterie_pct"), v.get("autonomie_electrique_km")
km = km if isinstance(km, int) and 1000 <= km <= 2_000_000 else None
pct = pct if isinstance(pct, int) and 0 <= pct <= 100 else None
if km is None:
print("📷 Kilométrage illisible sur l'image : rien à noter. Donne-le par écrit (« JEEP 112716 km 46 % »)."); return
lu = f"{km:,} km".replace(",", " ") + (f", batterie {pct} %" if pct is not None else ", batterie non lisible")
if isinstance(aut, int): lu += f", autonomie électrique {aut} km"
if not veh:
print(f"📷 Lu sur l'image : {lu}. Quel véhicule est-ce (JEEP, VW, ZERO) ? Rien n'est noté."); return
# 04/10 (EVCS) : le relevé est daté au branchement ; un % lu sur une photo prise après plus de 10 min de
# charge n'est plus celui du branchement et fausserait km/kWh et capacité → on ne garde que le kilométrage.
mb = minutes_branchee()
note = ""
if pct is not None and mb is not None and mb > 10:
note = (f" Niveau NON retenu : la voiture charge depuis {int(mb)} min, le % de la photo n'est plus celui "
f"du branchement (photo valable pour le niveau seulement au retour, avant de brancher).")
pct = None
lu = f"{km:,} km".replace(",", " ") + (f", autonomie électrique {aut} km" if isinstance(aut, int) else "")
texte = f"{veh} {km} km" + (f" {pct} %" if pct is not None else "")
json.dump({"texte": texte, "lu": lu, "vehicule": veh, "image": img, "a": int(time.time())}, open(PENDING, "w"))
print(f"📷 Lu sur l'image : {veh}, {lu}.{note} Je note ce relevé ? Réponds « oui » ou « non ». Rien n'est encore noté.")
def confirmer():
if not os.path.exists(PENDING): print("Aucune lecture de photo en attente : rien à noter."); return
p = json.load(open(PENDING))
if time.time() - p["a"] > 3600:
os.remove(PENDING); print("La lecture de photo date de plus d'une heure : annulée, renvoie la photo."); return
res = subprocess.run(["node", os.path.join(WS, "concierge-live.js"), "answer", p["texte"]],
capture_output=True, text=True, timeout=120)
os.remove(PENDING)
print((res.stdout.strip() or f"Rien n'est noté : erreur du concierge ({res.stderr.strip()[:200]})."))
cmd = sys.argv[1] if len(sys.argv) > 1 else ""
if cmd == "lire" and len(sys.argv) > 2:
lire(sys.argv[2], VEHICULES.get(sys.argv[3].upper()) if len(sys.argv) > 3 else None)
elif cmd == "confirmer": confirmer()
elif cmd == "annuler":
if os.path.exists(PENDING): os.remove(PENDING)
print("Lecture de photo annulée : rien n'est noté.")
else: print("usage : lecture-tableau.py lire <image> [JEEP|VW|ZERO] | confirmer | annuler")