Door Olaf Weijers

Redacteur

Zo laat een ESP32 Home Assistant je voordeur lokaal ontgrendelen

20-09-2026 • 06:00

7

Privacyvriendelijke geofencing met Home Assistant

De afgelopen jaren heb ik veel slimme sloten getest. Geofencing vind ik een van de fijnste functies. Die houdt je locatie bij en opent de deur automatisch als je na afwezigheid weer in de buurt komt.

Voor de functie ontkom je er niet aan je locatie te delen met de app die bij je slot hoort. Dat zal niet iedereen prettig vinden. Afhankelijk van de fabrikant komt je locatie in cloudsystemen terecht. Die data kan in verkeerde handen vallen of verkocht worden aan derden.

Om dat tegen te gaan, ben ik gaan experimenteren met Home Assistant om een eigen geofencingfunctie te bouwen waarbij je locatiegegevens in eigen beheer blijven. In dit artikel laat ik zien hoe je dit zelf kunt opzetten voor slechts een paar euro.

Hoe werkt het?

Home Assistant Companion deelt je locatie met Home Assistant en gebruikt je telefoon om een bluetoothbeacon uit te zenden. Deze functie werkt vooralsnog alleen met Android-telefoons. Gebruikers van een iPhone hebben dus een goede reden om over te stappen of zullen moeten wachten tot hun telefoon ook een beacon kan emuleren.

Zodra Home Assistant ziet dat je na afwezigheid weer in de buurt van je huis bent, draagt het systeem de Companion-app op om het beaconsignaal te activeren.

De bluetoothradio op het ESP32-bord luistert totdat die het signaal van de beacon ontvangt. Dat bevestigt dat je fysiek bij de voordeur bent aangekomen en is voor Home Assistant het sein om de voordeur te ontgrendelen.

Wat heb je nodig?

Met Home Assistant en een slim slot heb je bijna alles al in huis. Om geofencing op te zetten, heb je drie componenten nodig. De eerste is een slim slot dat met Home Assistant verbonden is. De overige twee zijn de Home Assistant Companion-app op je smartphone en een experimenteerbord met een ESP32-microcontroller.

Van de ESP32-microcontroller zijn meerdere varianten verkrijgbaar. Voor geofencing hebben we een uitvoering met bluetooth en wifi nodig. Geschikte varianten zijn de ESP32, C3, C6 en S3. Let op: de ESP32-S2 en ESP32-H2 hebben respectievelijk geen bluetooth en wifi. Die moet je vermijden.

Ook een op de ESP8266 gebaseerd bord werkt niet. Het draait wel ESPHome, maar ondersteunt geen bluetooth en is dus niet geschikt voor dit doel.

Het ESP32-bord moet een plekje krijgen in de buurt van de voordeur of langs de route ernaartoe. Op de beoogde plek heeft het een USB-voeding nodig. De plaatsing ervan is erg belangrijk voor de timing. Als het bord te ver weg staat, zal geofencing niet of te laat werken.

Home Assistant Companion voorbereiden

Voor geofencing gebruikt Home Assistant Companion de locatie- en beaconfunctie. We gaan ervan uit dat je je locatie al deelt met Home Assistant. Zo niet, dan moet je locatiedeling eerst activeren.

Ga daarvoor in de app naar de instellingen van Home Assistant en kies de optie 'Companion-app'.

Daar kies je voor 'Sensoren beheren' en zoek je naar de groep bluetoothsensoren. Daarin open je de 'BLE-zender (Bluetooth Low Energy)'. Bovenaan dit venster activeer je de beacon. Verderop vind je wat geavanceerde instellingen. Hier kies je bij 'Adverteermodus' voor 'Lage latentie (10Hz)' en bij 'Zendvermogen' voor 'Hoog'.

Controleer ook of je de instelling hebt uitgeschakeld die het uitzenden van de beacon beperkt als je niet met wifi verbonden bent.

