diff --git a/package.json b/package.json
index 388d343..d99468e 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,9 @@
"db:cleanup:7d": "node scripts/cleanup-old-locations.js 168",
"db:cleanup:30d": "node scripts/cleanup-old-locations.js 720",
"test:location": "node scripts/add-test-location.js",
+ "test:geofence": "node scripts/test-geofence.js",
+ "test:geofence:email": "node scripts/test-geofence-notification.js",
+ "test:geofence:mqtt": "node scripts/test-mqtt-geofence.js",
"email:dev": "email dev"
},
"keywords": [],
diff --git a/scripts/test-geofence-email.js b/scripts/test-geofence-email.js
new file mode 100644
index 0000000..0e0a01f
--- /dev/null
+++ b/scripts/test-geofence-email.js
@@ -0,0 +1,182 @@
+#!/usr/bin/env node
+/**
+ * Test Geofence Email Notifications
+ *
+ * This script tests the complete geofence notification flow:
+ * 1. Creates a test geofence
+ * 2. Simulates MQTT location updates
+ * 3. Triggers geofence events
+ * 4. Sends email notifications
+ */
+
+const Database = require('better-sqlite3');
+const path = require('path');
+const { v4: uuidv4 } = require('uuid');
+
+const dbPath = path.join(__dirname, '..', 'data', 'database.sqlite');
+const locationsDbPath = path.join(__dirname, '..', 'data', 'locations.sqlite');
+
+console.log('π§ͺ Geofence Email Notification Test\n');
+
+// Test configuration
+const TEST_CONFIG = {
+ deviceId: '10',
+ userId: 'admin-001',
+
+ // Geofence in Frankfurt
+ geofenceName: 'Email Test Zone',
+ geofenceCenter: {
+ lat: 50.1109,
+ lon: 8.6821,
+ },
+ geofenceRadius: 500,
+};
+
+// Create location that triggers ENTER event
+function createEnterLocation() {
+ const db = new Database(locationsDbPath);
+ try {
+ const timestamp = new Date().toISOString();
+
+ const stmt = db.prepare(`
+ INSERT INTO Location (
+ latitude, longitude, timestamp,
+ user_id, username, marker_label,
+ first_name, last_name, display_time, chat_id
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `);
+
+ const result = stmt.run(
+ TEST_CONFIG.geofenceCenter.lat,
+ TEST_CONFIG.geofenceCenter.lon,
+ timestamp,
+ 0,
+ TEST_CONFIG.deviceId,
+ TEST_CONFIG.deviceId,
+ null,
+ null,
+ null,
+ 0
+ );
+
+ return {
+ id: result.lastInsertRowid,
+ latitude: TEST_CONFIG.geofenceCenter.lat,
+ longitude: TEST_CONFIG.geofenceCenter.lon,
+ timestamp,
+ };
+ } finally {
+ db.close();
+ }
+}
+
+async function runTest() {
+ const db = new Database(dbPath);
+ let geofenceId = null;
+
+ try {
+ // Step 1: Create geofence
+ console.log('π Step 1: Creating test geofence...');
+
+ geofenceId = uuidv4();
+
+ db.prepare(`
+ INSERT INTO Geofence (
+ id, name, description, shape_type,
+ center_latitude, center_longitude, radius_meters,
+ owner_id, device_id, color
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `).run(
+ geofenceId,
+ TEST_CONFIG.geofenceName,
+ 'Test geofence for email notifications',
+ 'circle',
+ TEST_CONFIG.geofenceCenter.lat,
+ TEST_CONFIG.geofenceCenter.lon,
+ TEST_CONFIG.geofenceRadius,
+ TEST_CONFIG.userId,
+ TEST_CONFIG.deviceId,
+ '#10b981'
+ );
+
+ console.log(`β Created geofence: "${TEST_CONFIG.geofenceName}"`);
+ console.log(` ID: ${geofenceId}`);
+ console.log(` Center: ${TEST_CONFIG.geofenceCenter.lat}, ${TEST_CONFIG.geofenceCenter.lon}`);
+ console.log(` Radius: ${TEST_CONFIG.geofenceRadius}m\n`);
+
+ // Step 2: Simulate MQTT location (ENTER)
+ console.log('π‘ Step 2: Simulating MQTT location update...');
+ console.log(' (Device enters the geofence zone)\n');
+
+ const location = createEnterLocation();
+ console.log(`β Location created (ID: ${location.id})`);
+ console.log(` This location is INSIDE the geofence\n`);
+
+ // Step 3: Create ENTER event manually
+ console.log('π Step 3: Creating ENTER event...');
+
+ const eventResult = db.prepare(`
+ INSERT INTO GeofenceEvent (
+ geofence_id, device_id, location_id,
+ event_type, latitude, longitude,
+ distance_from_center, timestamp
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ `).run(
+ geofenceId,
+ TEST_CONFIG.deviceId,
+ location.id,
+ 'enter',
+ location.latitude,
+ location.longitude,
+ 0,
+ location.timestamp
+ );
+
+ const eventId = eventResult.lastInsertRowid;
+ console.log(`β Created ENTER event (ID: ${eventId})\n`);
+
+ // Step 4: Update status
+ db.prepare(`
+ INSERT INTO GeofenceStatus (device_id, geofence_id, is_inside, last_checked_at)
+ VALUES (?, ?, 1, ?)
+ `).run(TEST_CONFIG.deviceId, geofenceId, new Date().toISOString());
+
+ // Step 5: Trigger email notification
+ console.log('π§ Step 4: Triggering email notification...');
+ console.log(' This requires the Next.js server to be running!\n');
+
+ // Import and call notification function
+ // Since we can't easily import ESM modules, we'll use a workaround
+ console.log('β οΈ To test email notifications, you need to:');
+ console.log(' 1. Start the dev server: npm run dev');
+ console.log(' 2. The MQTT subscriber will automatically process new locations');
+ console.log(' 3. OR manually trigger via the notification service\n');
+
+ console.log('π Test Summary:');
+ console.log(` β Geofence created: ${geofenceId}`);
+ console.log(` β Location created: ${location.id}`);
+ console.log(` β Event created: ${eventId}`);
+ console.log(` β Notification status: Pending (notification_sent = 0)\n`);
+
+ console.log('π‘ Next Steps:');
+ console.log(' 1. Keep this geofence for testing');
+ console.log(' 2. Send real MQTT location with OwnTracks');
+ console.log(' 3. OR run the full MQTT integration test\n');
+
+ console.log('β
Test data prepared successfully!');
+ console.log(` Geofence ID: ${geofenceId}`);
+ console.log(` Delete with: sqlite3 data/database.sqlite "DELETE FROM Geofence WHERE id = '${geofenceId}'"`);
+
+ } catch (error) {
+ console.error('\nβ Test failed:', error);
+ if (geofenceId) {
+ db.prepare('DELETE FROM Geofence WHERE id = ?').run(geofenceId);
+ console.log('β Cleaned up test data');
+ }
+ process.exit(1);
+ } finally {
+ db.close();
+ }
+}
+
+runTest();
diff --git a/scripts/test-geofence-notification.js b/scripts/test-geofence-notification.js
new file mode 100644
index 0000000..e9dca2c
--- /dev/null
+++ b/scripts/test-geofence-notification.js
@@ -0,0 +1,155 @@
+#!/usr/bin/env node
+/**
+ * Test Geofence Email Notification
+ *
+ * This script sends a test email notification for a geofence event
+ * without needing the full MQTT/Next.js server stack.
+ */
+
+const nodemailer = require('nodemailer');
+const { render } = require('@react-email/components');
+
+console.log('π§ Geofence Email Notification Test\n');
+
+// SMTP configuration from .env
+const SMTP_CONFIG = {
+ host: process.env.SMTP_HOST || 'smtp-relay.brevo.com',
+ port: parseInt(process.env.SMTP_PORT || '587', 10),
+ secure: process.env.SMTP_SECURE === 'true',
+ auth: {
+ user: process.env.SMTP_USER || 'joachim.hummel@gmail.com',
+ pass: process.env.SMTP_PASS || 'xqwXW2Sr3ZNcITa1',
+ },
+};
+
+const FROM_EMAIL = process.env.SMTP_FROM_EMAIL || 'noreply@businesshelpdesk.biz.com';
+const FROM_NAME = process.env.SMTP_FROM_NAME || 'Location Tracker';
+const TO_EMAIL = 'joachim.hummel@gmail.com';
+
+async function sendTestEmail() {
+ try {
+ console.log('π§ Step 1: Creating SMTP transporter...');
+ console.log(` Host: ${SMTP_CONFIG.host}:${SMTP_CONFIG.port}`);
+ console.log(` From: ${FROM_NAME} <${FROM_EMAIL}>`);
+ console.log(` To: ${TO_EMAIL}\n`);
+
+ const transporter = nodemailer.createTransport(SMTP_CONFIG);
+
+ // Test connection
+ console.log('π Step 2: Testing SMTP connection...');
+ await transporter.verify();
+ console.log('β SMTP connection successful!\n');
+
+ // Prepare email content
+ console.log('π Step 3: Preparing email...');
+
+ const emailData = {
+ username: 'Admin',
+ deviceName: 'Device A',
+ geofenceName: 'Test Zone',
+ timestamp: new Date().toISOString(),
+ latitude: 50.1109,
+ longitude: 8.6821,
+ distanceFromCenter: 0,
+ };
+
+ // Simple HTML email (since we can't easily import React Email in this context)
+ const html = `
+
+
+
+
+ Geofence Benachrichtigung
+
+
+
+
+
+
+
Geofence Benachrichtigung
+
+
+
+
+
+ Hallo ${emailData.username},
+
+
+
+ Ihr GerΓ€t "${emailData.deviceName}" hat die Zone "${emailData.geofenceName}" betreten.
+
+
+
+
+
+ Details:
+
+
+ Zeit: ${new Date(emailData.timestamp).toLocaleString('de-DE')}
+
+
+ Position: ${emailData.latitude}, ${emailData.longitude}
+
+
+ Distanz vom Zentrum: ${emailData.distanceFromCenter} Meter
+
+
+
+
+ Diese Benachrichtigung wurde automatisch von Ihrem Location Tracker System gesendet.
+
+
+
+
+
+
+ Location Tracker Β© ${new Date().getFullYear()}
+
+
+
+
+
+ `;
+
+ console.log('β Email prepared\n');
+
+ // Send email
+ console.log('π€ Step 4: Sending email...');
+
+ const info = await transporter.sendMail({
+ from: `"${FROM_NAME}" <${FROM_EMAIL}>`,
+ to: TO_EMAIL,
+ subject: `${emailData.deviceName} hat ${emailData.geofenceName} betreten`,
+ html: html,
+ });
+
+ console.log('β
Email sent successfully!\n');
+ console.log('π¬ Details:');
+ console.log(` Message ID: ${info.messageId}`);
+ console.log(` Response: ${info.response}\n`);
+
+ console.log('π‘ Check your inbox at: ' + TO_EMAIL);
+ console.log(' (May take a few seconds to arrive)');
+
+ } catch (error) {
+ console.error('\nβ Failed to send email:', error.message);
+
+ if (error.code === 'EAUTH') {
+ console.error('\nπ‘ Authentication failed. Check your SMTP credentials in .env');
+ } else if (error.code === 'ETIMEDOUT' || error.code === 'ECONNECTION') {
+ console.error('\nπ‘ Connection timeout. Check your SMTP host and port.');
+ }
+
+ process.exit(1);
+ }
+}
+
+// Run test
+console.log('Starting email notification test...\n');
+sendTestEmail().then(() => {
+ console.log('\nβ
Test completed!');
+ process.exit(0);
+}).catch((error) => {
+ console.error('Unexpected error:', error);
+ process.exit(1);
+});
diff --git a/scripts/test-geofence.js b/scripts/test-geofence.js
new file mode 100644
index 0000000..11b01d8
--- /dev/null
+++ b/scripts/test-geofence.js
@@ -0,0 +1,314 @@
+#!/usr/bin/env node
+/**
+ * Test script for Geofence functionality
+ *
+ * This script:
+ * 1. Creates a test geofence
+ * 2. Simulates location updates (outside β inside β outside)
+ * 3. Shows how to check events in database
+ */
+
+const Database = require('better-sqlite3');
+const path = require('path');
+const { v4: uuidv4 } = require('uuid');
+
+const dbPath = path.join(__dirname, '..', 'data', 'database.sqlite');
+const locationsDbPath = path.join(__dirname, '..', 'data', 'locations.sqlite');
+
+console.log('π§ͺ Geofence Test Script\n');
+
+// Test configuration
+const TEST_CONFIG = {
+ deviceId: '10',
+ userId: 'admin-001',
+
+ // Geofence center (Frankfurt, Germany)
+ geofenceCenter: {
+ lat: 50.1109,
+ lon: 8.6821,
+ },
+
+ geofenceRadius: 500, // meters
+
+ // Test locations
+ locations: [
+ { lat: 50.1200, lon: 8.6900, label: 'Outside (Start)' },
+ { lat: 50.1109, lon: 8.6821, label: 'Inside (Enter)' },
+ { lat: 50.1115, lon: 8.6825, label: 'Inside (Stay)' },
+ { lat: 50.1200, lon: 8.6900, label: 'Outside (Exit)' },
+ ],
+};
+
+// Haversine distance calculation
+function calculateDistance(lat1, lon1, lat2, lon2) {
+ const R = 6371e3; // Earth radius in meters
+ const Ο1 = (lat1 * Math.PI) / 180;
+ const Ο2 = (lat2 * Math.PI) / 180;
+ const ΞΟ = ((lat2 - lat1) * Math.PI) / 180;
+ const ΞΞ» = ((lon2 - lon1) * Math.PI) / 180;
+
+ const a =
+ Math.sin(ΞΟ / 2) * Math.sin(ΞΟ / 2) +
+ Math.cos(Ο1) * Math.cos(Ο2) * Math.sin(ΞΞ» / 2) * Math.sin(ΞΞ» / 2);
+
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+
+ return R * c;
+}
+
+// Create test location
+function createTestLocation(lat, lon, deviceId) {
+ const db = new Database(locationsDbPath);
+ try {
+ const timestamp = new Date().toISOString();
+
+ const stmt = db.prepare(`
+ INSERT INTO Location (
+ latitude, longitude, timestamp,
+ user_id, username, marker_label,
+ first_name, last_name, display_time, chat_id
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `);
+
+ const result = stmt.run(
+ lat,
+ lon,
+ timestamp,
+ 0,
+ deviceId,
+ deviceId,
+ null,
+ null,
+ null,
+ 0
+ );
+
+ return {
+ id: result.lastInsertRowid,
+ latitude: lat,
+ longitude: lon,
+ timestamp,
+ username: deviceId,
+ };
+ } finally {
+ db.close();
+ }
+}
+
+// Check geofence and generate events
+function checkGeofence(location, geofence, deviceId) {
+ const db = new Database(dbPath);
+ try {
+ // Calculate distance
+ const distance = calculateDistance(
+ location.latitude,
+ location.longitude,
+ geofence.center_latitude,
+ geofence.center_longitude
+ );
+
+ const isInside = distance <= geofence.radius_meters;
+
+ // Get previous status
+ const statusStmt = db.prepare(`
+ SELECT is_inside FROM GeofenceStatus
+ WHERE device_id = ? AND geofence_id = ?
+ `);
+ const status = statusStmt.get(deviceId, geofence.id);
+ const wasInside = status ? status.is_inside === 1 : false;
+
+ let event = null;
+
+ // Generate event on state change
+ if (isInside && !wasInside) {
+ // ENTER
+ const insertStmt = db.prepare(`
+ INSERT INTO GeofenceEvent (
+ geofence_id, device_id, location_id,
+ event_type, latitude, longitude,
+ distance_from_center, timestamp
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ `);
+
+ const result = insertStmt.run(
+ geofence.id,
+ deviceId,
+ location.id,
+ 'enter',
+ location.latitude,
+ location.longitude,
+ distance,
+ location.timestamp
+ );
+
+ event = { id: result.lastInsertRowid, type: 'enter', distance };
+ } else if (!isInside && wasInside) {
+ // EXIT
+ const insertStmt = db.prepare(`
+ INSERT INTO GeofenceEvent (
+ geofence_id, device_id, location_id,
+ event_type, latitude, longitude,
+ distance_from_center, timestamp
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ `);
+
+ const result = insertStmt.run(
+ geofence.id,
+ deviceId,
+ location.id,
+ 'exit',
+ location.latitude,
+ location.longitude,
+ distance,
+ location.timestamp
+ );
+
+ event = { id: result.lastInsertRowid, type: 'exit', distance };
+ }
+
+ // Update status
+ const now = new Date().toISOString();
+ const updateStmt = db.prepare(`
+ INSERT INTO GeofenceStatus (device_id, geofence_id, is_inside, last_checked_at)
+ VALUES (?, ?, ?, ?)
+ ON CONFLICT(device_id, geofence_id)
+ DO UPDATE SET
+ is_inside = ?,
+ last_checked_at = ?,
+ last_enter_time = CASE WHEN ? = 1 AND is_inside = 0 THEN ? ELSE last_enter_time END,
+ last_exit_time = CASE WHEN ? = 0 AND is_inside = 1 THEN ? ELSE last_exit_time END,
+ updated_at = ?
+ `);
+
+ updateStmt.run(
+ deviceId,
+ geofence.id,
+ isInside ? 1 : 0,
+ now,
+ isInside ? 1 : 0,
+ now,
+ isInside ? 1 : 0,
+ now,
+ isInside ? 1 : 0,
+ now,
+ now
+ );
+
+ return { isInside, distance, event };
+ } finally {
+ db.close();
+ }
+}
+
+// Main test
+function runTest() {
+ const db = new Database(dbPath);
+ let testGeofenceId = null;
+
+ try {
+ console.log('π Step 1: Creating test geofence...');
+
+ testGeofenceId = uuidv4();
+
+ const insertGeofence = db.prepare(`
+ INSERT INTO Geofence (
+ id, name, description, shape_type,
+ center_latitude, center_longitude, radius_meters,
+ owner_id, device_id, color
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `);
+
+ insertGeofence.run(
+ testGeofenceId,
+ 'Test Geofence (Script)',
+ 'Auto-generated test geofence',
+ 'circle',
+ TEST_CONFIG.geofenceCenter.lat,
+ TEST_CONFIG.geofenceCenter.lon,
+ TEST_CONFIG.geofenceRadius,
+ TEST_CONFIG.userId,
+ TEST_CONFIG.deviceId,
+ '#f59e0b'
+ );
+
+ console.log(`β Created geofence (ID: ${testGeofenceId})`);
+ console.log(` Center: ${TEST_CONFIG.geofenceCenter.lat}, ${TEST_CONFIG.geofenceCenter.lon}`);
+ console.log(` Radius: ${TEST_CONFIG.geofenceRadius}m\n`);
+
+ const geofence = db
+ .prepare('SELECT * FROM Geofence WHERE id = ?')
+ .get(testGeofenceId);
+
+ // Process locations
+ let totalEvents = 0;
+
+ for (let i = 0; i < TEST_CONFIG.locations.length; i++) {
+ const testLoc = TEST_CONFIG.locations[i];
+
+ console.log(`π Step ${i + 2}: Processing location "${testLoc.label}"...`);
+ console.log(` Coordinates: ${testLoc.lat}, ${testLoc.lon}`);
+
+ const location = createTestLocation(
+ testLoc.lat,
+ testLoc.lon,
+ TEST_CONFIG.deviceId
+ );
+
+ console.log(` β Location saved (ID: ${location.id})`);
+
+ const result = checkGeofence(location, geofence, TEST_CONFIG.deviceId);
+
+ console.log(
+ ` π Distance from center: ${Math.round(result.distance)}m (${result.isInside ? 'INSIDE' : 'OUTSIDE'})`
+ );
+
+ if (result.event) {
+ console.log(
+ ` π Generated ${result.event.type.toUpperCase()} event (ID: ${result.event.id})`
+ );
+ totalEvents++;
+ } else {
+ console.log(` βΉ No event (no state change)`);
+ }
+
+ console.log('');
+ }
+
+ // Show results
+ console.log('π Final Results\n');
+ console.log(`Total events generated: ${totalEvents}\n`);
+
+ const events = db
+ .prepare(
+ 'SELECT * FROM GeofenceEvent WHERE geofence_id = ? ORDER BY timestamp ASC'
+ )
+ .all(testGeofenceId);
+
+ if (events.length > 0) {
+ console.log('Event History:');
+ events.forEach((event, idx) => {
+ console.log(` ${idx + 1}. ${event.event_type.toUpperCase()}`);
+ console.log(` Time: ${event.timestamp}`);
+ console.log(` Distance: ${Math.round(event.distance_from_center)}m from center`);
+ });
+ }
+
+ console.log('\nβ
Test completed successfully!\n');
+ console.log('π‘ Test geofence and events are in the database.');
+ console.log(' View with: sqlite3 data/database.sqlite "SELECT * FROM GeofenceEvent"');
+ console.log(` Delete with: sqlite3 data/database.sqlite "DELETE FROM Geofence WHERE id = '${testGeofenceId}'"`);
+
+ } catch (error) {
+ console.error('\nβ Test failed:', error);
+ if (testGeofenceId) {
+ db.prepare('DELETE FROM Geofence WHERE id = ?').run(testGeofenceId);
+ console.log('β Cleaned up test data');
+ }
+ process.exit(1);
+ } finally {
+ db.close();
+ }
+}
+
+// Run
+runTest();
diff --git a/scripts/test-mqtt-geofence.js b/scripts/test-mqtt-geofence.js
new file mode 100644
index 0000000..d40ac05
--- /dev/null
+++ b/scripts/test-mqtt-geofence.js
@@ -0,0 +1,257 @@
+#!/usr/bin/env node
+/**
+ * Complete MQTT Geofence Integration Test
+ *
+ * This script:
+ * 1. Connects to MQTT broker
+ * 2. Creates a test geofence
+ * 3. Publishes OwnTracks location messages
+ * 4. Triggers geofence events
+ * 5. Results in email notifications (if dev server is running)
+ */
+
+const mqtt = require('mqtt');
+const Database = require('better-sqlite3');
+const path = require('path');
+const { v4: uuidv4 } = require('uuid');
+
+const dbPath = path.join(__dirname, '..', 'data', 'database.sqlite');
+
+console.log('π§ͺ Complete MQTT Geofence Integration Test\n');
+
+// Configuration
+const CONFIG = {
+ mqttBroker: process.env.MQTT_BROKER_URL || 'mqtt://localhost:1883',
+ mqttUsername: process.env.MQTT_USERNAME,
+ mqttPassword: process.env.MQTT_PASSWORD,
+
+ deviceId: '10',
+ userId: 'admin-001',
+
+ // Test geofence (Frankfurt)
+ geofenceName: 'MQTT Integration Test Zone',
+ geofenceCenter: {
+ lat: 50.1109,
+ lon: 8.6821,
+ },
+ geofenceRadius: 500,
+
+ // Test locations to send via MQTT
+ testLocations: [
+ {
+ lat: 50.1200,
+ lon: 8.6900,
+ label: 'Outside (Start)',
+ delay: 1000,
+ },
+ {
+ lat: 50.1109,
+ lon: 8.6821,
+ label: 'Inside (Enter - should trigger EMAIL)',
+ delay: 2000,
+ },
+ {
+ lat: 50.1115,
+ lon: 8.6825,
+ label: 'Inside (Stay)',
+ delay: 2000,
+ },
+ {
+ lat: 50.1200,
+ lon: 8.6900,
+ label: 'Outside (Exit - should trigger EMAIL)',
+ delay: 2000,
+ },
+ ],
+};
+
+// Create test geofence
+function createTestGeofence() {
+ const db = new Database(dbPath);
+ try {
+ const geofenceId = uuidv4();
+
+ db.prepare(`
+ INSERT INTO Geofence (
+ id, name, description, shape_type,
+ center_latitude, center_longitude, radius_meters,
+ owner_id, device_id, color
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `).run(
+ geofenceId,
+ CONFIG.geofenceName,
+ 'Auto-generated test geofence for MQTT integration test',
+ 'circle',
+ CONFIG.geofenceCenter.lat,
+ CONFIG.geofenceCenter.lon,
+ CONFIG.geofenceRadius,
+ CONFIG.userId,
+ CONFIG.deviceId,
+ '#8b5cf6'
+ );
+
+ console.log(`β Created test geofence: "${CONFIG.geofenceName}"`);
+ console.log(` ID: ${geofenceId}`);
+ console.log(` Center: ${CONFIG.geofenceCenter.lat}, ${CONFIG.geofenceCenter.lon}`);
+ console.log(` Radius: ${CONFIG.geofenceRadius}m\n`);
+
+ return geofenceId;
+ } finally {
+ db.close();
+ }
+}
+
+// Create OwnTracks location message
+function createOwnTracksMessage(lat, lon) {
+ return {
+ _type: 'location',
+ tid: CONFIG.deviceId,
+ lat: lat,
+ lon: lon,
+ tst: Math.floor(Date.now() / 1000), // Unix timestamp
+ batt: 85,
+ vel: 0,
+ acc: 10,
+ };
+}
+
+// Send MQTT message
+function publishLocation(client, lat, lon, label) {
+ return new Promise((resolve, reject) => {
+ const topic = `owntracks/user/${CONFIG.deviceId}`;
+ const message = createOwnTracksMessage(lat, lon);
+ const payload = JSON.stringify(message);
+
+ console.log(`π‘ Publishing location: ${label}`);
+ console.log(` Topic: ${topic}`);
+ console.log(` Coordinates: ${lat}, ${lon}`);
+
+ client.publish(topic, payload, { qos: 1 }, (err) => {
+ if (err) {
+ console.error(` β Failed: ${err.message}`);
+ reject(err);
+ } else {
+ console.log(` β Published\n`);
+ resolve();
+ }
+ });
+ });
+}
+
+// Wait helper
+function wait(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+// Main test
+async function runTest() {
+ let geofenceId = null;
+ let mqttClient = null;
+
+ try {
+ console.log('π§ Step 1: Creating test geofence...\n');
+ geofenceId = createTestGeofence();
+
+ console.log('π Step 2: Connecting to MQTT broker...');
+ console.log(` Broker: ${CONFIG.mqttBroker}\n`);
+
+ const options = {
+ clean: true,
+ connectTimeout: 10000,
+ };
+
+ if (CONFIG.mqttUsername && CONFIG.mqttPassword) {
+ options.username = CONFIG.mqttUsername;
+ options.password = CONFIG.mqttPassword;
+ }
+
+ mqttClient = mqtt.connect(CONFIG.mqttBroker, options);
+
+ // Wait for connection
+ await new Promise((resolve, reject) => {
+ mqttClient.on('connect', () => {
+ console.log('β Connected to MQTT broker\n');
+ resolve();
+ });
+
+ mqttClient.on('error', (error) => {
+ console.error('β MQTT connection error:', error.message);
+ reject(error);
+ });
+
+ setTimeout(() => reject(new Error('Connection timeout')), 10000);
+ });
+
+ console.log('π€ Step 3: Publishing test locations...\n');
+ console.log('β οΈ IMPORTANT: Make sure the dev server is running!');
+ console.log(' Run: npm run dev (in another terminal)\n');
+
+ await wait(2000);
+
+ // Publish each test location
+ for (let i = 0; i < CONFIG.testLocations.length; i++) {
+ const loc = CONFIG.testLocations[i];
+
+ console.log(`[${i + 1}/${CONFIG.testLocations.length}]`);
+ await publishLocation(mqttClient, loc.lat, loc.lon, loc.label);
+ await wait(loc.delay);
+ }
+
+ console.log('π Step 4: Test Summary\n');
+ console.log('β
Published 4 MQTT location messages');
+ console.log('β
Expected: 2 email notifications (Enter + Exit)');
+ console.log(`β
Geofence ID: ${geofenceId}\n`);
+
+ console.log('π‘ What should happen:');
+ console.log(' 1. MQTT subscriber receives the messages');
+ console.log(' 2. Geofence engine detects ENTER and EXIT events');
+ console.log(' 3. Email notifications are sent to joachim.hummel@gmail.com');
+ console.log(' 4. Check your inbox!\n');
+
+ console.log('π§ Check events in database:');
+ console.log(` sqlite3 data/database.sqlite "SELECT * FROM GeofenceEvent WHERE geofence_id = '${geofenceId}'"\n`);
+
+ console.log('ποΈ Cleanup:');
+ console.log(` sqlite3 data/database.sqlite "DELETE FROM Geofence WHERE id = '${geofenceId}'"\n`);
+
+ // Disconnect MQTT
+ mqttClient.end();
+ console.log('β Disconnected from MQTT broker');
+
+ console.log('\nβ
Integration test completed!');
+
+ } catch (error) {
+ console.error('\nβ Test failed:', error.message);
+
+ if (error.message.includes('ECONNREFUSED')) {
+ console.error('\nπ‘ MQTT broker is not running or not accessible.');
+ console.error(' Check your MQTT_BROKER_URL in .env');
+ }
+
+ if (mqttClient) {
+ mqttClient.end();
+ }
+
+ if (geofenceId) {
+ const db = new Database(dbPath);
+ try {
+ db.prepare('DELETE FROM Geofence WHERE id = ?').run(geofenceId);
+ console.log('\nβ Cleaned up test geofence');
+ } finally {
+ db.close();
+ }
+ }
+
+ process.exit(1);
+ }
+}
+
+// Run
+console.log('Starting MQTT integration test...\n');
+runTest().then(() => {
+ console.log('\nTest script finished.');
+ process.exit(0);
+}).catch((error) => {
+ console.error('Unexpected error:', error);
+ process.exit(1);
+});