Alpha Phase
Erwerb Juni
ESP32-C6
Radarsensor mit bis zu 3 Ziele Erkennung gleichzeitig
Unbegrenzte Anzahl von Zonenerkennung über Javascript mit html integration
Helligkeitsanzeige in Lux
Optionaler Anschlussmöglichkeit für Temperatur und Druck Anzeige.
Bausatz für Bastler (ESP32-C6)
Elektronik-Bausatz mit 3m USB Kabel u. optionalem Gehäuse.
Auf Wunsch löte ich ihn privat fertig zusammen.
Platine ist Beigabe (ohne CE, keine Gewährleistung).
➡️ -->Kauflink für ca. 20,99€ - 27,99€<---
Aktuelles Projekt :
Ein auf dem ESP32-C6 basierendes Zigbee-Gerät mit angeschlossenem HLK-LD2450 3D-Radarsensor zur personenbasierten Präsenz- und Bewegungsanalyse – ideal für Smart Home Automation auf Raumebene.
Funktionen & Features:
-
Direkte Zigbee-Integration via eigener Firmware (ESP32-C6 + LD2450 per UART)
-
3 gleichzeitige Personen erkennbar mit Positionsdaten (X/Y), Abstand und Bewegungsrichtung
-
Zigbee-Datenpunkte:
distance
,x
,y
,velocity
,valid
,target_id
,resolution
– für alle Slots -
Volle VIS-Unterstützung (ioBroker) mit HTML-Raumansicht:
-
Echtzeit-Visualisierung der Person(en) im Raum
-
Automatische Rotation des Koordinatensystems je nach Sensorlage
-
Grafische Darstellung inkl. Zonenlogik, z. B. zur Licht- oder Heizungssteuerung
-
Besonderheiten:
-
Ideal für Multiraum-Setups: Jeder Raum mit einem Sensor möglich
-
Extrem geringe Latenz, keine Cloud nötig, läuft komplett lokal
-
Reduziert Fehlauslösungen gegenüber PIR oder mmWave deutlich
-
Perfekt erweiterbar mit ioBroker-Logik oder Blockly-Automationen
Anschluss eines LDR5528 (Lichtsensor) an PIN0. Dadurch Helligkeitsanzeige in Lux
Otional an PIN 21,22 Anschlussmöglichkeit eines BMP280
Unter Vollast ist der Stromverbrauch bei 5 V ca. 0,28 A (entspricht etwa 1,4 W Leistungsaufnahme)
Zusammenbau:
1. Erst den ESP von unten anlöten. Alle Pins an den ESP und danach alle Pins an die Platin!!!
Schön Luft lassen zwischen ESP und Platine. So wie im Bild die Stifte immer nur ein wenig rausragen lassen, zum anlöten.
Vor allem bei der Platine. Das da die Pins nicht weit rausgucken. Immer nur ein klein wenig. So das es halt reicht, zum anlöten.
2. Dann die Bauteile von oben an die schon verlöteten Pins mit dran anlöten.
Bedeutet, hier im Bild hab ich die Bauteile nur zur Veranschaulichung in die Löcher gesteckt.
In Wirklichkeit sind da ja schon die Pins vom ESP drin. Einfach an die Pins so mit anlöten.
Oberhalb der Platine werden die Bauteile so verlötet, das nichts direkt neben oder über dem Radarsensors sich was befindet.
Das würde den Sensor stören, da Radarwellen nicht Metall durchdringen kann.
3. Dann den Sensor draufstecken und von unten verlöten.
Je genauer der Sensor angelötet wird, um so besser.
Also nicht schief anlöten. Je perfekter er waagereicht und senkrecht und gleicher Abstand hat, um so genauer wird er messen.
!! FERTIG !!
LDR_PIN(Lichtsensor) an ESP -> 3,3V und ESP -> 0
10kOhm_Widerstand an ESP -> 0 und ESP -> GND
Optional(BMP280 3,3V muss selber gekauft werden):
BMP_SDA_PIN an ESP -> 21
BMP_SCL_PIN an ESP -> 22
BMP_VCC an ESP -> 3,3V
BMP_GND an ESP -> GND
Verwendete Komponenten:
-
ESP32-C6 https://amzn.to/4kdRQRx
-
HLK-LD2450 https://amzn.to/45ieN1a
LDR5528 https://amzn.to/3FLbLs4
10kOhm Widerstand https://amzn.to/45eAH5I
Optional BMP280 Sensor 3,3V https://amzn.to/4mPZMu3
-
Firmware und ioBroker-Anbindung komplett selbst entwickelt
Anleitung:
1.
Pairing Modus:
Pairing: wenn es blinkt, ist er im pairing modus.
Auf der Platine ist ein Loch zu sehen in der nähe vom USB Port.
Dort mit ein Stift 5 Sekunden drücken. Dann fängt es an zu blinken, für den Pairing Modus
2.
Javascript Adapter:
Kommando "exec" erlauben ein Häkchen setzen in der Adaptereinstellung
including.js erstellen durch javascript:
Das script erstellen und starten.
Dadurch wird in /opt/iobroker/iobroker-data/zigbee_0/ eine including.js Datei erstellt, was benötigt wird, das der Adapter das Gerät findet.
Falls nötig und der Adapter nicht standardmäßig installiert wurde ganz unten den Pfad anpassen:
fs.writeFile('/opt/iobroker/iobroker-data/zigbee_0/including.js', inhalt, (err) => {
- const fs = require('fs');
- const inhalt = `"use strict";
- // fzLD2450 direkt im File definieren → kein require nötig!
- const fzLD2450 = {
- cluster: 'genAnalogInput',
- type: ['attributeReport', 'readResponse'],
- convert: (model, msg) => {
- const ep = msg.endpoint.ID;
- const val = msg.data.presentValue;
- if (ep === 25) return { slot1_brightness: Math.round(val * 1000) / 1000 }; // z. B. 123.455 lx
- if (ep === 26) return { slot1_temperature: Math.round(val * 10) / 10 }; // z. B. 24.6 °C
- if (ep === 27) return { slot1_pressure: Math.round(val * 10) / 10 }; // z. B. 1018.3 hPa
- switch (ep) {
- case 1: return { slot1_distance: val };
- case 6: return { slot1_x: val };
- case 7: return { slot1_y: val };
- case 9: return { slot2_distance: val };
- case 14: return { slot2_x: val };
- case 15: return { slot2_y: val };
- case 17: return { slot3_distance: val };
- case 22: return { slot3_x: val };
- case 23: return { slot3_y: val };
- }
- },
- };
- // exposes helper → kompatibel zu e.numeric() → jetzt richtig!
- function numeric(property, access, unit, min, max) {
- return {
- type: 'numeric',
- property: property,
- access: access,
- unit: unit,
- value_min: min,
- value_max: max,
- };
- }
- // === FINALER EXPORT ===
- module.exports = [{
- zigbeeModel: ['LD2450'],
- fingerprint: [{ manufacturerName: 'erforscht', modelID: 'LD2450' }],
- model: 'LD2450_erforscht',
- vendor: 'erforscht',
- description: 'LD2450 12 endpoints',
- meta: { multiEndpoint: true },
- configure: async (device, coordinator) => {
- const used = [1,6,7,9,14,15,17,22,23,25,26,27];
- for (const ep of used) {
- const endpoint = device.getEndpoint(ep);
- if (!endpoint) continue;
- try {
- await endpoint.bind('genAnalogInput', coordinator);
- } catch (_) {}
- await endpoint.configureReporting('genAnalogInput', [{
- attribute: 0x0055,
- minimumReportInterval: 5,
- maximumReportInterval: 3600,
- reportableChange: 0.01,
- }]);
- }
- },
- fromZigbee: [fzLD2450],
- toZigbee: [],
- exposes: [
- numeric('slot1_distance', 1, 'm', 0, 6),
- numeric('slot1_x', 1, 'm', -6, 6),
- numeric('slot1_y', 1, 'm', -6, 6),
- numeric('slot1_brightness', 1, 'lx', 0, 100000),
- numeric('slot1_temperature', 1, '°C', -40, 85),
- numeric('slot1_pressure', 1, 'hPa', 300, 1100),
- numeric('slot2_distance', 1, 'm', 0, 6),
- numeric('slot2_x', 1, 'm', -6, 6),
- numeric('slot2_y', 1, 'm', -6, 6),
- numeric('slot3_distance', 1, 'm', 0, 6),
- numeric('slot3_x', 1, 'm', -6, 6),
- numeric('slot3_y', 1, 'm', -6, 6),
- ],
- }];
- `;
- fs.writeFile('/opt/iobroker/iobroker-data/zigbee_0/including.js', inhalt, (err) => {
- if (err) {
- console.error(`Fehler beim Schreiben der Datei: ${err.message}`);
- } else {
- console.log('including.js wurde erfolgreich erstellt / überschrieben.');
- }
- });
3.
Im Zigbee Adapter including.js eintippen und den Adapter neu starten.
4.
Im Zigbee Adapter das Gerät anlernen.
Danach am Gerät im Zigbee Adapter auf den Namen klicken und dann auf den "reconfigure" Button.
Dadurch werden die Werte dauerhaft übermittelt.
5.
Für jeden Bewegungsmelder dieses Javasricpt erstellen im Javascript Adapter.
Im Script die Zonen u.s.w. einrichten und das script starten.
Dadurch werden Datenpunkte erstellt im Javascript Adapter selber.
- /****************************************
- * VIS-Punkt im Raum
- * Autor: ErForscht
- *
- * Raum:
- * Es zählt immer die VIS Ansicht vom "Raum".
- * raumX = die Breite in cm vom Raum
- * raumY = die länge in cm vom Raum
- *
- * Sensor:
- * Sensor Standort:
- * sensorX = von links nach rechts in cm gemessen, wo der Sensor sich befindet.
- * sensorY = von unteren nach oben in cm gemessen, wo der Sensor sich befindet.
- *
- * Sensor Ausrichtung:
- * sensorZ = 0-359
- * sensorZ= Der Raum ist eine Uhr. also oben 12 Uhr. Rechts 3uhr u.s.w. wo der Sensor "hinguckt"
- * Wenn der Sensor: sensorZ: Er guckt von:
- * Untere Wand 0 6 → 12 Uhr
- * Rechte Wand 270 3 → 9 Uhr
- * Obere Wand 180 12 → 6 Uhr
- * Linke Wand 90 9 → 3 Uhr
- *
- * Zonen:
- * name= Name der Zone
- * color= Farbe der Zone. Es funktionieren alle Arten von Formeln
- * color:'orange' oder '#FF5733' oder 'rgb(255,87,51)' oder 'hsl(9,100%,60%)' oder'var(--my-accent-color)'
- ** Um Fehler zu minimieren, ca. 10cm von Raumwänden Abstand lassen, wenn direkt von Wand eine Zone beginnen soll.
- * x1= von links in cm gemessen bis Anfang des Bereiches
- * x2= von links in cm gemessen bis Ende des Bereiches
- * y1= von unten in cm gemessen bis Anfang des Bereiches
- * y2= von unten in cm gemessen bis Ende des Bereiches
- *
- * ROOM = Datenordner Name.
- * DEVICE = Datenpunkt Pfad zum Zigbee Sensor.
- * PREFIX = Datenordner Pfad.
- ****************************************/
- // -------- Einstellungen bitte anpassen--------
- const ROOM = 'arbeitszimmer'; // ① ioBroker-Ordner
- const DEVICE = 'zigbee.0.588c81fffe36c36c'; // ② Zigbee-Geräte-ID des BWM
- const PREFIX = `javascript.0.${ROOM}.`; // ② nicht ändern
- // ---- Vorgabewerte für Kalibrierung ----
- const calibrationDefaults = {
- raumX: 200, // ① Raum breite z.B. 200 cm
- raumY: 400, // ① Raum länge z.B. 400 cm
- sensorX: 100, // ① Sensor Position von links in cm z.B. 100 cm
- sensorY: 400, // ① Sensor Position von unten in cm z.B. 400 cm
- sensorZ: 180 // ① z.B. 180° wenn er oben wäre und nach unten überwachen soll
- };
- const debounceMs = 3000; // ① Verzögerung bis Zone false wird (ms)
- const EMPTY_THRESHOLD_M = 0.01; // ② Max. Abstand (1 cm) für leere Slots
- const zones = [
- { name:'schreibtisch', color:'orange', x1:10, x2:188, y1:210, y2:359 },
- { name:'vorne links', color:'cyan', x1:10, x2:130, y1:10, y2:210 },
- { name:'vorne rechts', color:'green', x1:131, x2:188, y1:10, y2:210 },
- ];
- // -------- State-Helper --------
- function ensure(id, def, opt){ if(!existsState(id)) createState(id, def, opt); }
- function setAckChanged(id, val){
- const o = getState(id);
- if(!o || o.val !== val) setState(id, val, true);
- }
- // Haupt-HTML-State
- ensure(PREFIX + 'widgetHtml', '', {type:'string', read:true, write:false});
- // Zonen-Boolean
- zones.forEach(z => ensure(PREFIX + 'zone.' + z.name, false,
- {type:'boolean', read:true, write:false}));
- // Kalibrierung
- ['raumX','raumY','sensorX','sensorY','sensorZ'].forEach((k) =>
- ensure(PREFIX + 'calibration.' + k,
- calibrationDefaults[k],
- {type:'number', read:true, write:true}));
- // Slot-Outputs
- ['1','2','3'].forEach(n => {
- ensure(PREFIX + `calibration.left${n}`, -100, {type:'number', read:true, write:false});
- ensure(PREFIX + `calibration.top${n}`, -100, {type:'number', read:true, write:false});
- });
- // -------- Slot-Rohdaten (DEVICE-basiert) --------
- const slots = ['1','2','3'].map((n,i) => ({
- x: `${DEVICE}.slot${n}_x`,
- y: `${DEVICE}.slot${n}_y`,
- l: PREFIX + `calibration.left${n}`,
- t: PREFIX + `calibration.top${n}`,
- color: ['red','blue','green'][i]
- }));
- // -------- Utils --------
- const rad = d => d * Math.PI / 180;
- const val = id => { const o = getState(id); return o ? Number(o.val) : null; };
- const hide = p => { setAckChanged(p.l, -100); setAckChanged(p.t, -100); };
- const offTimers = {};
- const zoneBool = (name,v) => setAckChanged(PREFIX + 'zone.' + name, v);
- const isEmptySlot = (x,y) => Math.abs(x) <= EMPTY_THRESHOLD_M && Math.abs(y) <= EMPTY_THRESHOLD_M;
- // -------- Haupt-Update --------
- function update(){
- const roomX = val(PREFIX + 'calibration.raumX') || 1;
- const roomY = val(PREFIX + 'calibration.raumY') || 1;
- const sx = val(PREFIX + 'calibration.sensorX') || 0;
- const sy = val(PREFIX + 'calibration.sensorY') || 0;
- const sz = rad(val(PREFIX + 'calibration.sensorZ') || 0);
- const occ = Object.fromEntries(zones.map(z => [z.name, false]));
- slots.forEach(p => {
- if(!existsState(p.x) || !existsState(p.y)){ hide(p); return; }
- const rx = val(p.x), ry = val(p.y);
- if(rx === null || ry === null || isEmptySlot(rx,ry)){ hide(p); return; }
- // Rohdaten → cm
- const rawX = rx * 100, rawY = ry * 100;
- // Rotation auf Raum-Koordinaten
- const relX = rawX * Math.cos(sz) - rawY * Math.sin(sz);
- const relY = rawX * Math.sin(sz) + rawY * Math.cos(sz);
- // Weltkoordinaten
- const wX = sx + relX;
- const wY = sy + relY;
- // Begrenzen & VIS-Position
- const cX = Math.max(0, Math.min(wX, roomX));
- const cY = Math.max(0, Math.min(wY, roomY));
- setAckChanged(p.l, +(cX / roomX * 100).toFixed(1));
- setAckChanged(p.t, +((1 - cY / roomY) * 100).toFixed(1));
- // Zonen-Check
- zones.forEach(z => {
- if(wX >= z.x1 && wX <= z.x2 && wY >= z.y1 && wY <= z.y2) occ[z.name] = true;
- });
- });
- // Boolean-Logik mit Timeout
- zones.forEach(z => {
- const cur = getState(PREFIX + 'zone.' + z.name).val;
- if(occ[z.name]){
- if(!cur) zoneBool(z.name, true);
- if(offTimers[z.name]){ clearTimeout(offTimers[z.name]); offTimers[z.name] = null; }
- }else{
- if(cur && !offTimers[z.name]){
- offTimers[z.name] = setTimeout(() => {
- zoneBool(z.name, false);
- offTimers[z.name] = null;
- }, debounceMs);
- }
- }
- });
- buildHtml(roomX, roomY);
- }
- // -------- HTML bauen --------
- function buildHtml(roomX, roomY){
- const dots = slots.map(p => {
- const l = getState(p.l).val, t = getState(p.t).val;
- return `<div class="dot" style="background:${p.color};left:${l}%;top:${t}%"></div>`;
- }).join('');
- const rects = zones.map(z => {
- const l = ((z.x1 / roomX) * 100).toFixed(1);
- const t = ((1 - z.y2 / roomY) * 100).toFixed(1);
- const w = (((z.x2 - z.x1) / roomX) * 100).toFixed(1);
- const h = (((z.y2 - z.y1) / roomY) * 100).toFixed(1);
- return `<div class="zone" style="border-color:${z.color};background:${z.color};
- left:${l}%;top:${t}%;width:${w}%;height:${h}%"><span>${z.name}</span></div>`;
- }).join('');
- const html = `
- <style>
- .roomWrap{position:relative;width:100%;height:100%;border:2px solid #000;background:#eee;overflow:hidden}
- .dot{position:absolute;width:16px;height:16px;border-radius:50%;transform:translate(-50%,-50%)}
- .zone{position:absolute;pointer-events:none;border:2px dashed;opacity:.15}
- .zone span{position:absolute;left:2px;top:2px;font-size:12px;color:#000}
- </style>
- <div class="roomWrap">${dots}${rects}</div>`;
- setAckChanged(PREFIX + 'widgetHtml', html);
- }
- // -------- Trigger --------
- on([
- PREFIX + 'calibration.raumX',
- PREFIX + 'calibration.raumY',
- PREFIX + 'calibration.sensorX',
- PREFIX + 'calibration.sensorY',
- PREFIX + 'calibration.sensorZ'
- ], update);
- on({
- id: new RegExp('^' + DEVICE.replace('.', '\\.') + '\\.slot[1-3]_[xy]$'),
- change: 'any',
- debounce: 200
- }, update);
- // Initialer Aufruf
- update();
6.
Alle Datenpunkte, die erstellt wurden durch das Javascipt, mit den richtigen Daten bearbeiten/füllen.
Wie man das macht, steht am Anfang von Javascript.
7.
VIS html Widget Code erstellen und Datenpunktverknüpfung javascript.0.arbeitszimmer.widgetHtml anpassen:
1= HTML Widget
2= Hier den Quellcode reinkopieren
3= Raumbreite 1 px = 1cm
4= Raumlänge 1 px = 1 cm
8. Fertig