Voor een soepele werking mag de Companion-app niet slapen als je thuiskomt. Deze moet zo snel mogelijk reageren op een notificatiebericht met hoge prioriteit. Zorg er daarom voor dat het energiegebruik niet wordt beperkt. Dit doe je door in Android de batterijinstellingen voor de Companion-app naar 'unrestricted' te zetten. Om de beacon uit te zenden, heeft Home Assistant Companion onder de locatieinstellingen ook toestemming voor 'Apparaten in de buurt' nodig. Androids energiebeperking is de grootste vijand voor soepele geofencing, ook bij commerciële apps. Zelfs met optimale instellingen is dit de meest veranderlijke factor voor de reactietijd.

Home Assistant voorbereiden

Om de geofencing enkel te starten als je naar huis reist, heb je de nabijheidssensor nodig. Die installeer je door op onderstaande link te klikken.

Home Assistant add integration knop

Nadat je de nabijheidssensor hebt geïnstalleerd, kies je in de configuratie eerst een zone (meestal 'Thuis') en daarna welke personen of apparaten je wilt volgen voor geofencing.

De ESP32 voorbereiden en flashen

Op de ESP32 gebruiken we de ESPHome-firmware. Dit pakket integreert naadloos met Home Assistant en je kunt het gebruiken om allerlei hardware te bouwen op basis van ESP32- en ESP8266-microcontrollers.

Klik op onderstaande link om ESPHome in Home Assistant te installeren.

Home Assistant add integration knop

Na de installatie verschijnt de ESPHome Device Builder in de navigatiebalk van Home Assistant. Is die niet zichtbaar, dan kun je de app vinden onder Instellingen -> Apps. Hier vind je ook de optie om de app op de navigatiebalk te tonen.

Binnen de ESPHome Device Builder klik je rechtsonder op de knop 'Apparaat aanmaken'.

Zoek in de volgende stappen je experimenteerbord in de lijst met mogelijkheden. Staat het bord ertussen, kies het dan. Anders werkt 'Generic ESP32 board' voor deze toepassing ook prima met elke variant van de ESP32-microcontroller.

Kies je bij de apparaatnaam voor 'geofencing', dan komen de entiteitsnamen in de automatiseringen uit deze handleiding overeen met die in Home Assistant. Kies je een andere naam, gebruik die dan in de entiteitsnamen van de automatiseringen verderop in dit artikel.

Als je het apparaat hebt aangemaakt, kun je de configuratie bewerken. Aan de rechterkant van het bewerkingsvenster vind je de configuratie in tekstvorm. De standaardconfiguratie eindigt bij 'captive_portal:'.

Laat de standaardconfiguratie staan, maar voeg onder de laatste regel de volgende tekst toe:


globals:
  - id: laatste_ibeacon_melding
    type: uint32_t
    restore_value: no
    initial_value: '0'

esp32_ble_tracker:
  scan_parameters:
    active: false
  on_ble_advertise:
    - then:
        - lambda: |-
            auto beacon = x.get_ibeacon();
            if (!beacon.has_value()) return;
            char buf[37];
            beacon->get_uuid().to_str(buf);
            std::string uuid(buf);
            for (auto &c : uuid) c = tolower(c);
            int rssi = x.get_rssi();
            uint32_t nu = millis();
            bool andere_beacon = id(laatst_geziene_ibeacon).state.compare(0, 36, uuid) != 0;
            if (nu - id(laatste_ibeacon_melding) >= (andere_beacon ? 10000 : 60000)) {
              id(laatste_ibeacon_melding) = nu;
              id(laatst_geziene_ibeacon).publish_state(uuid + " (" + std::to_string(rssi) + " dBm)");
            }
            const std::string &lijst = id(uuid_lijst).state;
            size_t start = 0;
            while (start < lijst.size()) {
              size_t eind = lijst.find(',', start);
              if (eind == std::string::npos) eind = lijst.size();
              if (lijst.compare(start, eind - start, uuid) == 0) {
                id(signaalsterkte_telefoon).publish_state(rssi);
                if (!id(openen_bij_eerste_ontvangst).state && rssi < id(minimale_rssi).state) return;
                if (id(gematchte_uuid).state != uuid) id(gematchte_uuid).publish_state(uuid);
                return;
              }
              start = eind + 1;
            }

