Files
huizenbot/huizenbot-spec.md

5.3 KiB

Huizenbot — Project Spec

Doel

Periodiek scrapen van makelaarswebsites in Delft en Schiedam, nieuwe woningen opslaan in SQLite, en pushnotificaties sturen via Home Assistant (+ optioneel email). Draait als één Docker container op homelab met cron.

Projectstructuur

huizenbot/
├── db.py              # schema, migraties, upsert helpers
├── notify.py          # HA webhook + email
├── travel.py          # OSRM (fiets) + Navitia (OV) clients
├── base.py            # AbstractScraper, orchestratie, run loop
├── adapters/          # één file per makelaar
│   ├── bjornd.py
│   └── ...
├── main.py            # entry point (aangeroepen door cron)
├── config.py          # locaties, credentials, endpoints
└── Dockerfile

Nieuwe makelaar toevoegen = nieuwe file in adapters/ die AbstractScraper implementeert. De runner pikt hem automatisch op via de base class registry.

Database

SQLite, één file gemount als Docker volume.

CREATE TABLE woningen (
    id                TEXT PRIMARY KEY,   -- sha256(url)
    url               TEXT UNIQUE NOT NULL,
    source_makelaar   TEXT NOT NULL,
    first_seen        TEXT NOT NULL,      -- ISO8601
    last_seen         TEXT NOT NULL,      -- ISO8601, geüpdatet elke run
    datum_aanmelding  TEXT,               -- datum van makelaar zelf, indien beschikbaar

    status            TEXT NOT NULL DEFAULT 'beschikbaar',
    -- enum: beschikbaar | onder_bod | verkocht

    -- locatie
    adres             TEXT,
    postcode          TEXT,
    stad              TEXT,

    -- woning
    prijs             INTEGER,            -- euros, geen float
    woningtype        TEXT,               -- appartement | tussenwoning | hoekwoning | vrijstaand | ...
    woonoppervlak     INTEGER,            -- m2
    perceeloppervlak  INTEGER,            -- m2, NULL voor appartementen
    kamers            INTEGER,
    slaapkamers       INTEGER,
    bouwjaar          INTEGER,
    energielabel      TEXT,

    -- media
    hero_image_url    TEXT,

    -- reistijd in minuten (berekend bij first_seen, niet opnieuw)
    fiets_persoon1    INTEGER,
    fiets_persoon2    INTEGER,
    ov_persoon1       INTEGER,
    ov_persoon2       INTEGER,

    -- makelaar-specifieke velden die niet in het schema passen
    extra             TEXT                -- JSON (sqlite 3.38+ json_extract() werkt hierop)
);

Upsert strategie: INSERT OR IGNORE op id voor nieuwe woningen, daarna UPDATE last_seen en status op elke run. Reistijd wordt alleen berekend bij first_seen.

Scraper architectuur

AbstractScraper (base.py)

Elke adapter erft hiervan en implementeert één methode:

def fetch_listings(self) -> list[RawListing]:
    ...

RawListing is een dataclass met exact de velden uit het schema (allemaal optioneel behalve url). De base class regelt:

  • deduplicatie / upsert naar DB
  • reistijdberekening aanroepen voor nieuwe woningen
  • notificatie triggeren voor nieuwe woningen
  • logging

Twee adapter-smaken

API-based (JSON response, bijv. makelaars met een interne REST API):

  • Doet een httpx request, parsed JSON direct naar RawListing

SSR-based (HTML scraping):

  • Doet een httpx request met nette User-Agent header
  • Parsed HTML via BeautifulSoup

Beide smaken zijn gewone subclasses — geen aparte base per smaak, het verschil zit alleen in de implementatie van fetch_listings.

Reistijd (travel.py)

Twee backends, beiden aangeroepen bij first_seen van een woning.

Fiets — OSRM (publieke instance, geen key nodig):

http://router.project-osrm.org/route/v1/cycling/{lon},{lat};{lon},{lat}?overview=false

OV — Navitia.io (gratis tier, API key nodig):

https://api.navitia.io/v1/coverage/nl/journeys?from=...&to=...&datetime=...

Voor OV wordt een vaste reistijd op een doordeweekse ochtend (bijv. 08:30) gebruikt als referentie — niet real-time.

Invoer zijn twee postcodes uit config.py (werklocatie persoon 1 en 2). Postcode → coördinaten via de Nominatim geocoder (OSM, geen key nodig, respecteer 1 req/s limiet).

Notificaties (notify.py)

Home Assistant

Webhook call naar HA met een payload die de notification service aanroept. Bevat:

  • adres + stad
  • prijs
  • status
  • hero image URL (als image in de notification)
  • reistijden persoon 1 en 2 (fiets + OV)
  • directe link naar de listing

Email (optioneel, fallback)

Plain HTML mail via SMTP met dezelfde info. Handig als HA buiten bereik is.

Configuratie (config.py)

PERSOON1_WERK_POSTCODE = "2600AA"
PERSOON2_WERK_POSTCODE = "3000AA"

HA_WEBHOOK_URL = "https://ha.jouwdomain.nl/api/webhook/huizenbot"

SMTP_HOST = "..."
SMTP_FROM = "..."
SMTP_TO   = "..."

USER_AGENT = "Huizenbot/1.0 (+jouw@email.nl)"

SCRAPE_INTERVAL_HOURS = 3   # alleen informatief, cron regelt de scheduling

Secrets (API keys etc.) via environment variables, niet in config.py.

Docker & cron

Één container, cron runt main.py elke 3 uur tussen 08:00 en 20:00:

0 8,11,14,17,20 * * * python /app/main.py >> /var/log/huizenbot.log 2>&1

SQLite DB en logfile als named volumes gemount.

Ethische scraping

  • User-Agent met contactinfo (zie config)
  • Één request tegelijk per domein
  • Respecteer Retry-After bij 429-responses
  • Geen nachtelijke runs
  • Alleen persoonlijk gebruik