agent bourse, revue glissante, niveau VW estime depuis l'autonomie, alertes

This commit is contained in:
2026-10-12 17:15:53 +02:00
parent 740c83e20e
commit 9d100b45c6
20 changed files with 535 additions and 20 deletions

View File

@@ -29,6 +29,22 @@ def minutes_branchee():
return None
VEHICULES = {"JEEP": "JEEP", "VW": "VW", "ZERO": "ZERO", "MOTO": "ZERO", "ZÉRO": "ZERO"}
# 12/10/2026 (propriétaire) : la VW n'affiche pas de % mais l'autonomie électrique. Le % est alors DÉDUIT ici
# (règle de trois sur l'autonomie pleine lue dans HA, recalée par EVCS), jamais par l'agent ; la ligne dit
# « estimé » et le relevé porte la source « photo_autonomie:<km>/<réf> » (EVCS : la capacité peut l'écarter).
AUTONOMIE_PLEINE = {"VW": "input_number.victron_evcs_autonomie_vw_km"}
def autonomie_pleine(veh):
"""Autonomie à 100 % lue dans HA ; None si absente ou illisible (alors pas d'estimation)."""
if veh not in AUTONOMIE_PLEINE: return None
try:
tok = open(os.path.join(WS, ".ha-token")).read().strip()
st = json.load(urllib.request.urlopen(urllib.request.Request(f"{HA}/api/states/{AUTONOMIE_PLEINE[veh]}",
headers={"Authorization": f"Bearer {tok}"}), timeout=10))
v = round(float(st["state"]))
return v if 10 <= v <= 1000 else None
except Exception:
return None
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 "
@@ -55,8 +71,14 @@ def lire(img, veh):
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"
estime, source = "", None
ref = autonomie_pleine(veh) if pct is None and isinstance(aut, int) else None
if ref and 0 <= aut <= ref * 1.2:
pct = min(100, round(aut * 100 / ref))
estime = f" (estimé : {aut} km d'autonomie sur {ref} km à 100 %)"
source = f"photo_autonomie:{aut}/{ref}"
lu = f"{km:,} km".replace(",", " ") + (f", batterie {pct} %{estime}" if pct is not None else ", batterie non lisible")
if isinstance(aut, int) and not estime: 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
@@ -66,10 +88,10 @@ def lire(img, veh):
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
pct, source = None, 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"))
json.dump({"texte": texte, "lu": lu, "vehicule": veh, "image": img, "a": int(time.time()), "source": source}, 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():
@@ -77,8 +99,10 @@ def confirmer():
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
env = dict(os.environ)
if p.get("source"): env["CONCIERGE_RELEVE_SOURCE"] = p["source"] # niveau estimé → 5e champ du relevé
res = subprocess.run(["node", os.path.join(WS, "concierge-live.js"), "answer", p["texte"]],
capture_output=True, text=True, timeout=120)
capture_output=True, text=True, timeout=120, env=env)
os.remove(PENDING)
print((res.stdout.strip() or f"Rien n'est noté : erreur du concierge ({res.stderr.strip()[:200]})."))