text:
  - platform: template
    name: UUID-lijst
    id: uuid_lijst
    mode: text
    max_length: 255
    set_action:
      - lambda: |-
          std::string opdracht = x;
          for (auto &c : opdracht) c = tolower(c);
          std::string lijst = id(uuid_lijst).state;
          std::string uuid;
          bool toevoegen = false, verwijderen = false, nieuw = false;
          if (opdracht.empty()) {
            lijst = "";
          } else if (opdracht.size() == 37 && (opdracht[0] == '+' || opdracht[0] == '-')) {
            uuid = opdracht.substr(1);
            toevoegen = opdracht[0] == '+';
            verwijderen = !toevoegen;
          } else if (opdracht.size() == 9 && opdracht[0] == 'n') {
            uint8_t bytes[16];
            if (random_bytes(bytes, sizeof(bytes))) {
              const char *hex = "0123456789abcdef";
              for (int i = 0; i < 16; i++) {
                if (i == 4 || i == 6 || i == 8 || i == 10) uuid += '-';
                uuid += hex[bytes[i] >> 4];
                uuid += hex[bytes[i] & 0x0f];
              }
              toevoegen = true;
              nieuw = true;
            }
          }
          if (toevoegen || verwijderen) {
            std::string bijgewerkt;
            size_t start = 0;
            while (start < lijst.size()) {
              size_t eind = lijst.find(',', start);
              if (eind == std::string::npos) eind = lijst.size();
              std::string item = lijst.substr(start, eind - start);
              if (!item.empty() && item != uuid) bijgewerkt += (bijgewerkt.empty() ? "" : ",") + item;
              start = eind + 1;
            }
            if (toevoegen) {
              while (bijgewerkt.size() + 37 > 255) {
                size_t komma = bijgewerkt.find(',');
                bijgewerkt = komma == std::string::npos ? "" : bijgewerkt.substr(komma + 1);
              }
              bijgewerkt += (bijgewerkt.empty() ? "" : ",") + uuid;
            }
            lijst = bijgewerkt;
          }
          id(uuid_lijst).publish_state(lijst);
          if (nieuw) id(nieuwe_uuid).publish_state(opdracht.substr(1) + ":" + uuid);

text_sensor:
  - platform: template
    name: Gematchte UUID
    id: gematchte_uuid
  - platform: template
    name: Nieuwe UUID
    id: nieuwe_uuid
    icon: mdi:key-variant
    entity_category: diagnostic
  - platform: template
    name: Laatst geziene iBeacon
    id: laatst_geziene_ibeacon
    icon: mdi:bluetooth-audio
    entity_category: diagnostic
    disabled_by_default: true

sensor:
  - platform: template
    name: Signaalsterkte toegestane telefoon
    id: signaalsterkte_telefoon
    unit_of_measurement: dBm
    device_class: signal_strength
    state_class: measurement
    accuracy_decimals: 0
    entity_category: diagnostic
    filters:
      - throttle: 1s
      - timeout: 10s

number:
  - platform: template
    name: Minimale RSSI
    id: minimale_rssi
    icon: mdi:signal-distance-variant
    entity_category: config
    optimistic: true
    restore_value: true
    initial_value: -85
    min_value: -100
    max_value: -40
    step: 1
    unit_of_measurement: dBm
    mode: box
  - platform: template
    name: Vertrekafstand
    id: vertrekafstand
    icon: mdi:map-marker-radius-outline
    entity_category: config
    optimistic: true
    restore_value: true
    initial_value: 200
    min_value: 100
    max_value: 5000
    step: 10
    unit_of_measurement: m
    mode: box
  - platform: template
    name: Armafstand
    id: armafstand
    icon: mdi:map-marker-radius
    entity_category: config
    optimistic: true
    restore_value: true
    initial_value: 50
    min_value: 20
    max_value: 1000
    step: 10
    unit_of_measurement: m
    mode: box
  - platform: template
    name: Zoekduur
    id: zoekduur
    icon: mdi:timer-outline
    entity_category: config
    optimistic: true
    restore_value: true
    initial_value: 5
    min_value: 1
    max_value: 60
    step: 1
    unit_of_measurement: min
    mode: box

