Compare commits
No commits in common. "fae128bc1bbb12c47416eefe394850246391d04d" and "5935064606f1d526e68a50ccf5ceeb505ed37893" have entirely different histories.
fae128bc1b
...
5935064606
10 changed files with 63 additions and 874 deletions
|
@ -68,8 +68,6 @@
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"@oslojs/crypto": "^1.0.1",
|
"@oslojs/crypto": "^1.0.1",
|
||||||
"@oslojs/encoding": "^1.1.0",
|
"@oslojs/encoding": "^1.1.0",
|
||||||
"d3-scale": "^4.0.2",
|
|
||||||
"d3-shape": "^3.2.0",
|
|
||||||
"drizzle-orm": "^0.43.1",
|
"drizzle-orm": "^0.43.1",
|
||||||
"mqtt": "^5.13.1",
|
"mqtt": "^5.13.1",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
|
|
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
|
@ -17,12 +17,6 @@ importers:
|
||||||
'@oslojs/encoding':
|
'@oslojs/encoding':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 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:
|
drizzle-orm:
|
||||||
specifier: ^0.43.1
|
specifier: ^0.43.1
|
||||||
version: 0.43.1(postgres@3.4.5)
|
version: 0.43.1(postgres@3.4.5)
|
||||||
|
|
|
@ -42,5 +42,4 @@ export const sensorData = pgTable("sensor_data", {
|
||||||
humidity: real().notNull(),
|
humidity: real().notNull(),
|
||||||
pressure: real().notNull(),
|
pressure: real().notNull(),
|
||||||
altitude: real().notNull(),
|
altitude: real().notNull(),
|
||||||
time: timestamp().notNull().defaultNow(),
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
// 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;
|
|
||||||
}
|
|
|
@ -3,74 +3,35 @@ import * as table from "$lib/server/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import type { PageServerLoad } from "./$types";
|
import type { PageServerLoad } from "./$types";
|
||||||
import { connect } from "mqtt";
|
import { connect } from "mqtt";
|
||||||
import { getDeviceSensorData } from "$lib/server/mqtt-devices.js";
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
// Convert slug to number for floor lookup
|
{
|
||||||
const floorNumber = Number(params.slug);
|
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) {
|
||||||
// First check if we have a saved floor configuration in the floors table
|
await db.insert(table.plans).values({ floor: params.slug, plan: {
|
||||||
const floorData = await db.select({
|
"regions": [
|
||||||
floor: table.floors.floor,
|
{ "start": { "x": 100, "y": 100 }, "end": { "x": 400, "y": 100 } },
|
||||||
url: table.floors.url
|
{ "start": { "x": 400, "y": 100 }, "end": { "x": 400, "y": 300 } },
|
||||||
}).from(table.floors).where(eq(table.floors.floor, floorNumber));
|
{ "start": { "x": 400, "y": 300 }, "end": { "x": 100, "y": 300 } },
|
||||||
|
{ "start": { "x": 100, "y": 300 }, "end": { "x": 100, "y": 100 } }
|
||||||
if (floorData.length > 0 && floorData[0].url && floorData[0].url !== "/") {
|
],
|
||||||
try {
|
"doors": [
|
||||||
// Try to parse the saved configuration
|
{ "location": { "x": 240, "y": 100 }, "width": 50, "rotation": 0 }
|
||||||
const config = JSON.parse(floorData[0].url);
|
],
|
||||||
|
"furnitures": [
|
||||||
// Add real sensor data to devices
|
{
|
||||||
if (config.devices) {
|
"minBound": { "x": 150, "y": 150 },
|
||||||
config.devices = config.devices.map(device => {
|
"maxBound": { "x": 200, "y": 200 },
|
||||||
const sensorData = getDeviceSensorData(device.id);
|
"equipName": "Table",
|
||||||
return {
|
"xPlacement": 150,
|
||||||
...device,
|
"yPlacement": 150,
|
||||||
sensorData: sensorData
|
"rotation": 0
|
||||||
};
|
}
|
||||||
});
|
]
|
||||||
}
|
}});
|
||||||
|
|
||||||
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));
|
const floor_ = await db.select({ floor: table.plans.floor, json: table.plans.plan }).from(table.plans).where(eq(table.plans.floor, params.slug));
|
||||||
return {
|
return { slug: params.slug, floor: floor_ };
|
||||||
slug: params.slug,
|
|
||||||
floor: floor_,
|
|
||||||
hasConfig: false
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,59 +1,26 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { Thermometer, Droplets, Gauge, Mountain, Cpu } from "@lucide/svelte";
|
|
||||||
import { Button, buttonVariants } from "$lib/components/ui/button/index.js";
|
|
||||||
import { Input } from "$lib/components/ui/input/index.js";
|
|
||||||
import { Label } from "$lib/components/ui/label/index.js";
|
|
||||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
|
||||||
|
|
||||||
const { data } = $props();
|
const { data } = $props();
|
||||||
|
|
||||||
let mqttMessage = $state("Waiting for MQTT message...");
|
let mqttMessage = $state("Waiting for MQTT message...");
|
||||||
let eventSource;
|
let eventSource;
|
||||||
let deviceSensorData = $state(new Map());
|
|
||||||
let refreshInterval;
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Connect to the SSE endpoint
|
// Connect to the SSE endpoint
|
||||||
console.log("Attempting to connect to MQTT SSE...");
|
|
||||||
eventSource = new EventSource("/mqtt");
|
eventSource = new EventSource("/mqtt");
|
||||||
|
|
||||||
eventSource.onopen = () => {
|
eventSource.onopen = () => {
|
||||||
console.log("SSE connection opened successfully!");
|
console.log("SSE connection opened.");
|
||||||
};
|
};
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
eventSource.onmessage = (event) => {
|
||||||
if (eventSource.readyState !== EventSource.OPEN) {
|
|
||||||
console.log("Floor page: Received message but connection is not open, ignoring");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const messageData = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
console.log("Floor page received SSE data:", messageData);
|
mqttMessage = data.message;
|
||||||
|
console.log("Received SSE message:", mqttMessage);
|
||||||
// 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) {
|
} catch (e) {
|
||||||
console.error("Floor page: Error parsing SSE message:", e, "Raw data:", event.data);
|
console.error("Error parsing SSE message:", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -64,16 +31,13 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (eventSource && eventSource.readyState !== EventSource.CLOSED) {
|
if (eventSource) {
|
||||||
console.log("Floor page: Closing SSE connection");
|
console.log("Closing SSE connection.");
|
||||||
eventSource.close();
|
eventSource.close(); // Clean up the connection when the component is destroyed
|
||||||
}
|
|
||||||
if (refreshInterval) {
|
|
||||||
clearInterval(refreshInterval);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let canvasEl = $state<HTMLCanvasElement>();
|
let canvasEl: HTMLCanvasElement;
|
||||||
let ctx;
|
let ctx;
|
||||||
|
|
||||||
function drawFloorplan(data) {
|
function drawFloorplan(data) {
|
||||||
|
@ -117,297 +81,14 @@
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!data.hasConfig && data.floor?.[0]?.json) {
|
ctx = canvasEl.getContext("2d");
|
||||||
ctx = canvasEl.getContext("2d");
|
drawFloorplan(data.floor[0].json);
|
||||||
drawFloorplan(data.floor[0].json);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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 { 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([
|
|
||||||
{ 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 timeRange = $state("90d");
|
|
||||||
|
|
||||||
const selectedLabel = $derived.by(() => {
|
|
||||||
switch (timeRange) {
|
|
||||||
case "90d":
|
|
||||||
return "Last 3 months";
|
|
||||||
case "30d":
|
|
||||||
return "Last 30 days";
|
|
||||||
case "7d":
|
|
||||||
return "Last 7 days";
|
|
||||||
default:
|
|
||||||
return "Last 3 months";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredData = $derived(
|
|
||||||
chartData.filter((item) => {
|
|
||||||
const referenceDate = new Date("2024-06-30");
|
|
||||||
let daysToSubtract = 90;
|
|
||||||
if (timeRange === "30d") {
|
|
||||||
daysToSubtract = 30;
|
|
||||||
} else if (timeRange === "7d") {
|
|
||||||
daysToSubtract = 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
referenceDate.setDate(referenceDate.getDate() - daysToSubtract);
|
|
||||||
return item.date >= referenceDate;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const chartConfig = {
|
|
||||||
desktop: { label: "Desktop", color: "var(--chart-1)" },
|
|
||||||
mobile: { label: "Mobile", color: "var(--chart-2)" },
|
|
||||||
} satisfies Chart.ChartConfig;
|
|
||||||
|
|
||||||
const table = $state([
|
|
||||||
{ visible: true, id: "temperature" },
|
|
||||||
{ visible: true, id: "altitude" },
|
|
||||||
{ visible: true, id: "humidity" },
|
|
||||||
]);
|
|
||||||
|
|
||||||
let onOpenChange = $state(async (open: boolean) => {
|
|
||||||
if (open) {
|
|
||||||
const response = await fetch("/" + data.slug + "/stats");
|
|
||||||
const number = await response.json();
|
|
||||||
console.log(number);
|
|
||||||
|
|
||||||
chartData = [];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container mx-auto p-6">
|
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
||||||
<!-- <div style="display: flex; justify-content: center; align-items: center; height: 100%;"> -->
|
<h1 class="text-center text-4xl font-bold">Floor {data.slug}</h1>
|
||||||
<h1 class="mb-6 text-center text-4xl font-bold">Floor {data.slug}</h1>
|
<span>{mqttMessage}</span>
|
||||||
|
<canvas bind:this={canvasEl} class="obj-contain" width="800%" height="600%"></canvas>
|
||||||
{#if data.hasConfig && data.floorConfig}
|
|
||||||
<!-- Display saved floor plan configuration -->
|
|
||||||
<div class="relative mx-auto max-w-4xl">
|
|
||||||
{#if data.floorConfig.image}
|
|
||||||
<div class="relative overflow-hidden rounded-lg border bg-gray-50">
|
|
||||||
<img src={data.floorConfig.image} alt="Floor plan" class="h-auto w-full" />
|
|
||||||
|
|
||||||
{#if data.floorConfig.devices}
|
|
||||||
{#each data.floorConfig.devices as device}
|
|
||||||
<div
|
|
||||||
class="absolute -translate-x-1/2 -translate-y-1/2 transform rounded-lg border-2 border-blue-500 bg-white p-3 shadow-lg"
|
|
||||||
style="left: {device.x}%; top: {device.y}%;"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Cpu class="h-5 w-5 text-blue-600" />
|
|
||||||
<span class="text-xs font-medium">{device.name}</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-2 grid grid-cols-2 gap-1">
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<Thermometer class="h-3 w-3 text-orange-500" />
|
|
||||||
<span class="text-xs">
|
|
||||||
{#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}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<Droplets class="h-3 w-3 text-blue-500" />
|
|
||||||
<span class="text-xs">
|
|
||||||
{#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}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<Gauge class="h-3 w-3 text-green-500" />
|
|
||||||
<span class="text-xs">
|
|
||||||
{#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}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<Mountain class="h-3 w-3 text-purple-500" />
|
|
||||||
<span class="text-xs">
|
|
||||||
{#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}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 rounded-lg bg-gray-100 p-4">
|
|
||||||
<p class="text-sm text-gray-600">MQTT Status: {mqttMessage}</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<!-- Fallback to canvas drawing -->
|
|
||||||
<div class="flex flex-col items-center">
|
|
||||||
<span class="mb-4">{mqttMessage}</span>
|
|
||||||
<canvas bind:this={canvasEl} class="border" width="800" height="600"></canvas>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<Dialog.Root bind:onOpenChange>
|
|
||||||
<Dialog.Trigger class={buttonVariants({ variant: "outline" })}>Statistics</Dialog.Trigger>
|
|
||||||
<Dialog.Content class="sm:max-h-2/3 sm:max-w-2/3">
|
|
||||||
<Dialog.Header>
|
|
||||||
<DropdownMenu.Root>
|
|
||||||
<DropdownMenu.Trigger>
|
|
||||||
{#snippet child({ props })}
|
|
||||||
<Button variant="outline" size="sm" {...props}>
|
|
||||||
<Columns2 />
|
|
||||||
<span class="hidden lg:inline">Customize Columns</span>
|
|
||||||
<span class="lg:hidden">Columns</span>
|
|
||||||
<ChevronDownIcon />
|
|
||||||
</Button>
|
|
||||||
{/snippet}
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
<DropdownMenu.Content align="end" class="w-56">
|
|
||||||
{#each table as column}
|
|
||||||
<DropdownMenu.CheckboxItem
|
|
||||||
class="capitalize"
|
|
||||||
checked={column.visible}
|
|
||||||
onCheckedChange={(value) => (column.visible = !value)}
|
|
||||||
>
|
|
||||||
{column.id}
|
|
||||||
</DropdownMenu.CheckboxItem>
|
|
||||||
{/each}
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
<div class="grid flex-1 gap-1 text-center sm:text-left">
|
|
||||||
<Card.Title>Data for floor {data.slug}</Card.Title>
|
|
||||||
<Card.Description>Showing total data for the last 3 months</Card.Description>
|
|
||||||
</div>
|
|
||||||
<Select.Root type="single" bind:value={timeRange}>
|
|
||||||
<Select.Trigger class="w-[160px] rounded-lg sm:ml-auto" aria-label="Select a value">
|
|
||||||
{selectedLabel}
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content class="rounded-xl">
|
|
||||||
<Select.Item value="90d" class="rounded-lg">Last 3 months</Select.Item>
|
|
||||||
<Select.Item value="30d" class="rounded-lg">Last 30 days</Select.Item>
|
|
||||||
<Select.Item value="7d" class="rounded-lg">Last 7 days</Select.Item>
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
</Dialog.Header>
|
|
||||||
|
|
||||||
<ChartContainer config={chartConfig} class="aspect-auto h-[250px] w-full">
|
|
||||||
<AreaChart
|
|
||||||
legend
|
|
||||||
data={filteredData}
|
|
||||||
x="date"
|
|
||||||
xScale={scaleUtc()}
|
|
||||||
series={[
|
|
||||||
{
|
|
||||||
key: "mobile",
|
|
||||||
label: "Mobile",
|
|
||||||
color: chartConfig.mobile.color,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "desktop",
|
|
||||||
label: "Desktop",
|
|
||||||
color: chartConfig.desktop.color,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
seriesLayout="stack"
|
|
||||||
props={{
|
|
||||||
area: {
|
|
||||||
curve: curveNatural,
|
|
||||||
"fill-opacity": 0.4,
|
|
||||||
line: { class: "stroke-1" },
|
|
||||||
motion: "tween",
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
ticks: timeRange === "7d" ? 7 : undefined,
|
|
||||||
format: (v) => {
|
|
||||||
return v.toLocaleDateString("en-US", {
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
yAxis: { format: () => "" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#snippet marks({ series, getAreaProps })}
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="5%" stop-color="var(--color-desktop)" stop-opacity={1.0} />
|
|
||||||
<stop offset="95%" stop-color="var(--color-desktop)" stop-opacity={0.1} />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="fillMobile" x1="0" y1="0" x2="0" y2="1">
|
|
||||||
<stop offset="5%" stop-color="var(--color-mobile)" stop-opacity={0.8} />
|
|
||||||
<stop offset="95%" stop-color="var(--color-mobile)" stop-opacity={0.1} />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<ChartClipPath
|
|
||||||
initialWidth={0}
|
|
||||||
motion={{
|
|
||||||
width: { type: "tween", duration: 1000, easing: cubicInOut },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#each series as s, i (s.key)}
|
|
||||||
<Area
|
|
||||||
{...getAreaProps(s, i)}
|
|
||||||
fill={s.key === "desktop" ? "url(#fillDesktop)" : "url(#fillMobile)"}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</ChartClipPath>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet tooltip()}
|
|
||||||
<Chart.Tooltip
|
|
||||||
labelFormatter={(v: Date) => {
|
|
||||||
return v.toLocaleDateString("en-US", {
|
|
||||||
month: "long",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
indicator="line"
|
|
||||||
/>
|
|
||||||
{/snippet}
|
|
||||||
</AreaChart>
|
|
||||||
</ChartContainer>
|
|
||||||
|
|
||||||
<Dialog.Footer></Dialog.Footer>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Root>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
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"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,35 +1,10 @@
|
||||||
import { db } from "$lib/server/db";
|
import { db } from "$lib/server/db";
|
||||||
import * as table from "$lib/server/db/schema";
|
import * as table from "$lib/server/db/schema";
|
||||||
import { getCurrentDevices } from "$lib/server/mqtt-devices.js";
|
|
||||||
import { fail } from "@sveltejs/kit";
|
import { fail } from "@sveltejs/kit";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export const load = async (event) => {
|
export const load = async (event) => {
|
||||||
// Fetch all available floors
|
return {};
|
||||||
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 = {
|
export const actions = {
|
||||||
|
@ -66,49 +41,4 @@ export const actions = {
|
||||||
|
|
||||||
await db.delete(table.floors).where(eq(table.floors.floor, n));
|
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!" });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { enhance } from "$app/forms";
|
import { enhance } from "$app/forms";
|
||||||
import { onMount, onDestroy } from "svelte";
|
|
||||||
import Button from "$lib/components/ui/button/button.svelte";
|
import Button from "$lib/components/ui/button/button.svelte";
|
||||||
import Input from "$lib/components/ui/input/input.svelte";
|
import Input from "$lib/components/ui/input/input.svelte";
|
||||||
import Label from "$lib/components/ui/label/label.svelte";
|
import Label from "$lib/components/ui/label/label.svelte";
|
||||||
|
@ -11,83 +10,19 @@
|
||||||
import CardTitle from "$lib/components/ui/card/card-title.svelte";
|
import CardTitle from "$lib/components/ui/card/card-title.svelte";
|
||||||
import { Thermometer, Droplets, Gauge, Mountain, Upload, Cpu } from "@lucide/svelte";
|
import { Thermometer, Droplets, Gauge, Mountain, Upload, Cpu } from "@lucide/svelte";
|
||||||
|
|
||||||
const { form, data } = $props();
|
const { form } = $props();
|
||||||
|
|
||||||
let floorPlanImage = $state(null);
|
let floorPlanImage = $state(null);
|
||||||
let availableDevices = $state(data.devices || []);
|
let availableDevices = $state([
|
||||||
let deviceSensorData = $state(new Map());
|
{ id: "esp8266-1", name: "ESP8266 #1", type: "esp8266", status: "online" },
|
||||||
let eventSource;
|
{ 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 placedDevices = $state([]);
|
let placedDevices = $state([]);
|
||||||
let draggedDevice = $state(null);
|
let draggedDevice = $state(null);
|
||||||
let floorPlanRef = $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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up real-time updates via SSE
|
|
||||||
onMount(() => {
|
|
||||||
console.log("Settings: Attempting to connect to MQTT SSE...");
|
|
||||||
eventSource = new EventSource("/mqtt");
|
|
||||||
|
|
||||||
eventSource.onopen = () => {
|
|
||||||
console.log("Settings: SSE connection opened successfully!");
|
|
||||||
};
|
|
||||||
|
|
||||||
eventSource.onmessage = (event) => {
|
|
||||||
if (eventSource.readyState !== EventSource.OPEN) {
|
|
||||||
console.log("Settings: Received message but connection is not open, ignoring");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
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) {
|
|
||||||
newData.set(device.id, device.sensorData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
deviceSensorData = newData;
|
|
||||||
console.log("Updated deviceSensorData:", deviceSensorData);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Settings: Error parsing SSE message:", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
eventSource.onerror = (error) => {
|
|
||||||
console.error("SSE Error:", error);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
if (eventSource && eventSource.readyState !== EventSource.CLOSED) {
|
|
||||||
console.log("Settings: Closing SSE connection");
|
|
||||||
eventSource.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleFileUpload(event) {
|
function handleFileUpload(event) {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
|
@ -233,65 +168,6 @@
|
||||||
<p class="mt-2 text-sm text-gray-600">
|
<p class="mt-2 text-sm text-gray-600">
|
||||||
Drag ESP8266 devices from below onto the floor plan to place them in specific rooms.
|
Drag ESP8266 devices from below onto the floor plan to place them in specific rooms.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if floorPlanImage && placedDevices.length > 0}
|
|
||||||
<div class="mt-4 rounded-lg border bg-blue-50 p-4">
|
|
||||||
<h4 class="mb-3 font-semibold">Save Floor Configuration</h4>
|
|
||||||
<form
|
|
||||||
method="post"
|
|
||||||
action="?/savefloor"
|
|
||||||
use:enhance={() => {
|
|
||||||
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",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<input type="hidden" name="floorPlanImage" value={floorPlanImage} />
|
|
||||||
<input type="hidden" name="devices" value={JSON.stringify(placedDevices)} />
|
|
||||||
<div class="mb-3 flex items-end gap-4">
|
|
||||||
<div class="flex-1">
|
|
||||||
<Label for="floorNumber">Select Floor</Label>
|
|
||||||
<select
|
|
||||||
name="floorNumber"
|
|
||||||
id="floorNumber"
|
|
||||||
bind:value={selectedFloorNumber}
|
|
||||||
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
{#if data.floors && data.floors.length > 0}
|
|
||||||
{#each data.floors as floor}
|
|
||||||
<option value={floor.toString()}>
|
|
||||||
Floor {floor}
|
|
||||||
</option>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<Button type="submit" variant="default" disabled={!selectedFloorNumber}>
|
|
||||||
Save Floor Configuration
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{#if saveMessage}
|
|
||||||
<p
|
|
||||||
class="mt-2 text-sm {saveMessage.type === 'success'
|
|
||||||
? 'text-green-600'
|
|
||||||
: 'text-red-600'}"
|
|
||||||
>
|
|
||||||
{saveMessage.text}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
|
@ -324,43 +200,19 @@
|
||||||
<div class="grid grid-cols-2 gap-1">
|
<div class="grid grid-cols-2 gap-1">
|
||||||
<div class="flex items-center gap-1 text-xs">
|
<div class="flex items-center gap-1 text-xs">
|
||||||
<Thermometer class="h-3 w-3 text-orange-500" />
|
<Thermometer class="h-3 w-3 text-orange-500" />
|
||||||
<span>
|
<span>Temperature</span>
|
||||||
{#if device.sensorData}
|
|
||||||
{device.sensorData.temperature.toFixed(1)}°C
|
|
||||||
{:else}
|
|
||||||
--°C
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 text-xs">
|
<div class="flex items-center gap-1 text-xs">
|
||||||
<Droplets class="h-3 w-3 text-blue-500" />
|
<Droplets class="h-3 w-3 text-blue-500" />
|
||||||
<span>
|
<span>Humidity</span>
|
||||||
{#if device.sensorData}
|
|
||||||
{device.sensorData.humidity.toFixed(1)}%
|
|
||||||
{:else}
|
|
||||||
--%
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 text-xs">
|
<div class="flex items-center gap-1 text-xs">
|
||||||
<Gauge class="h-3 w-3 text-green-500" />
|
<Gauge class="h-3 w-3 text-green-500" />
|
||||||
<span>
|
<span>Pressure</span>
|
||||||
{#if device.sensorData}
|
|
||||||
{Math.round(device.sensorData.pressure)}Pa
|
|
||||||
{:else}
|
|
||||||
--Pa
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 text-xs">
|
<div class="flex items-center gap-1 text-xs">
|
||||||
<Mountain class="h-3 w-3 text-purple-500" />
|
<Mountain class="h-3 w-3 text-purple-500" />
|
||||||
<span>
|
<span>Altitude</span>
|
||||||
{#if device.sensorData}
|
|
||||||
{device.sensorData.altitude.toFixed(1)}m
|
|
||||||
{:else}
|
|
||||||
--m
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
// src/routes/mqtt/+server.js
|
// 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";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
// A Svelte store to hold the latest MQTT message.
|
// A Svelte store to hold the latest MQTT message.
|
||||||
|
@ -11,85 +10,6 @@ const devices = writable([]);
|
||||||
|
|
||||||
let client = null;
|
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,
|
|
||||||
type: "esp8266",
|
|
||||||
status: "online",
|
|
||||||
lastSeen: now,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update sensor data if provided
|
|
||||||
if (sensorData) {
|
|
||||||
deviceSensorData.set(deviceId, {
|
|
||||||
...sensorData,
|
|
||||||
timestamp: now,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the devices store
|
|
||||||
updateDevicesStore();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to parse sensor data from MQTT payload
|
|
||||||
function parseSensorData(payload) {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(payload);
|
|
||||||
return {
|
|
||||||
temperature: parseFloat(data.temperature) || 0,
|
|
||||||
humidity: parseFloat(data.humidity?.replace("%", "")) || 0,
|
|
||||||
pressure: parseFloat(data.pressure) || 0,
|
|
||||||
altitude: parseFloat(data.altitude) || 0,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error parsing sensor data:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
return {
|
|
||||||
...device,
|
|
||||||
sensorData: sensorData || null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
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;
|
|
||||||
|
|
||||||
for (const [deviceId, device] of connectedDevices.entries()) {
|
|
||||||
if (device.lastSeen < fiveMinutesAgo && device.status === "online") {
|
|
||||||
connectedDevices.set(deviceId, { ...device, status: "offline" });
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
updateDevicesStore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run cleanup every minute
|
|
||||||
setInterval(cleanupDevices, 60000);
|
|
||||||
|
|
||||||
// getCurrentDevices is now imported from the shared module
|
|
||||||
|
|
||||||
// Function to connect to MQTT
|
// Function to connect to MQTT
|
||||||
function connectMqtt() {
|
function connectMqtt() {
|
||||||
if (client && client.connected) {
|
if (client && client.connected) {
|
||||||
|
@ -98,7 +18,6 @@ function connectMqtt() {
|
||||||
|
|
||||||
// Replace with your MQTT broker details
|
// Replace with your MQTT broker details
|
||||||
const BROKER_URL = "mqtt://kada49.it:1883"; // Example public broker
|
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 TOPIC = "esp8266/+/data"; // Replace with your desired topic
|
||||||
|
|
||||||
client = mqtt.connect(BROKER_URL);
|
client = mqtt.connect(BROKER_URL);
|
||||||
|
@ -118,23 +37,7 @@ function connectMqtt() {
|
||||||
const payload = message.toString();
|
const payload = message.toString();
|
||||||
console.log(`Received message from topic "${topic}": ${payload}`);
|
console.log(`Received message from topic "${topic}": ${payload}`);
|
||||||
latestMessage.set(payload); // Update the Svelte store
|
latestMessage.set(payload); // Update the Svelte store
|
||||||
|
console.log(topic.split("/")[1]);
|
||||||
// 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);
|
|
||||||
} else {
|
|
||||||
// Still update device as online even if data parsing failed
|
|
||||||
updateDevice(deviceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("error", (err) => {
|
client.on("error", (err) => {
|
||||||
|
@ -167,98 +70,24 @@ export async function GET({ request }) {
|
||||||
return new Response(
|
return new Response(
|
||||||
new ReadableStream({
|
new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
console.log("Client connected to MQTT SSE stream");
|
console.log("Client connected to SSE stream.");
|
||||||
|
|
||||||
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;
|
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
interval = null;
|
|
||||||
}
|
|
||||||
sseControllers.delete(controller);
|
|
||||||
if (unsubscribe) {
|
|
||||||
unsubscribe();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send current device data
|
|
||||||
function sendDevices() {
|
|
||||||
if (!isConnected) {
|
|
||||||
cleanup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const devices = getCurrentDevices();
|
|
||||||
const message = {
|
|
||||||
devices: devices,
|
|
||||||
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")
|
|
||||||
) {
|
|
||||||
console.log("MQTT SSE controller closed, stopping interval");
|
|
||||||
} else {
|
|
||||||
console.error("Error sending MQTT device data:", error);
|
|
||||||
}
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) => {
|
const unsubscribe = latestMessage.subscribe((message) => {
|
||||||
if (message !== "No message yet" && isConnected) {
|
if (message !== "No message yet") {
|
||||||
try {
|
const data = `data: ${JSON.stringify({ message: message })}\n\n`;
|
||||||
const data = `data: ${JSON.stringify({
|
controller.enqueue(data);
|
||||||
message: message,
|
|
||||||
devices: getCurrentDevices(),
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
})}\n\n`;
|
|
||||||
if (isConnected) {
|
|
||||||
controller.enqueue(data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (
|
|
||||||
error.code === "ERR_INVALID_STATE" ||
|
|
||||||
error.message.includes("Controller is already closed")
|
|
||||||
) {
|
|
||||||
console.log("MQTT SSE controller closed during message send");
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle client disconnection
|
// Handle client disconnection
|
||||||
request.signal.addEventListener("abort", () => {
|
request.signal.addEventListener("abort", () => {
|
||||||
console.log("Client disconnected from MQTT SSE stream");
|
console.log("Client disconnected from SSE stream.");
|
||||||
cleanup();
|
unsubscribe(); // Stop listening to store updates
|
||||||
|
controller.close();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
console.log("MQTT SSE stream cancelled");
|
console.log("SSE stream cancelled.");
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{ headers },
|
{ headers },
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue