From 5d689e6005e6a5c3684973d082324e88d0e862cb Mon Sep 17 00:00:00 2001 From: Yan Zhou Date: Thu, 12 Jun 2025 18:53:02 +0200 Subject: [PATCH 01/10] add floor plan --- src/routes/(app)/[slug]/+page.server.ts | 78 ++++++++++++++------- src/routes/(app)/[slug]/+page.svelte | 69 +++++++++++++++++-- src/routes/(app)/settings/+page.server.js | 64 ++++++++++++++++- src/routes/(app)/settings/+page.svelte | 83 +++++++++++++++++++++-- src/routes/mqtt/+server.js | 1 + 5 files changed, 254 insertions(+), 41 deletions(-) diff --git a/src/routes/(app)/[slug]/+page.server.ts b/src/routes/(app)/[slug]/+page.server.ts index 4488322..68d7496 100644 --- a/src/routes/(app)/[slug]/+page.server.ts +++ b/src/routes/(app)/[slug]/+page.server.ts @@ -5,33 +5,59 @@ import type { PageServerLoad } from "./$types"; import { connect } from "mqtt"; export const load: PageServerLoad = async ({ params }) => { - { - const floor_cnt = await db.select({ floor: table.plans.floor, json: table.plans.plan }).from(table.plans).where(eq(table.plans.floor, params.slug)); - if (floor_cnt.length == 0) { - await db.insert(table.plans).values({ floor: params.slug, plan: { - "regions": [ - { "start": { "x": 100, "y": 100 }, "end": { "x": 400, "y": 100 } }, - { "start": { "x": 400, "y": 100 }, "end": { "x": 400, "y": 300 } }, - { "start": { "x": 400, "y": 300 }, "end": { "x": 100, "y": 300 } }, - { "start": { "x": 100, "y": 300 }, "end": { "x": 100, "y": 100 } } - ], - "doors": [ - { "location": { "x": 240, "y": 100 }, "width": 50, "rotation": 0 } - ], - "furnitures": [ - { - "minBound": { "x": 150, "y": 150 }, - "maxBound": { "x": 200, "y": 200 }, - "equipName": "Table", - "xPlacement": 150, - "yPlacement": 150, - "rotation": 0 - } - ] - }}); + // Convert slug to number for floor lookup + const floorNumber = Number(params.slug); + + // First check if we have a saved floor configuration in the floors table + const floorData = await db.select({ + floor: table.floors.floor, + url: table.floors.url + }).from(table.floors).where(eq(table.floors.floor, floorNumber)); + + if (floorData.length > 0 && floorData[0].url && floorData[0].url !== "/") { + try { + // Try to parse the saved configuration + const config = JSON.parse(floorData[0].url); + return { + slug: params.slug, + floorConfig: config, + hasConfig: true + }; + } catch (e) { + console.error("Error parsing floor configuration:", e); } - } + + // Fallback to the old canvas drawing system + const floor_cnt = await db.select({ floor: table.plans.floor, json: table.plans.plan }).from(table.plans).where(eq(table.plans.floor, params.slug)); + if (floor_cnt.length == 0) { + await db.insert(table.plans).values({ floor: params.slug, plan: { + "regions": [ + { "start": { "x": 100, "y": 100 }, "end": { "x": 400, "y": 100 } }, + { "start": { "x": 400, "y": 100 }, "end": { "x": 400, "y": 300 } }, + { "start": { "x": 400, "y": 300 }, "end": { "x": 100, "y": 300 } }, + { "start": { "x": 100, "y": 300 }, "end": { "x": 100, "y": 100 } } + ], + "doors": [ + { "location": { "x": 240, "y": 100 }, "width": 50, "rotation": 0 } + ], + "furnitures": [ + { + "minBound": { "x": 150, "y": 150 }, + "maxBound": { "x": 200, "y": 200 }, + "equipName": "Table", + "xPlacement": 150, + "yPlacement": 150, + "rotation": 0 + } + ] + }}); + } + const floor_ = await db.select({ floor: table.plans.floor, json: table.plans.plan }).from(table.plans).where(eq(table.plans.floor, params.slug)); - return { slug: params.slug, floor: floor_ }; + return { + slug: params.slug, + floor: floor_, + hasConfig: false + }; }; diff --git a/src/routes/(app)/[slug]/+page.svelte b/src/routes/(app)/[slug]/+page.svelte index 2f90051..27ee060 100644 --- a/src/routes/(app)/[slug]/+page.svelte +++ b/src/routes/(app)/[slug]/+page.svelte @@ -1,5 +1,6 @@ -
-

Floor {data.slug}

- {mqttMessage} - +
+

Floor {data.slug}

+ + {#if data.hasConfig && data.floorConfig} + +
+ {#if data.floorConfig.image} +
+ Floor plan + + {#if data.floorConfig.devices} + {#each data.floorConfig.devices as device} +
+
+ + {device.name} +
+
+
+ + 23°C +
+
+ + 45% +
+
+ + 1013 +
+
+ + 120m +
+
+
+ {/each} + {/if} +
+ +
+

MQTT Status: {mqttMessage}

+
+ {/if} +
+ {:else} + +
+ {mqttMessage} + +
+ {/if}
diff --git a/src/routes/(app)/settings/+page.server.js b/src/routes/(app)/settings/+page.server.js index 365a6b8..f65e398 100644 --- a/src/routes/(app)/settings/+page.server.js +++ b/src/routes/(app)/settings/+page.server.js @@ -4,7 +4,24 @@ import { fail } from "@sveltejs/kit"; import { eq } from "drizzle-orm"; export const load = async (event) => { - return {}; + // Fetch all available floors + const floors = await db + .select({ floor: table.floors.floor }) + .from(table.floors) + .orderBy(table.floors.floor); + + // Provide mock ESP8266 devices for now + const devices = [ + { id: "esp8266-001", name: "ESP8266 #001", type: "esp8266", status: "online" }, + { id: "esp8266-002", name: "ESP8266 #002", type: "esp8266", status: "online" }, + { id: "esp8266-003", name: "ESP8266 #003", type: "esp8266", status: "offline" }, + { id: "esp8266-004", name: "ESP8266 #004", type: "esp8266", status: "online" }, + ]; + + return { + floors: floors.map((f) => f.floor), + devices: devices, + }; }; export const actions = { @@ -41,4 +58,49 @@ export const actions = { await db.delete(table.floors).where(eq(table.floors.floor, n)); }, + savefloor: async (event) => { + const formData = await event.request.formData(); + const floorNumber = formData.get("floorNumber"); + const floorPlanImage = formData.get("floorPlanImage"); + const devices = formData.get("devices"); + + const n = Number(floorNumber); + if (isNaN(n)) return fail(400, { message: "Invalid floor number!" }); + + if (!floorPlanImage || !devices) { + return fail(400, { message: "Missing floor plan or device configuration!" }); + } + + try { + const deviceData = JSON.parse(devices); + + // Check if floor exists + const exists = await db + .select({ floor: table.floors.floor }) + .from(table.floors) + .where(eq(table.floors.floor, n)); + + if (exists.length === 0) { + return fail(400, { message: `Floor ${n} does not exist! Please create it first.` }); + } + + // Update floor with configuration + // Note: In a real implementation, you would store this data properly + // For now, we'll just update the url field as a JSON string + const floorConfig = { + image: floorPlanImage, + devices: deviceData, + }; + + await db + .update(table.floors) + .set({ url: JSON.stringify(floorConfig) }) + .where(eq(table.floors.floor, n)); + + return { success: true, message: `Floor ${n} configuration saved successfully!` }; + } catch (error) { + console.error("Error saving floor configuration:", error); + return fail(500, { message: "Failed to save floor configuration!" }); + } + }, }; diff --git a/src/routes/(app)/settings/+page.svelte b/src/routes/(app)/settings/+page.svelte index 173578d..d8a8fb6 100644 --- a/src/routes/(app)/settings/+page.svelte +++ b/src/routes/(app)/settings/+page.svelte @@ -10,19 +10,29 @@ import CardTitle from "$lib/components/ui/card/card-title.svelte"; import { Thermometer, Droplets, Gauge, Mountain, Upload, Cpu } from "@lucide/svelte"; - const { form } = $props(); + const { form, data } = $props(); let floorPlanImage = $state(null); - let availableDevices = $state([ - { id: "esp8266-1", name: "ESP8266 #1", type: "esp8266", status: "online" }, - { id: "esp8266-2", name: "ESP8266 #2", type: "esp8266", status: "online" }, - { id: "esp8266-3", name: "ESP8266 #3", type: "esp8266", status: "offline" }, - { id: "esp8266-4", name: "ESP8266 #4", type: "esp8266", status: "online" }, - ]); + let availableDevices = $state(data.devices || []); let placedDevices = $state([]); let draggedDevice = $state(null); let floorPlanRef = $state(null); + let selectedFloorNumber = $state(""); + let saveMessage = $state(null); + + $effect(() => { + if (data.floors && data.floors.length > 0 && !selectedFloorNumber) { + selectedFloorNumber = data.floors[0].toString(); + } + }); + + $effect(() => { + // Update devices when data changes + if (data.devices) { + availableDevices = data.devices; + } + }); function handleFileUpload(event) { const file = event.target.files?.[0]; @@ -168,6 +178,65 @@

Drag ESP8266 devices from below onto the floor plan to place them in specific rooms.

+ + {#if floorPlanImage && placedDevices.length > 0} +
+

Save Floor Configuration

+
{ + return async ({ result }) => { + if (result.type === "success") { + saveMessage = { type: "success", text: result.data.message }; + // Clear form after successful save + selectedFloorNumber = ""; + } else if (result.type === "failure") { + saveMessage = { + type: "error", + text: result.data?.message || "Failed to save configuration", + }; + } + }; + }} + > + + +
+
+ + +
+ +
+
+ {#if saveMessage} +

+ {saveMessage.text} +

+ {/if} +
+ {/if} {/if}
diff --git a/src/routes/mqtt/+server.js b/src/routes/mqtt/+server.js index a834279..984b83f 100644 --- a/src/routes/mqtt/+server.js +++ b/src/routes/mqtt/+server.js @@ -18,6 +18,7 @@ function connectMqtt() { // Replace with your MQTT broker details const BROKER_URL = "mqtt://kada49.it:1883"; // Example public broker + //const BROKER_URL = "mqtt://test.mosquitto.org:1883"; const TOPIC = "esp8266/+/data"; // Replace with your desired topic client = mqtt.connect(BROKER_URL); From 91ca53880b9c2120af2a3e22e9d6d4ab1fc2918b Mon Sep 17 00:00:00 2001 From: Yan Zhou Date: Thu, 12 Jun 2025 20:36:14 +0200 Subject: [PATCH 02/10] added get device and update the values live --- src/lib/server/mqtt-devices.js | 42 +++++ src/routes/(app)/[slug]/+page.server.ts | 13 ++ src/routes/(app)/[slug]/+page.svelte | 87 ++++++++-- src/routes/(app)/settings/+page.server.js | 22 ++- src/routes/(app)/settings/+page.svelte | 87 +++++++++- src/routes/mqtt/+server.js | 188 ++++++++++++++++++++-- 6 files changed, 407 insertions(+), 32 deletions(-) create mode 100644 src/lib/server/mqtt-devices.js diff --git a/src/lib/server/mqtt-devices.js b/src/lib/server/mqtt-devices.js new file mode 100644 index 0000000..8445506 --- /dev/null +++ b/src/lib/server/mqtt-devices.js @@ -0,0 +1,42 @@ +// src/lib/server/mqtt-devices.js +// This module provides access to MQTT device data for other parts of the application + +// We'll import the maps directly from the MQTT server module + +let connectedDevices = new Map(); +let deviceSensorData = new Map(); + +// Export the maps so the MQTT server can set them +export { connectedDevices, deviceSensorData }; + +// Clean up offline devices (not seen in last 5 minutes) +function cleanupDevices() { + const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); + let updated = false; + + for (const [deviceId, device] of connectedDevices.entries()) { + if (device.lastSeen < fiveMinutesAgo && device.status === "online") { + connectedDevices.set(deviceId, { ...device, status: "offline" }); + updated = true; + } + } + + return updated; +} + +// Export function to get current devices +export function getCurrentDevices() { + cleanupDevices(); + return Array.from(connectedDevices.values()).map((device) => { + const sensorData = deviceSensorData.get(device.id); + return { + ...device, + sensorData: sensorData || null, + }; + }); +} + +// Export function to get sensor data for a specific device +export function getDeviceSensorData(deviceId) { + return deviceSensorData.get(deviceId) || null; +} diff --git a/src/routes/(app)/[slug]/+page.server.ts b/src/routes/(app)/[slug]/+page.server.ts index 68d7496..8ce7412 100644 --- a/src/routes/(app)/[slug]/+page.server.ts +++ b/src/routes/(app)/[slug]/+page.server.ts @@ -3,6 +3,7 @@ import * as table from "$lib/server/db/schema"; import { eq } from "drizzle-orm"; import type { PageServerLoad } from "./$types"; import { connect } from "mqtt"; +import { getDeviceSensorData } from "$lib/server/mqtt-devices.js"; export const load: PageServerLoad = async ({ params }) => { // Convert slug to number for floor lookup @@ -18,6 +19,18 @@ export const load: PageServerLoad = async ({ params }) => { try { // Try to parse the saved configuration const config = JSON.parse(floorData[0].url); + + // Add real sensor data to devices + if (config.devices) { + config.devices = config.devices.map(device => { + const sensorData = getDeviceSensorData(device.id); + return { + ...device, + sensorData: sensorData + }; + }); + } + return { slug: params.slug, floorConfig: config, diff --git a/src/routes/(app)/[slug]/+page.svelte b/src/routes/(app)/[slug]/+page.svelte index 27ee060..3aa8411 100644 --- a/src/routes/(app)/[slug]/+page.svelte +++ b/src/routes/(app)/[slug]/+page.svelte @@ -6,22 +6,50 @@ let mqttMessage = $state("Waiting for MQTT message..."); let eventSource; + let deviceSensorData = $state(new Map()); + let refreshInterval; onMount(() => { // Connect to the SSE endpoint + console.log("Attempting to connect to MQTT SSE..."); eventSource = new EventSource("/mqtt"); eventSource.onopen = () => { - console.log("SSE connection opened."); + console.log("SSE connection opened successfully!"); }; eventSource.onmessage = (event) => { + if (eventSource.readyState !== EventSource.OPEN) { + console.log("Floor page: Received message but connection is not open, ignoring"); + return; + } + try { - const data = JSON.parse(event.data); - mqttMessage = data.message; - console.log("Received SSE message:", mqttMessage); + const messageData = JSON.parse(event.data); + console.log("Floor page received SSE data:", messageData); + + // Handle device updates + if (messageData.devices) { + console.log("Received devices:", messageData.devices); + const newData = new Map(); + messageData.devices.forEach((device) => { + console.log(`Device ${device.id} has sensor data:`, device.sensorData); + if (device.sensorData) { + newData.set(device.id, device.sensorData); + } + }); + deviceSensorData = newData; + console.log("Updated deviceSensorData:", deviceSensorData); + } + + // Handle MQTT message if present + if (messageData.message) { + mqttMessage = messageData.message; + } else { + mqttMessage = `Last update: ${messageData.timestamp}`; + } } catch (e) { - console.error("Error parsing SSE message:", e); + console.error("Floor page: Error parsing SSE message:", e, "Raw data:", event.data); } }; @@ -32,9 +60,12 @@ }); onDestroy(() => { - if (eventSource) { - console.log("Closing SSE connection."); - eventSource.close(); // Clean up the connection when the component is destroyed + if (eventSource && eventSource.readyState !== EventSource.CLOSED) { + console.log("Floor page: Closing SSE connection"); + eventSource.close(); + } + if (refreshInterval) { + clearInterval(refreshInterval); } }); @@ -114,19 +145,51 @@
- 23°C + + {#if deviceSensorData.get(device.id)?.temperature !== undefined} + {deviceSensorData.get(device.id).temperature.toFixed(1)}°C + {:else if device.sensorData} + {device.sensorData.temperature.toFixed(1)}°C + {:else} + --°C + {/if} +
- 45% + + {#if deviceSensorData.get(device.id)?.humidity !== undefined} + {deviceSensorData.get(device.id).humidity.toFixed(1)}% + {:else if device.sensorData} + {device.sensorData.humidity.toFixed(1)}% + {:else} + --% + {/if} +
- 1013 + + {#if deviceSensorData.get(device.id)?.pressure !== undefined} + {Math.round(deviceSensorData.get(device.id).pressure)}Pa + {:else if device.sensorData} + {Math.round(device.sensorData.pressure)}Pa + {:else} + --Pa + {/if} +
- 120m + + {#if deviceSensorData.get(device.id)?.altitude !== undefined} + {deviceSensorData.get(device.id).altitude.toFixed(1)}m + {:else if device.sensorData} + {device.sensorData.altitude.toFixed(1)}m + {:else} + --m + {/if} +
diff --git a/src/routes/(app)/settings/+page.server.js b/src/routes/(app)/settings/+page.server.js index f65e398..e7bdcb0 100644 --- a/src/routes/(app)/settings/+page.server.js +++ b/src/routes/(app)/settings/+page.server.js @@ -1,5 +1,6 @@ import { db } from "$lib/server/db"; import * as table from "$lib/server/db/schema"; +import { getCurrentDevices } from "$lib/server/mqtt-devices.js"; import { fail } from "@sveltejs/kit"; import { eq } from "drizzle-orm"; @@ -10,13 +11,20 @@ export const load = async (event) => { .from(table.floors) .orderBy(table.floors.floor); - // Provide mock ESP8266 devices for now - const devices = [ - { id: "esp8266-001", name: "ESP8266 #001", type: "esp8266", status: "online" }, - { id: "esp8266-002", name: "ESP8266 #002", type: "esp8266", status: "online" }, - { id: "esp8266-003", name: "ESP8266 #003", type: "esp8266", status: "offline" }, - { id: "esp8266-004", name: "ESP8266 #004", type: "esp8266", status: "online" }, - ]; + // Get real connected devices from MQTT + let devices = getCurrentDevices(); + + // If no real devices, provide fallback mock devices + if (devices.length === 0) { + devices = [ + { + id: "no-devices", + name: "No ESP8266 devices connected", + type: "esp8266", + status: "offline", + }, + ]; + } return { floors: floors.map((f) => f.floor), diff --git a/src/routes/(app)/settings/+page.svelte b/src/routes/(app)/settings/+page.svelte index d8a8fb6..9b57069 100644 --- a/src/routes/(app)/settings/+page.svelte +++ b/src/routes/(app)/settings/+page.svelte @@ -1,5 +1,6 @@
+

Floor {data.slug}

{#if data.hasConfig && data.floorConfig} @@ -209,4 +287,127 @@
{/if} + + Statistics + + + + + {#snippet child({ props })} + + {/snippet} + + + {#each table as column} + (column.visible = !value)} + > + {column.id} + + {/each} + + +
+ Data for floor {data.slug} + Showing total data for the last 3 months +
+ + + {selectedLabel} + + + Last 3 months + Last 30 days + Last 7 days + + +
+ + + { + return v.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + }, + }, + + yAxis: { format: () => "" }, + }} + > + {#snippet marks({ series, getAreaProps })} + + + + + + + + + + + + {#each series as s, i (s.key)} + + {/each} + + {/snippet} + {#snippet tooltip()} + { + return v.toLocaleDateString("en-US", { + month: "long", + }); + }} + indicator="line" + /> + {/snippet} + + + + +
+
diff --git a/src/routes/(app)/[slug]/stats/+server.ts b/src/routes/(app)/[slug]/stats/+server.ts new file mode 100644 index 0000000..6356340 --- /dev/null +++ b/src/routes/(app)/[slug]/stats/+server.ts @@ -0,0 +1,13 @@ +import { db } from "$lib/server/db"; +import * as table from "$lib/server/db/schema" + +export const GET = async () => { + const data = await db.select({ sensor: table.sensorData.sensor }).from(table.sensorData); + console.log(data); + + return new Response(JSON.stringify(data), { + headers: { + "Content-Type": "application/json" + } + }); +} diff --git a/src/routes/mqtt/+server.js b/src/routes/mqtt/+server.js index d705f2a..be1d000 100644 --- a/src/routes/mqtt/+server.js +++ b/src/routes/mqtt/+server.js @@ -1,6 +1,6 @@ // src/routes/mqtt/+server.js import { connectedDevices, deviceSensorData, getCurrentDevices } from "$lib/server/mqtt-devices.js"; -import * as mqtt from "mqtt"; +import mqtt from "mqtt"; import { writable } from "svelte/store"; // A Svelte store to hold the latest MQTT message. From e29c6db90800968fbcacf62507da0234f6275424 Mon Sep 17 00:00:00 2001 From: David Senoner Date: Thu, 12 Jun 2025 22:46:07 +0200 Subject: [PATCH 04/10] save data if sensor is on some floor --- src/lib/server/db/schema.js | 4 ++-- src/routes/(app)/[slug]/+page.svelte | 15 ++++----------- src/routes/(app)/[slug]/stats/+server.ts | 2 +- src/routes/(app)/settings/+page.server.js | 3 +++ src/routes/mqtt/+server.js | 16 +++++++++++++++- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/lib/server/db/schema.js b/src/lib/server/db/schema.js index 9e7680e..e048caa 100644 --- a/src/lib/server/db/schema.js +++ b/src/lib/server/db/schema.js @@ -34,7 +34,7 @@ export const sensors = pgTable("sensors", { }); export const sensorData = pgTable("sensor_data", { - uuid: uuid().primaryKey(), + uuid: uuid().primaryKey().defaultRandom(), sensor: text("sensor") .references(() => sensors.id) .notNull(), @@ -42,5 +42,5 @@ export const sensorData = pgTable("sensor_data", { humidity: real().notNull(), pressure: real().notNull(), altitude: real().notNull(), - time: timestamp().notNull().defaultNow(), + time: timestamp({ withTimezone: true, mode: "date" }).notNull().defaultNow(), }); diff --git a/src/routes/(app)/[slug]/+page.svelte b/src/routes/(app)/[slug]/+page.svelte index e31f282..5be7cd3 100644 --- a/src/routes/(app)/[slug]/+page.svelte +++ b/src/routes/(app)/[slug]/+page.svelte @@ -136,16 +136,7 @@ import { cubicInOut } from "svelte/easing"; import { ChevronDownIcon, Columns2 } from "@lucide/svelte"; - let chartData = $state([ - { date: new Date("2024-04-01"), desktop: 222, mobile: 150 }, - { date: new Date("2024-04-02"), desktop: 97, mobile: 180 }, - { date: new Date("2024-04-03"), desktop: 167, mobile: 120 }, - { date: new Date("2024-04-04"), desktop: 242, mobile: 260 }, - { date: new Date("2024-06-27"), desktop: 448, mobile: 490 }, - { date: new Date("2024-06-28"), desktop: 149, mobile: 200 }, - { date: new Date("2024-06-29"), desktop: 103, mobile: 160 }, - { date: new Date("2024-06-30"), desktop: 446, mobile: 400 }, - ]); + let chartData = $state([]); let timeRange = $state("90d"); @@ -194,7 +185,9 @@ const number = await response.json(); console.log(number); - chartData = []; + chartData = number.map((obj) => { + return { date: new Date(obj.date), desktop: obj.temp, mobile: obj.pressure }; + }); } }); diff --git a/src/routes/(app)/[slug]/stats/+server.ts b/src/routes/(app)/[slug]/stats/+server.ts index 6356340..a719178 100644 --- a/src/routes/(app)/[slug]/stats/+server.ts +++ b/src/routes/(app)/[slug]/stats/+server.ts @@ -2,7 +2,7 @@ import { db } from "$lib/server/db"; import * as table from "$lib/server/db/schema" export const GET = async () => { - const data = await db.select({ sensor: table.sensorData.sensor }).from(table.sensorData); + const data = await db.select({ date: table.sensorData.time, temp: table.sensorData.temperature, pressure: table.sensorData.pressure }).from(table.sensorData); console.log(data); return new Response(JSON.stringify(data), { diff --git a/src/routes/(app)/settings/+page.server.js b/src/routes/(app)/settings/+page.server.js index e7bdcb0..1939028 100644 --- a/src/routes/(app)/settings/+page.server.js +++ b/src/routes/(app)/settings/+page.server.js @@ -81,6 +81,9 @@ export const actions = { try { const deviceData = JSON.parse(devices); + deviceData.forEach(async (dev) => { + await db.insert(table.sensors).values({ id: dev.id, user: event.locals.session.userId }); + }); // Check if floor exists const exists = await db diff --git a/src/routes/mqtt/+server.js b/src/routes/mqtt/+server.js index be1d000..75ae0aa 100644 --- a/src/routes/mqtt/+server.js +++ b/src/routes/mqtt/+server.js @@ -1,5 +1,8 @@ // src/routes/mqtt/+server.js +import { db } from "$lib/server/db"; +import * as table from "$lib/server/db/schema"; import { connectedDevices, deviceSensorData, getCurrentDevices } from "$lib/server/mqtt-devices.js"; +import { eq } from "drizzle-orm"; import mqtt from "mqtt"; import { writable } from "svelte/store"; @@ -114,7 +117,7 @@ function connectMqtt() { }); }); - client.on("message", (topic, message) => { + client.on("message", async (topic, message) => { const payload = message.toString(); console.log(`Received message from topic "${topic}": ${payload}`); latestMessage.set(payload); // Update the Svelte store @@ -130,6 +133,17 @@ function connectMqtt() { if (sensorData) { console.log(`Parsed sensor data:`, sensorData); updateDevice(deviceId, sensorData); + const devices = await db.select().from(table.sensors).where(eq(table.sensors.id, deviceId)); + if (devices.length == 1) + await db + .insert(table.sensorData) + .values({ + sensor: deviceId, + temperature: sensorData.temperature, + humidity: sensorData.humidity, + altitude: sensorData.altitude, + pressure: sensorData.pressure, + }); } else { // Still update device as online even if data parsing failed updateDevice(deviceId); From 1841746f6a40abe70dfe908dd6357c1fc2af258b Mon Sep 17 00:00:00 2001 From: David Senoner Date: Fri, 13 Jun 2025 09:04:05 +0200 Subject: [PATCH 05/10] show all info on graph --- src/routes/(app)/[slug]/+page.svelte | 96 +++++++++++++++--------- src/routes/(app)/[slug]/stats/+server.ts | 2 +- 2 files changed, 63 insertions(+), 35 deletions(-) diff --git a/src/routes/(app)/[slug]/+page.svelte b/src/routes/(app)/[slug]/+page.svelte index 5be7cd3..63b3cbe 100644 --- a/src/routes/(app)/[slug]/+page.svelte +++ b/src/routes/(app)/[slug]/+page.svelte @@ -1,6 +1,6 @@ @@ -316,7 +335,7 @@ {/if} Statistics - +
Statistics for floor {data.slug} @@ -326,27 +345,29 @@
- + {#snippet child({ props })} {/snippet} - {#each table as column} - - {column.key} - - {/each} + + {#each sensorOptions as sensor} + + {sensor.label} + + {/each} + - + {selectedLabel} @@ -362,71 +383,215 @@
- - { - return v.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - }, - }, + {#if isLoading} +
+
+
+

Loading statistics...

+
+
+ {:else if hasError} +
+
+

Error loading statistics

+

Please try again later

+
+
+ {:else if chartData.length === 0} +
+
+

No data available

+

+ Statistics will appear once sensor data is collected +

+
+
+ {:else} + + + {selectedSensorConfig.label} + + +
+ {#if filteredData.length > 0} + +
+
+
Latest
+
+ {filteredData[filteredData.length - 1]?.[selectedSensor]?.toFixed(1)} + {#if selectedSensor === "temperature"}°C + {:else if selectedSensor === "humidity"}% + {:else if selectedSensor === "pressure"}kPa + {:else if selectedSensor === "altitude"}m + {/if} +
+
+
+
Average
+
+ {( + filteredData.reduce((sum, item) => sum + item[selectedSensor], 0) / + filteredData.length + ).toFixed(1)} + {#if selectedSensor === "temperature"}°C + {:else if selectedSensor === "humidity"}% + {:else if selectedSensor === "pressure"}kPa + {:else if selectedSensor === "altitude"}m + {/if} +
+
+
+
Range
+
+ {Math.min(...filteredData.map((d) => d[selectedSensor])).toFixed(1)} - {Math.max( + ...filteredData.map((d) => d[selectedSensor]), + ).toFixed(1)} + {#if selectedSensor === "temperature"}°C + {:else if selectedSensor === "humidity"}% + {:else if selectedSensor === "pressure"}kPa + {:else if selectedSensor === "altitude"}m + {/if} +
+
+
- yAxis: { format: () => "" }, - }} - > - {#snippet marks({ series, getAreaProps })} - - - - - - - - - - - - {#each series as s, i (s.key)} - - {/each} - - {/snippet} - {#snippet tooltip()} - { - return v.toLocaleDateString("en-US", { - month: "long", - }); - }} - indicator="line" - /> - {/snippet} - - + +
+ + + {#if filteredData.length > 1} + {@const minValue = Math.min(...filteredData.map((d) => d[selectedSensor]))} + {@const maxValue = Math.max(...filteredData.map((d) => d[selectedSensor]))} + {@const valueRange = maxValue - minValue || 1} + {@const padding = 30} + {@const chartWidth = 800 - padding * 2} + {@const chartHeight = 400 - padding * 2} + + + + + + + + + + + {maxValue.toFixed(1)} + {((minValue + maxValue) / 2).toFixed(1)} + {minValue.toFixed(1)} + + + {#if filteredData.length > 0} + {@const firstDate = filteredData[0].date} + {@const lastDate = filteredData[filteredData.length - 1].date} + {@const midIndex = Math.floor(filteredData.length / 2)} + {@const midDate = filteredData[midIndex].date} + + + {@const formatDate = (date) => { + if (timeRange === "7d") { + return date.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + }); + } else if (timeRange === "30d") { + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + } else { + return date.toLocaleDateString("en-US", { month: "short" }); + } + }} + + {formatDate(firstDate)} + {formatDate(midDate)} + {formatDate(lastDate)} + {/if} + + + { + const x = padding + (i / (filteredData.length - 1)) * chartWidth; + const y = + padding + + chartHeight - + ((d[selectedSensor] - minValue) / valueRange) * chartHeight; + return `${x},${y}`; + }) + .join(" ")} + /> + {/if} + +
+ +
+ Data from {filteredData[0]?.date?.toLocaleDateString()} to {filteredData[ + filteredData.length - 1 + ]?.date?.toLocaleDateString()} +
+ {:else} +
+
+

No data available

+

+ No data found for the selected time period +

+
+
+ {/if} +
+
+
+ {/if} diff --git a/src/routes/(app)/[slug]/stats/+server.ts b/src/routes/(app)/[slug]/stats/+server.ts index 32fa2db..fc68394 100644 --- a/src/routes/(app)/[slug]/stats/+server.ts +++ b/src/routes/(app)/[slug]/stats/+server.ts @@ -2,8 +2,23 @@ import { db } from "$lib/server/db"; import * as table from "$lib/server/db/schema" export const GET = async () => { - const data = await db.select({ altitude: table.sensorData.altitude, humidity: table.sensorData.humidity, pressure: table.sensorData.pressure, temperature: table.sensorData.temperature, date: table.sensorData.time, }).from(table.sensorData); - console.log(data); + // Get all sensor data (like the original working version) + const rawData = await db.select({ + altitude: table.sensorData.altitude, + humidity: table.sensorData.humidity, + pressure: table.sensorData.pressure, + temperature: table.sensorData.temperature, + date: table.sensorData.time, + }).from(table.sensorData); + + // Scale pressure values to be in a similar range as other sensors + // Divide by 1000 to convert from Pa to kPa (more reasonable scale) + const data = rawData.map(item => ({ + ...item, + pressure: Math.round((item.pressure / 1000) * 10) / 10 // Convert to kPa with 1 decimal place + })); + + console.log(`Returning ${data.length} data points`); return new Response(JSON.stringify(data), { headers: { From 9fc7453a44ab15d3c9469b4112c991dd50b98068 Mon Sep 17 00:00:00 2001 From: David Senoner Date: Sat, 14 Jun 2025 10:12:53 +0200 Subject: [PATCH 07/10] merge settings and use same style for dropdowns --- src/routes/(app)/[slug]/+page.svelte | 76 +++++++++++++++++----------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/src/routes/(app)/[slug]/+page.svelte b/src/routes/(app)/[slug]/+page.svelte index 6491725..286d852 100644 --- a/src/routes/(app)/[slug]/+page.svelte +++ b/src/routes/(app)/[slug]/+page.svelte @@ -129,11 +129,6 @@ import * as Card from "$lib/components/ui/card/index.js"; import * as Select from "$lib/components/ui/select/index.js"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; - import { scaleUtc } from "d3-scale"; - import { Area, AreaChart, ChartClipPath } from "layerchart"; - import { curveNatural } from "d3-shape"; - import ChartContainer from "$lib/components/ui/chart/chart-container.svelte"; - import { cubicInOut } from "svelte/easing"; import { ChevronDownIcon, Columns2 } from "@lucide/svelte"; let chartData = $state([]); @@ -177,6 +172,20 @@ let selectedSensor = $state("temperature"); + const sensorLabel = $derived.by(() => { + switch (selectedSensor) { + case "altitude": + return "Altitude (m)"; + case "humidity": + return "Humidity (%)"; + case "pressure": + return "Pressure (kPa)"; + case "temperature": + default: + return "Temperature (°C)"; + } + }); + const sensorOptions = [ { key: "temperature", @@ -203,6 +212,7 @@ const selectedSensorConfig = $derived( sensorOptions.find((s) => s.key === selectedSensor) || sensorOptions[0], ); + const device = "macaddress"; let isLoading = $state(false); let hasError = $state(false); @@ -243,14 +253,31 @@ } } }); + + function exportCSV(): void { + let csv = "data:text/csv;charset=utf-8,"; + + const headers = "time," + selectedSensorConfig.key; + csv += headers + "\n"; + + chartData.forEach((obj: any) => { + csv += obj.date.toISOString() + "," + obj[selectedSensorConfig.key] + "\n"; + }); + + const encodedUri = encodeURI(csv); + const link = document.createElement("a"); + link.setAttribute("href", encodedUri); + link.setAttribute("download", device + "_" + selectedSensorConfig.key + "_data.csv"); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }
-

Floor {data.slug}

{#if data.hasConfig && data.floorConfig} -
{#if data.floorConfig.image}
@@ -327,7 +354,6 @@ {/if}
{:else} -
{mqttMessage} @@ -344,27 +370,17 @@
- - - {#snippet child({ props })} - - {/snippet} - - - - {#each sensorOptions as sensor} - - {sensor.label} - - {/each} - - - + + + {sensorLabel} + + + Altitude (m) + Humidity (%) + Pressure (kPa) + Temperature (°C) + + @@ -377,7 +393,7 @@ - From b57d9b787559aa6cd9bca8645043e7ff305cc65e Mon Sep 17 00:00:00 2001 From: David Senoner Date: Sat, 14 Jun 2025 11:02:44 +0200 Subject: [PATCH 08/10] add selector for esp sensors in statistics screen --- src/routes/(app)/[slug]/+page.svelte | 41 ++++++++++++++++++------ src/routes/(app)/[slug]/stats/+server.ts | 10 ++++-- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/routes/(app)/[slug]/+page.svelte b/src/routes/(app)/[slug]/+page.svelte index 286d852..b825992 100644 --- a/src/routes/(app)/[slug]/+page.svelte +++ b/src/routes/(app)/[slug]/+page.svelte @@ -128,11 +128,17 @@ import * as Chart from "$lib/components/ui/chart/index.js"; import * as Card from "$lib/components/ui/card/index.js"; import * as Select from "$lib/components/ui/select/index.js"; - import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js"; - import { ChevronDownIcon, Columns2 } from "@lucide/svelte"; let chartData = $state([]); + let newChartData = $derived( + chartData.filter((item: object) => { + if (item.sensorId === selectedESP) { + return { ...item }; + } + }), + ); + let timeRange = $state("90d"); const selectedLabel = $derived.by(() => { @@ -149,7 +155,7 @@ }); const filteredData = $derived( - chartData.filter((item) => { + newChartData.filter((item) => { const now = new Date(); let daysToSubtract = 90; if (timeRange === "30d") { @@ -212,7 +218,9 @@ const selectedSensorConfig = $derived( sensorOptions.find((s) => s.key === selectedSensor) || sensorOptions[0], ); - const device = "macaddress"; + + let esps = $state([]); + let selectedESP = $state("Select ESP sensor"); let isLoading = $state(false); let hasError = $state(false); @@ -234,6 +242,10 @@ const rawData = await response.json(); console.log("Raw stats data:", rawData); + const allesps = rawData.map((i: any) => { + return i.sensorId; + }); + esps = allesps.filter((v: any, i: any, a: any) => a.indexOf(v) === i); if (!rawData || rawData.length === 0) { console.log("No data available for statistics"); @@ -260,14 +272,14 @@ const headers = "time," + selectedSensorConfig.key; csv += headers + "\n"; - chartData.forEach((obj: any) => { + newChartData.forEach((obj: any) => { csv += obj.date.toISOString() + "," + obj[selectedSensorConfig.key] + "\n"; }); const encodedUri = encodeURI(csv); const link = document.createElement("a"); link.setAttribute("href", encodedUri); - link.setAttribute("download", device + "_" + selectedSensorConfig.key + "_data.csv"); + link.setAttribute("download", selectedESP + "_" + selectedSensorConfig.key + "_data.csv"); document.body.appendChild(link); link.click(); document.body.removeChild(link); @@ -370,8 +382,19 @@
+ + + {selectedESP} + + + {#each esps as esp} + {esp} + {/each} + + + - + {sensorLabel} @@ -383,7 +406,7 @@ - + {selectedLabel} @@ -415,7 +438,7 @@

Please try again later

- {:else if chartData.length === 0} + {:else if newChartData.length === 0}

No data available

diff --git a/src/routes/(app)/[slug]/stats/+server.ts b/src/routes/(app)/[slug]/stats/+server.ts index fc68394..226f421 100644 --- a/src/routes/(app)/[slug]/stats/+server.ts +++ b/src/routes/(app)/[slug]/stats/+server.ts @@ -1,15 +1,19 @@ import { db } from "$lib/server/db"; import * as table from "$lib/server/db/schema" +import { eq } from "drizzle-orm"; -export const GET = async () => { +export const GET = async (event) => { // Get all sensor data (like the original working version) + console.log(event.locals.session.userId) const rawData = await db.select({ altitude: table.sensorData.altitude, humidity: table.sensorData.humidity, pressure: table.sensorData.pressure, temperature: table.sensorData.temperature, - date: table.sensorData.time, - }).from(table.sensorData); + date: table.sensorData.time, + sensorId: table.sensorData.sensor, + user: table.sensors.user, + }).from(table.sensorData).innerJoin(table.sensors, eq(table.sensors.id, table.sensorData.sensor)).where(eq(table.sensors.user, event.locals.session.userId)); // Scale pressure values to be in a similar range as other sensors // Divide by 1000 to convert from Pa to kPa (more reasonable scale) From b8b0f2d6a1f5ebc5bcfc2855f1f6f73d3c27b134 Mon Sep 17 00:00:00 2001 From: David Senoner Date: Sun, 15 Jun 2025 14:23:16 +0200 Subject: [PATCH 09/10] remove unused imports --- src/lib/server/mqtt-devices.js | 9 ---- src/routes/(app)/[slug]/+page.server.ts | 6 --- src/routes/(app)/[slug]/+page.svelte | 16 +----- src/routes/(app)/[slug]/stats/+server.ts | 5 +- src/routes/(app)/settings/+page.server.js | 7 --- src/routes/(app)/settings/+page.svelte | 9 ---- src/routes/(auth)/login/+page.server.js | 2 - src/routes/mqtt/+server.js | 59 +++++------------------ 8 files changed, 15 insertions(+), 98 deletions(-) diff --git a/src/lib/server/mqtt-devices.js b/src/lib/server/mqtt-devices.js index 8445506..ad0b62d 100644 --- a/src/lib/server/mqtt-devices.js +++ b/src/lib/server/mqtt-devices.js @@ -1,15 +1,8 @@ -// src/lib/server/mqtt-devices.js -// This module provides access to MQTT device data for other parts of the application - -// We'll import the maps directly from the MQTT server module - let connectedDevices = new Map(); let deviceSensorData = new Map(); -// Export the maps so the MQTT server can set them export { connectedDevices, deviceSensorData }; -// Clean up offline devices (not seen in last 5 minutes) function cleanupDevices() { const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); let updated = false; @@ -24,7 +17,6 @@ function cleanupDevices() { return updated; } -// Export function to get current devices export function getCurrentDevices() { cleanupDevices(); return Array.from(connectedDevices.values()).map((device) => { @@ -36,7 +28,6 @@ export function getCurrentDevices() { }); } -// Export function to get sensor data for a specific device export function getDeviceSensorData(deviceId) { return deviceSensorData.get(deviceId) || null; } diff --git a/src/routes/(app)/[slug]/+page.server.ts b/src/routes/(app)/[slug]/+page.server.ts index 8ce7412..119d062 100644 --- a/src/routes/(app)/[slug]/+page.server.ts +++ b/src/routes/(app)/[slug]/+page.server.ts @@ -2,14 +2,11 @@ import { db } from "$lib/server/db"; import * as table from "$lib/server/db/schema"; import { eq } from "drizzle-orm"; import type { PageServerLoad } from "./$types"; -import { connect } from "mqtt"; import { getDeviceSensorData } from "$lib/server/mqtt-devices.js"; export const load: PageServerLoad = async ({ params }) => { - // Convert slug to number for floor lookup const floorNumber = Number(params.slug); - // First check if we have a saved floor configuration in the floors table const floorData = await db.select({ floor: table.floors.floor, url: table.floors.url @@ -17,10 +14,8 @@ export const load: PageServerLoad = async ({ params }) => { if (floorData.length > 0 && floorData[0].url && floorData[0].url !== "/") { try { - // Try to parse the saved configuration const config = JSON.parse(floorData[0].url); - // Add real sensor data to devices if (config.devices) { config.devices = config.devices.map(device => { const sensorData = getDeviceSensorData(device.id); @@ -41,7 +36,6 @@ export const load: PageServerLoad = async ({ params }) => { } } - // Fallback to the old canvas drawing system const floor_cnt = await db.select({ floor: table.plans.floor, json: table.plans.plan }).from(table.plans).where(eq(table.plans.floor, params.slug)); if (floor_cnt.length == 0) { await db.insert(table.plans).values({ floor: params.slug, plan: { diff --git a/src/routes/(app)/[slug]/+page.svelte b/src/routes/(app)/[slug]/+page.svelte index b825992..45c60c6 100644 --- a/src/routes/(app)/[slug]/+page.svelte +++ b/src/routes/(app)/[slug]/+page.svelte @@ -14,7 +14,6 @@ let refreshInterval; onMount(() => { - // Connect to the SSE endpoint console.log("Attempting to connect to MQTT SSE..."); eventSource = new EventSource("/mqtt"); @@ -32,7 +31,6 @@ const messageData = JSON.parse(event.data); console.log("Floor page received SSE data:", messageData); - // Handle device updates if (messageData.devices) { console.log("Received devices:", messageData.devices); const newData = new Map(); @@ -46,7 +44,6 @@ console.log("Updated deviceSensorData:", deviceSensorData); } - // Handle MQTT message if present if (messageData.message) { mqttMessage = messageData.message; } else { @@ -59,7 +56,7 @@ eventSource.onerror = (error) => { console.error("SSE Error:", error); - eventSource.close(); // Close the connection on error + eventSource.close(); }; }); @@ -82,7 +79,6 @@ return; } - // Draw walls, doors, and furniture (simplified example) drawRegions(data.regions); drawDoors(data.doors); drawFurniture(data.furnitures); @@ -100,7 +96,7 @@ function drawDoors(doors) { doors.forEach((door) => { ctx.beginPath(); - ctx.rect(door.location.x, door.location.y, door.width, 5); // Simplified door drawing + ctx.rect(door.location.x, door.location.y, door.width, 5); ctx.stroke(); }); } @@ -455,7 +451,6 @@
{#if filteredData.length > 0} -
Latest
@@ -497,10 +492,8 @@
-
- {#if filteredData.length > 1} {@const minValue = Math.min(...filteredData.map((d) => d[selectedSensor]))} {@const maxValue = Math.max(...filteredData.map((d) => d[selectedSensor]))} @@ -509,7 +502,6 @@ {@const chartWidth = 800 - padding * 2} {@const chartHeight = 400 - padding * 2} - - {maxValue.toFixed(1)} @@ -545,14 +536,12 @@ text-anchor="end">{minValue.toFixed(1)} - {#if filteredData.length > 0} {@const firstDate = filteredData[0].date} {@const lastDate = filteredData[filteredData.length - 1].date} {@const midIndex = Math.floor(filteredData.length / 2)} {@const midDate = filteredData[midIndex].date} - {@const formatDate = (date) => { if (timeRange === "7d") { return date.toLocaleDateString("en-US", { @@ -590,7 +579,6 @@ > {/if} - { - // Get all sensor data (like the original working version) console.log(event.locals.session.userId) const rawData = await db.select({ altitude: table.sensorData.altitude, @@ -15,11 +14,9 @@ export const GET = async (event) => { user: table.sensors.user, }).from(table.sensorData).innerJoin(table.sensors, eq(table.sensors.id, table.sensorData.sensor)).where(eq(table.sensors.user, event.locals.session.userId)); - // Scale pressure values to be in a similar range as other sensors - // Divide by 1000 to convert from Pa to kPa (more reasonable scale) const data = rawData.map(item => ({ ...item, - pressure: Math.round((item.pressure / 1000) * 10) / 10 // Convert to kPa with 1 decimal place + pressure: Math.round((item.pressure / 1000) * 10) / 10 })); console.log(`Returning ${data.length} data points`); diff --git a/src/routes/(app)/settings/+page.server.js b/src/routes/(app)/settings/+page.server.js index 1939028..20e70f4 100644 --- a/src/routes/(app)/settings/+page.server.js +++ b/src/routes/(app)/settings/+page.server.js @@ -5,16 +5,13 @@ import { fail } from "@sveltejs/kit"; import { eq } from "drizzle-orm"; export const load = async (event) => { - // Fetch all available floors const floors = await db .select({ floor: table.floors.floor }) .from(table.floors) .orderBy(table.floors.floor); - // Get real connected devices from MQTT let devices = getCurrentDevices(); - // If no real devices, provide fallback mock devices if (devices.length === 0) { devices = [ { @@ -85,7 +82,6 @@ export const actions = { await db.insert(table.sensors).values({ id: dev.id, user: event.locals.session.userId }); }); - // Check if floor exists const exists = await db .select({ floor: table.floors.floor }) .from(table.floors) @@ -95,9 +91,6 @@ export const actions = { return fail(400, { message: `Floor ${n} does not exist! Please create it first.` }); } - // Update floor with configuration - // Note: In a real implementation, you would store this data properly - // For now, we'll just update the url field as a JSON string const floorConfig = { image: floorPlanImage, devices: deviceData, diff --git a/src/routes/(app)/settings/+page.svelte b/src/routes/(app)/settings/+page.svelte index 9b57069..7c79fee 100644 --- a/src/routes/(app)/settings/+page.svelte +++ b/src/routes/(app)/settings/+page.svelte @@ -31,13 +31,11 @@ }); $effect(() => { - // Update devices when data changes if (data.devices) { availableDevices = data.devices; } }); - // Set up real-time updates via SSE onMount(() => { console.log("Settings: Attempting to connect to MQTT SSE..."); eventSource = new EventSource("/mqtt"); @@ -56,13 +54,10 @@ const messageData = JSON.parse(event.data); console.log("Settings page received SSE data:", messageData); - // Update device sensor data and device list from real-time updates if (messageData.devices) { console.log("Updating available devices with:", messageData.devices); - // Update available devices with latest data availableDevices = messageData.devices; - // Update sensor data map const newData = new Map(); messageData.devices.forEach((device) => { if (device.sensorData) { @@ -116,14 +111,11 @@ const x = ((event.clientX - rect.left) / rect.width) * 100; const y = ((event.clientY - rect.top) / rect.height) * 100; - // Check if device is already placed const existingIndex = placedDevices.findIndex((d) => d.id === draggedDevice.id); if (existingIndex >= 0) { - // Update position placedDevices[existingIndex] = { ...placedDevices[existingIndex], x, y }; } else { - // Add new device placedDevices = [...placedDevices, { ...draggedDevice, x, y }]; } @@ -244,7 +236,6 @@ return async ({ result }) => { if (result.type === "success") { saveMessage = { type: "success", text: result.data.message }; - // Clear form after successful save selectedFloorNumber = ""; } else if (result.type === "failure") { saveMessage = { diff --git a/src/routes/(auth)/login/+page.server.js b/src/routes/(auth)/login/+page.server.js index e4b51c5..686785a 100644 --- a/src/routes/(auth)/login/+page.server.js +++ b/src/routes/(auth)/login/+page.server.js @@ -63,7 +63,6 @@ export const actions = { const userId = generateUserId(); const passwordHash = await hash(password, { - // recommended minimum parameters memoryCost: 19456, timeCost: 2, outputLen: 32, @@ -84,7 +83,6 @@ export const actions = { }; function generateUserId() { - // ID with 120 bits of entropy, or about the same as UUID v4. const bytes = crypto.getRandomValues(new Uint8Array(15)); const id = encodeBase64url(bytes); return id; diff --git a/src/routes/mqtt/+server.js b/src/routes/mqtt/+server.js index 75ae0aa..999b0e7 100644 --- a/src/routes/mqtt/+server.js +++ b/src/routes/mqtt/+server.js @@ -1,4 +1,3 @@ -// src/routes/mqtt/+server.js import { db } from "$lib/server/db"; import * as table from "$lib/server/db/schema"; import { connectedDevices, deviceSensorData, getCurrentDevices } from "$lib/server/mqtt-devices.js"; @@ -6,20 +5,15 @@ import { eq } from "drizzle-orm"; import mqtt from "mqtt"; import { writable } from "svelte/store"; -// A Svelte store to hold the latest MQTT message. -// In a real application, you might want to store more data or -// use a more robust way to manage messages, but for a basic example, this works. const latestMessage = writable("No message yet"); const devices = writable([]); let client = null; -// Function to update device status and sensor data function updateDevice(deviceId, sensorData = null) { const now = new Date(); const deviceName = `ESP8266 #${deviceId.slice(-6)}`; - // Update device info connectedDevices.set(deviceId, { id: deviceId, name: deviceName, @@ -28,7 +22,6 @@ function updateDevice(deviceId, sensorData = null) { lastSeen: now, }); - // Update sensor data if provided if (sensorData) { deviceSensorData.set(deviceId, { ...sensorData, @@ -36,11 +29,9 @@ function updateDevice(deviceId, sensorData = null) { }); } - // Update the devices store updateDevicesStore(); } -// Function to parse sensor data from MQTT payload function parseSensorData(payload) { try { const data = JSON.parse(payload); @@ -56,10 +47,8 @@ function parseSensorData(payload) { } } -// Store SSE controllers for cleanup const sseControllers = new Set(); -// Function to update devices store with latest data function updateDevicesStore() { const deviceList = Array.from(connectedDevices.values()).map((device) => { const sensorData = deviceSensorData.get(device.id); @@ -71,7 +60,6 @@ function updateDevicesStore() { devices.set(deviceList); } -// Clean up offline devices (not seen in last 5 minutes) function cleanupDevices() { const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); let updated = false; @@ -88,21 +76,15 @@ function cleanupDevices() { } } -// Run cleanup every minute setInterval(cleanupDevices, 60000); -// getCurrentDevices is now imported from the shared module - -// Function to connect to MQTT function connectMqtt() { if (client && client.connected) { - return; // Already connected + return; } - // Replace with your MQTT broker details - const BROKER_URL = "mqtt://kada49.it:1883"; // Example public broker - //const BROKER_URL = "mqtt://test.mosquitto.org:1883"; - const TOPIC = "esp8266/+/data"; // Replace with your desired topic + const BROKER_URL = "mqtt://kada49.it:1883"; + const TOPIC = "esp8266/+/data"; client = mqtt.connect(BROKER_URL); @@ -120,32 +102,27 @@ function connectMqtt() { client.on("message", async (topic, message) => { const payload = message.toString(); console.log(`Received message from topic "${topic}": ${payload}`); - latestMessage.set(payload); // Update the Svelte store + latestMessage.set(payload); - // Extract device ID from topic (esp8266/DEVICE_ID/data) const topicParts = topic.split("/"); if (topicParts.length >= 3 && topicParts[0] === "esp8266" && topicParts[2] === "data") { const deviceId = topicParts[1]; console.log(`Processing data for device: ${deviceId}`); - // Parse sensor data const sensorData = parseSensorData(payload); if (sensorData) { console.log(`Parsed sensor data:`, sensorData); updateDevice(deviceId, sensorData); const devices = await db.select().from(table.sensors).where(eq(table.sensors.id, deviceId)); if (devices.length == 1) - await db - .insert(table.sensorData) - .values({ - sensor: deviceId, - temperature: sensorData.temperature, - humidity: sensorData.humidity, - altitude: sensorData.altitude, - pressure: sensorData.pressure, - }); + await db.insert(table.sensorData).values({ + sensor: deviceId, + temperature: sensorData.temperature, + humidity: sensorData.humidity, + altitude: sensorData.altitude, + pressure: sensorData.pressure, + }); } else { - // Still update device as online even if data parsing failed updateDevice(deviceId); } } @@ -154,20 +131,17 @@ function connectMqtt() { client.on("error", (err) => { console.error(`MQTT error: ${err}`); if (client) { - client.end(); // Close connection on error + client.end(); } client = null; - // Implement re-connection logic here if needed }); client.on("close", () => { console.log("MQTT connection closed."); client = null; - // Implement re-connection logic here if needed }); } -// Connect to MQTT when the server starts connectMqtt(); /** @type {import("./$types").RequestHandler} */ @@ -186,10 +160,8 @@ export async function GET({ request }) { let isConnected = true; let interval; - // Add this controller to the broadcast set sseControllers.add(controller); - // Cleanup function const cleanup = () => { console.log("Cleaning up MQTT SSE connection"); isConnected = false; @@ -203,7 +175,6 @@ export async function GET({ request }) { } }; - // Send current device data function sendDevices() { if (!isConnected) { cleanup(); @@ -217,12 +188,10 @@ export async function GET({ request }) { timestamp: new Date().toISOString(), }; - // Check if controller is still valid before enqueueing if (isConnected) { controller.enqueue(`data: ${JSON.stringify(message)}\n\n`); } } catch (error) { - // Handle controller closed error specifically if ( error.code === "ERR_INVALID_STATE" || error.message.includes("Controller is already closed") @@ -235,13 +204,10 @@ export async function GET({ request }) { } } - // Send initial data sendDevices(); - // Send updates every 2 seconds interval = setInterval(sendDevices, 2000); - // Subscribe to MQTT messages via the store for real-time updates const unsubscribe = latestMessage.subscribe((message) => { if (message !== "No message yet" && isConnected) { try { @@ -265,7 +231,6 @@ export async function GET({ request }) { } }); - // Handle client disconnection request.signal.addEventListener("abort", () => { console.log("Client disconnected from MQTT SSE stream"); cleanup(); From 0f79e7d121f85e89303f9d3d7ea6a89350ffae4f Mon Sep 17 00:00:00 2001 From: David Senoner Date: Sun, 15 Jun 2025 14:23:37 +0200 Subject: [PATCH 10/10] updated db --- drizzle/20241104190811_lucia.sql | 19 -- drizzle/20250614083214_lucia.sql | 43 ++++ drizzle/meta/20241104190811_snapshot.json | 103 --------- drizzle/meta/20250614083214_snapshot.json | 261 ++++++++++++++++++++++ drizzle/meta/_journal.json | 4 +- 5 files changed, 306 insertions(+), 124 deletions(-) delete mode 100644 drizzle/20241104190811_lucia.sql create mode 100644 drizzle/20250614083214_lucia.sql delete mode 100644 drizzle/meta/20241104190811_snapshot.json create mode 100644 drizzle/meta/20250614083214_snapshot.json diff --git a/drizzle/20241104190811_lucia.sql b/drizzle/20241104190811_lucia.sql deleted file mode 100644 index 691820a..0000000 --- a/drizzle/20241104190811_lucia.sql +++ /dev/null @@ -1,19 +0,0 @@ -CREATE TABLE IF NOT EXISTS "sessions" ( - "id" text PRIMARY KEY NOT NULL, - "user_id" text NOT NULL, - "expires_at" timestamp with time zone NOT NULL -); ---> statement-breakpoint -CREATE TABLE IF NOT EXISTS "users" ( - "id" text PRIMARY KEY NOT NULL, - "age" integer, - "username" text NOT NULL, - "password_hash" text NOT NULL, - CONSTRAINT "users_username_unique" UNIQUE("username") -); ---> statement-breakpoint -DO $$ BEGIN - ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; -EXCEPTION - WHEN duplicate_object THEN null; -END $$; diff --git a/drizzle/20250614083214_lucia.sql b/drizzle/20250614083214_lucia.sql new file mode 100644 index 0000000..5b65242 --- /dev/null +++ b/drizzle/20250614083214_lucia.sql @@ -0,0 +1,43 @@ +CREATE TABLE "floors" ( + "floor" integer PRIMARY KEY NOT NULL, + "url" text NOT NULL, + "image" text +); +--> statement-breakpoint +CREATE TABLE "plans" ( + "floor" integer PRIMARY KEY NOT NULL, + "plan" json NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sensor_data" ( + "uuid" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "sensor" text NOT NULL, + "temperature" real NOT NULL, + "humidity" real NOT NULL, + "pressure" real NOT NULL, + "altitude" real NOT NULL, + "time" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sensors" ( + "id" text PRIMARY KEY NOT NULL, + "user" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sessions" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "expires_at" timestamp with time zone NOT NULL +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" text PRIMARY KEY NOT NULL, + "age" integer, + "username" text NOT NULL, + "password_hash" text NOT NULL, + CONSTRAINT "users_username_unique" UNIQUE("username") +); +--> statement-breakpoint +ALTER TABLE "sensor_data" ADD CONSTRAINT "sensor_data_sensor_sensors_id_fk" FOREIGN KEY ("sensor") REFERENCES "public"."sensors"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sensors" ADD CONSTRAINT "sensors_user_users_id_fk" FOREIGN KEY ("user") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/meta/20241104190811_snapshot.json b/drizzle/meta/20241104190811_snapshot.json deleted file mode 100644 index 13d527c..0000000 --- a/drizzle/meta/20241104190811_snapshot.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "id": "4b3474a3-7de5-4b99-878e-f839b53c52f7", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.sessions": { - "name": "sessions", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": { - "sessions_user_id_users_id_fk": { - "name": "sessions_user_id_users_id_fk", - "tableFrom": "sessions", - "tableTo": "users", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.users": { - "name": "users", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "age": { - "name": "age", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "username": { - "name": "username", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "password_hash": { - "name": "password_hash", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "users_username_unique": { - "name": "users_username_unique", - "nullsNotDistinct": false, - "columns": ["username"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/drizzle/meta/20250614083214_snapshot.json b/drizzle/meta/20250614083214_snapshot.json new file mode 100644 index 0000000..129a3c6 --- /dev/null +++ b/drizzle/meta/20250614083214_snapshot.json @@ -0,0 +1,261 @@ +{ + "id": "23f11c9d-f98b-4321-aca4-54ec7fc4eece", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.floors": { + "name": "floors", + "schema": "", + "columns": { + "floor": { + "name": "floor", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plans": { + "name": "plans", + "schema": "", + "columns": { + "floor": { + "name": "floor", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "json", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensor_data": { + "name": "sensor_data", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sensor": { + "name": "sensor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "temperature": { + "name": "temperature", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "humidity": { + "name": "humidity", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "pressure": { + "name": "pressure", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "altitude": { + "name": "altitude", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "time": { + "name": "time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sensor_data_sensor_sensors_id_fk": { + "name": "sensor_data_sensor_sensors_id_fk", + "tableFrom": "sensor_data", + "tableTo": "sensors", + "columnsFrom": ["sensor"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sensors": { + "name": "sensors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user": { + "name": "user", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sensors_user_users_id_fk": { + "name": "sensors_user_users_id_fk", + "tableFrom": "sensors", + "tableTo": "users", + "columnsFrom": ["user"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": ["username"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 2c97128..c0eeced 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1730747291671, - "tag": "20241104190811_lucia", + "when": 1749889934943, + "tag": "20250614083214_lucia", "breakpoints": true } ]