switch:
  - platform: template
    name: Openen bij eerste ontvangst
    id: openen_bij_eerste_ontvangst
    icon: mdi:bluetooth-connect
    entity_category: config
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
  - platform: template
    name: Slot openen in plaats van ontgrendelen
    id: slot_openen
    icon: mdi:door-open
    entity_category: config
    optimistic: true
    restore_mode: RESTORE_DEFAULT_OFF
  - platform: template
    name: Melding bij ontgrendelen
    id: melding_bij_ontgrendelen
    icon: mdi:bell-ring-outline
    entity_category: config
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON

Hierna kun je de configuratie opslaan en installeren op de ESP32. De eenvoudigste methode is om het ESP32-bord via USB te verbinden met je computer en vanuit de browser te laten flashen.

De ESPHome Device Builder begeleidt je hier doorheen. Flashen via de browser werkt niet met Firefox, maar wel met Chrome of op Chromium gebaseerde browsers zoals Edge.

Als de firmware eenmaal is geflasht, kun je de ESP32 bijwerken via wifi en is USB daarvoor niet meer nodig.

De ESP32 kun je nu op zijn definitieve plek plaatsen: zo dicht mogelijk bij de voordeur of, indien mogelijk, langs het pad daarnaartoe, zodat hij sneller reageert.

De automatisering aanmaken

Voor geofencing met de ESP32-scanner heb je in Home Assistant een automatisering nodig.

De automatisering in dit artikel staat in YAML-formaat. Je kunt die direct kopiëren en plakken. Hiervoor ga je in Home Assistant naar 'Automatiseringen en scènes → Automatisering aanmaken → menu rechtsboven → Bewerken in YAML'.

Alle stappen zijn samengebracht in één automatisering per gebruiker. Voor elke huisgenoot moet je een aparte automatisering aanmaken met de juiste entiteiten. In dit voorbeeld is de automatisering voor 'Bob'. Die naam heb ik vetgedrukt, zodat je deze voor je eigen situatie kan aanpassen.

Daarnaast is de naam 'geofencing' vetgedrukt. Als je de ESP32 zo hebt genoemd, hoef je niets aan te passen. Anders moet je de waarde naar wens aanpassen.

Tot slot selecteer je onder variabelen het juiste slot. In het voorbeeld heet dit simpelweg 'voordeur'.


alias: Geofencing auto-unlock - Bob

mode: single
max_exceeded: silent

variables:
  afstand_sensor: sensor.thuis_bob_distance
  telefoon: notify.mobile_app_telefoon_bob
  slot: lock.voordeur
  uuid_lijst: text.geofencing_uuid_lijst
  match_sensor: sensor.geofencing_gematchte_uuid
  nieuwe_uuid: sensor.geofencing_nieuwe_uuid
  vertrekafstand: number.geofencing_vertrekafstand
  armafstand: number.geofencing_armafstand
  zoekduur: number.geofencing_zoekduur
  slot_openen: switch.geofencing_slot_openen_in_plaats_van_ontgrendelen
  melding: switch.geofencing_melding_bij_ontgrendelen

triggers:
  - trigger: state
    entity_id: sensor.thuis_bob_distance
    not_to:
      - unavailable
      - unknown
  - trigger: homeassistant
    event: start
    id: start

