diff --git a/makelaars.md b/makelaars.md index 66323f8..0e4a09e 100644 --- a/makelaars.md +++ b/makelaars.md @@ -31,7 +31,7 @@ | [x] | De Witte Garantiemakelaars | dewittegarantiemakelaars.nl | Philippusweg 2 | | [x] | Makelaardij Wassenaar | makelaardijwassenaar.nl | Gerrit Verboonstraat 12 | | [x] | 3D Makelaars | 3dmakelaars.nl | Gerrit Verboonstraat 17 | -| [ ] | Dupont Makelaars | dupont.nl | Rotterdamsedijk 437 | +| [x] | Dupont Makelaars | dupont.nl | Rotterdamsedijk 437 | | [x] | D&S Makelaardij | densmakelaars.nl | Land van Belofte 50 | | [ ] | Moerman & De Jong Makelaars | moerman-dejong.nl | Lange Kerkstraat 80B | | [ ] | Hagestein Makelaardij | — | Degerfors 54 | diff --git a/src/adapters/ssr.py b/src/adapters/ssr.py index 366df42..9793ec9 100644 --- a/src/adapters/ssr.py +++ b/src/adapters/ssr.py @@ -734,6 +734,128 @@ def fetch_3dmakelaars() -> list[RawListing]: return listings +# --------------------------------------------------------------------------- +# Dupont ERA Makelaars (Schiedam/Rotterdam) +# --------------------------------------------------------------------------- + +_DUPONT_BASE = "https://www.dupont.nl" + +_DUPONT_STATUS_MAP = { + "te koop": "beschikbaar", + "nieuw": "beschikbaar", + "onder bod": "onder_bod", + "verkocht onder voorbehoud": "onder_bod", + "verkocht": "verkocht", +} + + +def _dupont_detail(detail_url: str) -> dict: + """Fetch Dupont detail page and extract kenmerken from dt/dd pairs.""" + try: + soup = fetch_soup(detail_url) + + # Parse dt/dd pairs into label → value map + kv: dict[str, str] = {} + dts = soup.select("dt") + dds = soup.select("dd") + + for dt, dd in zip(dts, dds): + label = dt.get_text(strip=True).lower() + value = dd.get_text(strip=True) + kv[label] = value + + # Extract postcode from small tag (format: "NNNN AA CITY") + postcode = None + small_tag = soup.select_one("section div.container-fluid small") + if small_tag: + postcode = _extract_postcode(small_tag.get_text()) + + return { + "postcode": postcode, + "woningtype": kv.get("soort woning"), + "bouwjaar": kv.get("bouwjaar"), + "woonoppervlak": kv.get("woonoppervlakte"), + "kamers": kv.get("aantal kamers"), + "slaapkamers": kv.get("aantal slaapkamers"), + "energielabel": kv.get("energielabel"), + } + except Exception as e: + log.warning("dupont: detail fetch fout %s: %s", detail_url, e) + return {} + + +def fetch_dupont() -> list[RawListing]: + """Fetch Dupont ERA Makelaars listings with pagination and detail pages.""" + listings = [] + page = 1 + + while True: + url = f"{_DUPONT_BASE}/aanbod/koopwoningen?page={page}" + soup = fetch_soup(url) + cards = soup.select("article.object") + if not cards: + break + + for card in cards: + try: + # Extract URL + a_tag = card.select_one("a[href]") + if not a_tag or "href" not in a_tag.attrs: + continue + detail_url = a_tag["href"] + if not detail_url.startswith("http"): + detail_url = _DUPONT_BASE + detail_url + + # Extract listing-level data + adres = _text(card, "h3") + stad = _text(card, "h4") + prijs_text = _text(card, "div.price") + prijs = parse_prijs(prijs_text) + + # Extract status from label + status_label = _text(card, "div.label") or "beschikbaar" + status_label = status_label.strip().lower() + status = _DUPONT_STATUS_MAP.get(status_label, "beschikbaar") + + # Extract image + img_tag = card.select_one("img.img-responsive") + hero = img_tag["src"] if img_tag else None + if hero and not hero.startswith("http"): + hero = _DUPONT_BASE + hero + + # Fetch detail page for full data + detail_data = _dupont_detail(detail_url) + + # Use postcode from detail if available + postcode = detail_data.get("postcode") + + listings.append(RawListing( + url=detail_url, + source_makelaar="dupont", + adres=adres, + postcode=postcode, + stad=stad or _infer_stad(postcode), + prijs=prijs, + status=status, + hero_image_url=hero, + woningtype=detail_data.get("woningtype"), + bouwjaar=int(detail_data["bouwjaar"]) if detail_data.get("bouwjaar") else None, + woonoppervlak=parse_m2(detail_data.get("woonoppervlak")), + kamers=int(detail_data["kamers"]) if detail_data.get("kamers") else None, + slaapkamers=int(detail_data["slaapkamers"]) if detail_data.get("slaapkamers") else None, + energielabel=detail_data.get("energielabel"), + )) + except Exception as e: + log.warning("dupont: parse fout: %s", e) + + if len(cards) < 10: + break + page += 1 + + log.info("dupont: %d listings opgehaald", len(listings)) + return listings + + # --------------------------------------------------------------------------- # SCRAPERS — exporteer hier alle actieve SSR adapters # --------------------------------------------------------------------------- @@ -745,4 +867,5 @@ SCRAPERS = { 'wassenaar': fetch_wassenaar, 'dens': fetch_dens, '3dmakelaars': fetch_3dmakelaars, + 'dupont': fetch_dupont, } diff --git a/tests/test_adapters.py b/tests/test_adapters.py index ed6cb67..00d242a 100644 --- a/tests/test_adapters.py +++ b/tests/test_adapters.py @@ -10,13 +10,13 @@ from adapters import SCRAPERS logging.basicConfig( stream=sys.stdout, - level=logging.DEBUG, + level=logging.INFO, # debug costs too many tokens format="%(asctime)s %(levelname)s %(name)s — %(message)s", datefmt="%Y-%m-%dT%H:%M:%S", ) # --- change this to test a different adapter --- -ADAPTER = SCRAPERS['3dmakelaars'] +ADAPTER = SCRAPERS['dupont'] if __name__ == "__main__": print(f"Testing adapter: {ADAPTER.__name__}") diff --git a/tests/test_ha.py b/tests/test_ha.py index e69de29..04054b1 100644 --- a/tests/test_ha.py +++ b/tests/test_ha.py @@ -0,0 +1,37 @@ +import sys +sys.path.insert(0, "../src") + +import logging + +from huizenbot import notify_ha, RawListing + + +logging.basicConfig( + stream=sys.stdout, + level=logging.INFO, # debug costs too many tokens + format="%(asctime)s %(levelname)s %(name)s — %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S", +) + + +TEST_LISTING = RawListing( + url="https://home.kalsbeek.dev/api/webhook/new_house", + source_makelaar="test", + adres="Teststraat 1", + stad="Delft", + postcode="2613AA", + prijs=350000, + hero_image_url=None, +) + +TEST_TRAVEL = { + "fiets_persoon1": 20, + "fiets_persoon2": 35, + "ov_persoon1": 30, + "ov_persoon2": 45, +} + +if __name__ == "__main__": + print("=== Home Assistant webhook ===") + notify_ha(TEST_LISTING, TEST_TRAVEL) + print(" verstuurd (check HA voor bevestiging)")