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
httpxrequest, parsed JSON direct naarRawListing
SSR-based (HTML scraping):
- Doet een
httpxrequest met netteUser-Agentheader - 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
imagein 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-Agentmet contactinfo (zie config)- Één request tegelijk per domein
- Respecteer
Retry-Afterbij 429-responses - Geen nachtelijke runs
- Alleen persoonlijk gebruik