actions:
  - if:
      - condition: trigger
        id: start
    then:
      - action: "{{ telefoon }}"
        data:
          message: command_ble_transmitter
          data:
            command: turn_off
            priority: high
            ttl: 0
      - action: text.set_value
        target:
          entity_id: "{{ uuid_lijst }}"
        data:
          value: ""
  - condition: template
    value_template: "{{ states(afstand_sensor) | float(0) > states(vertrekafstand) | float(99999) }}"
  - wait_template: "{{ states(afstand_sensor) | float(99999) < states(armafstand) | float(0) }}"
  - variables:
      volgnummer: >-
        {% set ns = namespace(h='') %}{% for i in range(8) %}{% set ns.h = ns.h ~ ('0123456789abcdef' | random) %}{% endfor %}{{ ns.h }}
  - action: text.set_value
    target:
      entity_id: "{{ uuid_lijst }}"
    data:
      value: "n{{ volgnummer }}"
  - wait_template: "{{ states(nieuwe_uuid).startswith(volgnummer ~ ':') }}"
    timeout: "00:00:15"
    continue_on_timeout: true
  - if:
      - condition: template
        value_template: "{{ not states(nieuwe_uuid).startswith(volgnummer ~ ':') }}"
    then:
      - stop: De ESP32 leverde geen nieuwe UUID. Staat het apparaat online en draait het het geofencingblok?
        error: true
  - variables:
      uuid: "{{ states(nieuwe_uuid).split(':')[1] }}"
  - action: "{{ telefoon }}"
    data:
      message: command_ble_transmitter
      data:
        command: ble_set_uuid
        ble_uuid: "{{ uuid }}"
        priority: high
        ttl: 0
  - action: "{{ telefoon }}"
    data:
      message: command_ble_transmitter
      data:
        command: turn_on
        priority: high
        ttl: 0
  - wait_template: "{{ is_state(match_sensor, uuid) or states(afstand_sensor) | float(0) > states(vertrekafstand) | float(99999) }}"
    timeout: "{{ (states(zoekduur) | float(5) * 60) | int }}"
    continue_on_timeout: true
  - if:
      - condition: template
        value_template: "{{ is_state(match_sensor, uuid) }}"
    then:
      - action: "{{ 'lock.open' if is_state(slot_openen, 'on') else 'lock.unlock' }}"
        target:
          entity_id: "{{ slot }}"
      - if:
          - condition: template
            value_template: "{{ is_state(melding, 'on') }}"
        then:
          - action: "{{ telefoon }}"
            data:
              title: Geofencing
              message: Het slot is automatisch geopend bij thuiskomst.
  - action: "{{ telefoon }}"
    data:
      message: command_ble_transmitter
      data:
        command: turn_off
        priority: high
        ttl: 0
  - action: text.set_value
    target:
      entity_id: "{{ uuid_lijst }}"
    data:
      value: "-{{ uuid }}"

Geofencingparameters afstellen

Onder 'Integraties -> ESPHome' vind je nu de ESP32-geofencingmodule. Je vindt er alle instellingen om de geofencing op je eigen situatie af te stemmen. Zo kun je de deur direct laten openen bij de eerste ontvangst van het juiste beaconpakket. Je kunt de deur ook pas laten openen als het telefoonsignaal sterker wordt omdat je dichter bij de deur bent.

De verschillende instelmogelijkheden werken als volgt:

Instelling Verklaring
Armafstand Bepaalt op welke afstand van het huis geofencing wordt geactiveerd wanneer je het huis nadert.
Minimale RSSI Bepaalt hoe sterk het telefoonsignaal minimaal moet zijn om het slot te openen.
Openen bij eerste ontvangst Bepaalt of het slot direct bij de eerste ontvangst van het telefoonsignaal wordt geopend, ongeacht de minimale RSSI.
Slot openen in plaats van ontgrendelen Bepaalt of naast de nachtschoot ook de dagschoot wordt geopend. Schakel deze instelling niet in als je slot het openen van de dagschoot niet ondersteunt.
Vertrekafstand Bepaalt hoe ver je minimaal van huis moet zijn geweest voordat geofencing bij terugkomst wordt geactiveerd.
Zoekduur Bepaalt hoelang de telefoon na het bereiken van de armafstand de beacon blijft uitzenden en het ongebruikte beacon-ID geldig blijft.
Melding bij ontgrendelen Geeft een notificatiebericht als de automatisering je deur ontgrendelt.

Hoe veilig is dit?

