diff --git a/package.json b/package.json
index 75383b6..fb9ac4e 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,8 @@
"@node-rs/argon2": "^2.0.2",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.2.0",
"drizzle-orm": "^0.43.1",
"mqtt": "^5.13.1",
"postgres": "^3.4.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7918f60..c8adcf8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -17,6 +17,12 @@ importers:
'@oslojs/encoding':
specifier: ^1.1.0
version: 1.1.0
+ d3-scale:
+ specifier: ^4.0.2
+ version: 4.0.2
+ d3-shape:
+ specifier: ^3.2.0
+ version: 3.2.0
drizzle-orm:
specifier: ^0.43.1
version: 0.43.1(postgres@3.4.5)
diff --git a/src/lib/server/db/schema.js b/src/lib/server/db/schema.js
index 0ae02ed..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,4 +42,5 @@ export const sensorData = pgTable("sensor_data", {
humidity: real().notNull(),
pressure: real().notNull(),
altitude: real().notNull(),
+ time: timestamp({ withTimezone: true, mode: "date" }).notNull().defaultNow(),
});
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 4488322..8ce7412 100644
--- a/src/routes/(app)/[slug]/+page.server.ts
+++ b/src/routes/(app)/[slug]/+page.server.ts
@@ -3,35 +3,74 @@ 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 }) => {
- {
- 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);
+
+ // 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,
+ 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..6491725 100644
--- a/src/routes/(app)/[slug]/+page.svelte
+++ b/src/routes/(app)/[slug]/+page.svelte
@@ -1,26 +1,59 @@
-
-
Floor {data.slug}
-
{mqttMessage}
-
+
+
+
Floor {data.slug}
+
+ {#if data.hasConfig && data.floorConfig}
+
+
+ {#if data.floorConfig.image}
+
+

+
+ {#if data.floorConfig.devices}
+ {#each data.floorConfig.devices as device}
+
+ {/each}
+ {/if}
+
+
+
+
MQTT Status: {mqttMessage}
+
+ {/if}
+
+ {:else}
+
+
+ {mqttMessage}
+
+
+ {/if}
+
+ Statistics
+
+
+
+ Statistics for floor {data.slug}
+ Showing total data for the last 3 months
+
+
+
+
+
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+
+ {#each sensorOptions as sensor}
+
+ {sensor.label}
+
+ {/each}
+
+
+
+
+
+
+ {selectedLabel}
+
+
+ Last 3 months
+ Last 30 days
+ Last 7 days
+
+
+
+
+
+
+ {#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}
+
+
+
+
+
+
+
+
+
+
+ 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
new file mode 100644
index 0000000..fc68394
--- /dev/null
+++ b/src/routes/(app)/[slug]/stats/+server.ts
@@ -0,0 +1,28 @@
+import { db } from "$lib/server/db";
+import * as table from "$lib/server/db/schema"
+
+export const GET = async () => {
+ // 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: {
+ "Content-Type": "application/json"
+ }
+ });
+}
diff --git a/src/routes/(app)/settings/+page.server.js b/src/routes/(app)/settings/+page.server.js
index 365a6b8..1939028 100644
--- a/src/routes/(app)/settings/+page.server.js
+++ b/src/routes/(app)/settings/+page.server.js
@@ -1,10 +1,35 @@
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";
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);
+
+ // 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),
+ devices: devices,
+ };
};
export const actions = {
@@ -41,4 +66,52 @@ 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);
+ 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
+ .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..9b57069 100644
--- a/src/routes/(app)/settings/+page.svelte
+++ b/src/routes/(app)/settings/+page.svelte
@@ -1,5 +1,6 @@