SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%

hydro : flux live fonctionnel (2captcha) — structure API réelle

- lieu-consommation : clé listeLieuxConsommation + cleUnique au niveau racine,
  code postal reformaté avec espace (exigé), idConso = item.id
- estimation : {cleUnique, idConso} -> montantAnnuel + consommationAnnuelle
- expose kwh_annuel ; testé live : 20 rue Lockwell = 283 $/an / 1100 kWh (12 s),
  2e appel servi du cache
Activé via HQ_CAPTCHA_KEY (2captcha) dans .env.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 24, 2026) parent 08a2c6c

3 changed files +29 −24

modified frontend/src/api.ts +1 −1
@@ -640,7 +640,7 @@ export const fetchCommerces = (lat: number, lng: number) =>
640 640
641 641 export interface HydroEstimate {
642 642 disponible: boolean; raison?: string; adresse?: string;
643 − cout_annuel?: number; cout_mensuel?: number; cache?: boolean;
643 + cout_annuel?: number; cout_mensuel?: number; kwh_annuel?: number; cache?: boolean;
644 644 }
645 645
646 646 /** Coût d'électricité estimé à l'adresse (outil public Hydro-Québec). */
modified frontend/src/components/HydroEstimation.tsx +4 −3
@@ -42,9 +42,10 @@ export default function HydroEstimation({ adresse }:
42 42 </span>
43 43 </div>
44 44 <p className="fine">
45 − Estimation Hydro-Québec pour {d.adresse} — fondée sur la
46 − consommation réelle du logement. Le montant réel varie selon
47 − l'occupation et les habitudes.
45 + Estimation Hydro-Québec pour {d.adresse}
46 + {d.kwh_annuel ? ` — ${d.kwh_annuel.toLocaleString("fr-CA")} kWh/an` : ""},
47 + fondée sur la consommation réelle du logement. Le montant réel
48 + varie selon l'occupation et les habitudes.
48 49 </p>
49 50 </>
50 51 ) : d && !d.disponible ? (
modified louka/hydro.py +24 −20
@@ -151,16 +151,17 @@ def _parse_addr(adresse: str) -> tuple[str, str] | None:
151 151 return civ.group(1), cp.group(1).upper().replace(" ", "")
152 152
153 153
154 −def _montant(d: dict) -> tuple[float | None, float | None]:
155 − """Retrouve le coût annuel/mensuel dans la réponse d'estimation."""
156 − txt = json.dumps(d)
157 − an = re.search(r'"(?:coutAnnuel|montantAnnuel|cout|montant)"\s*:\s*'
158 − r'([0-9]+(?:\.[0-9]+)?)', txt)
159 − a = float(an.group(1)) if an else None
160 − if a and a < 400: # valeur mensuelle -> annualiser
161 − return round(a * 12), round(a)
162 − return (round(a) if a else None,
163 − round(a / 12) if a else None)
154 +def _montant(d: dict) -> tuple[float | None, float | None, int | None]:
155 + """(coût annuel, coût mensuel, conso annuelle kWh) de l'estimation."""
156 + a = d.get("montantAnnuel")
157 + kwh = d.get("consommationAnnuelle")
158 + if a is None:
159 + m = re.search(r'"montantAnnuel"\s*:\s*([0-9]+(?:\.[0-9]+)?)',
160 + json.dumps(d))
161 + a = float(m.group(1)) if m else None
162 + if a is None:
163 + return None, None, kwh
164 + return round(a), round(a / 12), kwh
164 165
165 166
166 167 def estimate(civic: str | None = None, postal: str | None = None,
@@ -178,6 +179,8 @@ def estimate(civic: str | None = None, postal: str | None = None,
178 179 postal = postal.upper().replace(" ", "")
179 180 cle = f"{civic}|{postal}"
180 181
182 + postal_espace = re.sub(r"^([A-Z]\d[A-Z])\s*(\d[A-Z]\d)$", r"\1 \2", postal)
183 +
181 184 con = _connect()
182 185 row = con.execute("SELECT * FROM hydro_cache WHERE cle=? AND fetched_at>?",
183 186 (cle, time.time() - TTL)).fetchone()
@@ -197,24 +200,25 @@ def estimate(civic: str | None = None, postal: str | None = None,
197 200 return {"disponible": False, "raison": "échec du captcha"}
198 201
199 202 opener = _opener()
200 − lieux = _post(opener, URL_LIEU, {"noCivique": civic, "codePostal": postal,
203 + lieux = _post(opener, URL_LIEU, {"noCivique": civic,
204 + "codePostal": postal_espace,
201 205 "recaptchaResponse": token})
202 − items = (lieux or {}).get("lieuxConsommation") or (lieux or {}).get(
203 − "lieux") or (lieux if isinstance(lieux, list) else [])
204 − if not items:
206 + items = (lieux or {}).get("listeLieuxConsommation") or []
207 + cle_unique = (lieux or {}).get("cleUnique")
208 + if not items or not cle_unique:
205 209 con.close()
206 − return {"disponible": False, "raison": "adresse inconnue d'Hydro-Québec"}
210 + return {"disponible": False,
211 + "raison": "adresse inconnue d'Hydro-Québec"}
207 212 lieu = items[0]
208 − id_conso = lieu.get("idConso") or lieu.get("id")
209 − cle_unique = lieu.get("cleUnique") or lieu.get("cle")
210 − adr = lieu.get("adresse") or f"{civic}, {postal}"
213 + id_conso = lieu.get("id")
214 + adr = lieu.get("libelleAdresse") or f"{civic} {lieu.get('rue', '')}".strip()
211 215
212 216 est = _post(opener, URL_ESTIM, {"cleUnique": cle_unique,
213 217 "idConso": id_conso})
214 218 if not est:
215 219 con.close()
216 220 return {"disponible": False, "raison": "estimation indisponible"}
217 − an, mens = _montant(est)
221 + an, mens, kwh = _montant(est)
218 222 if not an:
219 223 con.close()
220 224 return {"disponible": False, "raison": "montant introuvable"}
@@ -223,4 +227,4 @@ def estimate(civic: str | None = None, postal: str | None = None,
223 227 (cle, adr, an, mens, json.dumps(est), time.time()))
224 228 con.close()
225 229 return {"disponible": True, "adresse": adr, "cout_annuel": an,
226 − "cout_mensuel": mens, "cache": False}
230 + "cout_mensuel": mens, "kwh_annuel": kwh, "cache": False}
227 231