Geen enkele beveiliging is waterdicht en deze methode zeker niet. Beaconsignalen zijn niet versleuteld en kunnen worden opgevangen. In theorie kan iemand het beaconsignaal ontvangen en klonen. Als deze gekloonde beacon bij je deur wordt uitgezonden voordat je er zelf bent, wordt de deur geopend.

De ID's voor de beacons worden willekeurig gegenereerd voor elke geofencingsessie, waardoor een gebruikte code nooit meer zal werken. Hiervoor gebruikt de ESP32 zijn random number generator. Die haalt zijn willekeur uit ruis in de hardware en is daardoor niet te voorspellen. De willekeurfunctie van Home Assistant is een wiskundige reeks die je na genoeg waarnemingen kunt narekenen.

Wil je misbruik zoveel mogelijk uitsluiten, dan zul je de automatisering nauwkeurig moeten afstellen. Zo voorkom je dat de telefoon het beaconsignaal al uitzendt terwijl je nog kilometers van huis bent. Ook gaat de deur dan niet open terwijl je nog om de hoek komt aanrijden.

In zo'n geval zal een 'kloner' continu bij je in de buurt moeten blijven om het signaal te onderscheppen. Die kan dan misschien alsnog enkele seconden eerder naar binnen lopen dan jij.

De gemiddelde inbreker zal geen geavanceerde bluetoothhacking gebruiken om bij je in te breken. Die slaat een ruit in of breekt je slotcilinder. Heb je een conflict met een vaardige medetweaker of een binnen- of buitenlandse veiligheidsdienst, dan is het altijd een beter idee om helemaal geen slim slot of andere smarthomeapparatuur zoals een spraakbediende televisie te installeren.

Tot slot

Hoewel het heel jammer is dat de Companion-app nog geen beacons kan uitzenden op iPhones, is het relatief simpel en goedkoop om zelf geofencing te bouwen met Home Assistant. Daarmee houd je je gegevens uit handen van datahongerige fabrikanten die ze vaak maar al te graag verzamelen, analyseren en zelfs verkopen aan derden. Niet elke fabrikant van slimme sloten is even datahongerig. Toch is het nog beter om ze niet in verleiding te brengen door de data helemaal niet beschikbaar te stellen.

Redactie: Olaf Weijers • Eindredactie: Monique van den Boomen

Reacties (7)

Sorteer op:

Weergave:

Ik snap niet dat je een hele how to schrijft en dan zegt: maar eigenlijk is dit (in theorie) helemaal niet veilig. Hoe zou iemand zich dan alsnog geroepen voelen om met je how to aan de slag te gaan? Dan heb je het artikel toch helemaal voor nop geschreven?
Een 'sleutel' is een stuke ijzer in een specifiek vormpje geslepen. Iedereen kan dit stelen, klopieren of zelfs afkijken met een foto en een 3d printer tegenwoordig.

Er is nooit 'veilig', maar altijd 'veilig genoeg'.
Alle slimme apparaten zijn per definitie onveilig.

Dan moet je helemaal niet beginnen asn Home Automatusering
Dat is te kort door de bocht. Het opvallende aan dit artikel is dat er (terecht) wordt benoemd wat er allemaal mis kan gaan.
De gemiddelde inbreker zal geen geavanceerde bluetoothhacking gebruiken om bij je in te breken. Die slaat een ruit in of breekt je slotcilinder. Heb je een conflict met een vaardige medetweaker of een binnen- of buitenlandse veiligheidsdienst, dan is het altijd een beter idee om helemaal geen slim slot of andere smarthomeapparatuur zoals een spraakbediende televisie te installeren.
De vraag is hoelang dit nog waar is. Met AI kan je, zeker als je een beetje kennis hebt, veel sneller tooltjes maken en dit soort zaken hacken. Het is met autosleutels ook al gedaan. Maar goed de doelgroep (als in targets) is klein.
Onnodige sneer 'Gebruikers van een iPhone hebben dus een goede reden om over te stappen'. Apple gebruikers gebruiken veelal al HomeKeys. met Ultra Wide band

Om te kunnen reageren moet je ingelogd zijn