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. 29,99€ Beginn Juni<---
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
Zusammenbau:
Der ESP32C6 wird unterhalb der Platine als erstes verlötet.(linkes Bild)
Oberhalb der Platine werden die Bauteile so verlötet, das nichts direkt neben oder über dem Radarsensors sich was befindet(linkes Bild).
Das würde den Sensor stören, da Radarwellen nicht Metall durchdringen kann.
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.
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: val };
- if (ep === 26) return { slot1_temperature: val };
- if (ep === 27) return { slot1_pressure: val };
- 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.');
- }
- });
2.
Im Zigbee Adapter including.js eintippen und den Adapter neu starten.
3.
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.
4.
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.
- ****************************************/
- const ROOM = 'arbeitszimmer'; // ① ioBroker-Ordner
- const DEVICE = 'zigbee.0.588c81fffe36c36c'; // ② Zigbee-Geräte-ID
- const PREFIX = `javascript.0.${ROOM}.`;
- // -------- Einstellungen --------
- const debounceMs = 3000;
- const zones = [
- { name:'schreibtisch', color:'orange', x1:10, x2:188, y1:210, y2:359 },
- { name:'vorne link', 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); }
- // Nur schreiben, wenn Wert sich geändert hat (ACK=true)
- 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,i) =>
- ensure(PREFIX + 'calibration.' + k,
- [200,400,100,0,0][i],
- {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 --------
- function rad(d){ return d * Math.PI / 180; }
- function val(id){ const o = getState(id); return o ? Number(o.val) : null; }
- function hide(p){ setAckChanged(p.l, -100); setAckChanged(p.t, -100); }
- const offTimers = {};
- function zoneBool(name, v){ setAckChanged(PREFIX + 'zone.' + name, v); }
- // -------- 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 = {};
- zones.forEach(z => occ[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){ hide(p); return; }
- // Rohdaten in cm
- const rawX = rx * 100;
- const rawY = ry * 100;
- // Rotation auf Koordinatensystem des Raums
- const relX = rawX * Math.cos(sz) - rawY * Math.sin(sz);
- const relY = rawX * Math.sin(sz) + rawY * Math.cos(sz);
- // Weltkoordinaten (orig. Bezug)
- const wX = sx + relX;
- const wY = sy + relY;
- // Clamping an Raumgrenzen
- const cX = Math.max(0, Math.min(wX, roomX));
- const cY = Math.max(0, Math.min(wY, roomY));
- // Prozentuale Positionen für VIS-Darstellung
- setAckChanged(p.l, +(cX / roomX * 100).toFixed(1));
- setAckChanged(p.t, +((1 - cY / roomY) * 100).toFixed(1));
- // Zonen-Belegung
- zones.forEach(z => {
- if(wX >= z.x1 && wX <= z.x2 && wY >= z.y1 && wY <= z.y2) occ[z.name] = true;
- });
- });
- // Zonen-Booleans mit Verzögerung zurücksetzen
- 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 & speichern --------
- 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 --------
- // Kalibrierung (selten geändert) – ohne Debounce
- on([
- PREFIX + 'calibration.raumX',
- PREFIX + 'calibration.raumY',
- PREFIX + 'calibration.sensorX',
- PREFIX + 'calibration.sensorY',
- PREFIX + 'calibration.sensorZ'
- ], update);
- // Radar-Slots – mit 200 ms Debounce, damit schnelle Fluten zusammengefasst werden
- on({
- id: new RegExp('^' + DEVICE.replace('.', '\\.') + '\\.slot[1-3]_[xy]$'),
- change: 'any',
- debounce: 200
- }, update);
- // Initialer Aufruf
- update();
5.
Alle Datenpunkte, die erstellt wurden durch das Javascipt, mit den richtigen Daten bearbeiten/füllen.
Wie man das macht, steht am Anfang von Javascript.
6.
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
7. Fertig