From 5d689e6005e6a5c3684973d082324e88d0e862cb Mon Sep 17 00:00:00 2001 From: Yan Zhou Date: Thu, 12 Jun 2025 18:53:02 +0200 Subject: [PATCH 1/3] 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 2/3] 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.