diff --git a/src/pages/Device.tsx b/src/pages/Device.tsx index 6dd2623..55fd8b9 100644 --- a/src/pages/Device.tsx +++ b/src/pages/Device.tsx @@ -3,7 +3,8 @@ import Grid from '@mui/material/Grid2'; import { Component, Device, Interaction, User } from "models"; import { getContextKeys, getContextTypes } from "pbHelpers/Context"; import { useDeviceAPI, useGlobalState, useSnackbar } from "providers"; -import { useEffect, useMemo, useState } from "react"; +import { useDeviceStatusStreams } from "hooks/useDeviceStatusStreams"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useLocation, useParams } from "react-router-dom"; import PageContainer from "./PageContainer"; import { useHTTP, useMobile } from "hooks"; @@ -23,6 +24,25 @@ import DeviceWizard from "device/DeviceWizard"; import DeviceScannedComponents from "device/autoDetect/DeviceScannedComponents"; import { useWebSocket } from "hooks/useWebSocket"; +type TimestampedReading = { timestamp?: string }; + +function getTimestampMillis(timestamp?: string): number | undefined { + if (!timestamp) return undefined; + const parsed = Date.parse(timestamp); + return Number.isNaN(parsed) ? undefined : parsed; +} + +function pickNewestReading( + current: T | null | undefined, + incoming: T | null | undefined +): T | null | undefined { + const currentTs = getTimestampMillis(current?.timestamp); + const incomingTs = getTimestampMillis(incoming?.timestamp); + if (incomingTs === undefined) return current ?? incoming; + if (currentTs === undefined) return incoming ?? current; + return incomingTs >= currentTs ? incoming : current; +} + export interface DevicePageData { device: Device; components: Component[]; @@ -76,6 +96,55 @@ export default function DevicePage() { ? as : undefined; + const liveStatusDeviceIds = useMemo( + () => (deviceID ? [deviceID] : []), + [deviceID] + ); + + const handleLiveDeviceStatus = useCallback( + (streamDeviceId: string, status: pond.DeviceStatus) => { + setDevice((prev) => { + if (prev.id().toString() !== streamDeviceId) return prev; + const updated = Device.clone(prev); + const incomingStatus = pond.DeviceStatus.fromObject(status); + const currentStatus = prev.status ?? pond.DeviceStatus.create(); + + incomingStatus.plenum = pickNewestReading(currentStatus.plenum, incomingStatus.plenum); + incomingStatus.sen5x = pickNewestReading(currentStatus.sen5x, incomingStatus.sen5x); + incomingStatus.co = pickNewestReading(currentStatus.co, incomingStatus.co); + incomingStatus.co2 = pickNewestReading(currentStatus.co2, incomingStatus.co2); + incomingStatus.no2 = pickNewestReading(currentStatus.no2, incomingStatus.no2); + incomingStatus.o2 = pickNewestReading(currentStatus.o2, incomingStatus.o2); + incomingStatus.lel = pickNewestReading(currentStatus.lel, incomingStatus.lel); + incomingStatus.h2s = pickNewestReading(currentStatus.h2s, incomingStatus.h2s); + + const currentLastActive = getTimestampMillis(currentStatus.lastActive); + const incomingLastActive = getTimestampMillis(incomingStatus.lastActive); + if ( + currentLastActive !== undefined && + incomingLastActive !== undefined && + incomingLastActive < currentLastActive + ) { + incomingStatus.lastActive = currentStatus.lastActive; + } + + updated.status = incomingStatus; + return updated; + }); + }, + [] + ); + + useDeviceStatusStreams({ + deviceIds: liveStatusDeviceIds, + onStatusUpdate: handleLiveDeviceStatus, + token, + keys: liveContextKeys, + types: liveContextTypes, + as: liveAs, + enabled: Boolean(deviceID && token), + }); + const loadDevice = () => { if (loading) return if (state?.devicePageData) { @@ -212,35 +281,6 @@ export default function DevicePage() { enabled: !!deviceID, }); - // --- Real-time device updates (optional) --- - // Updates device-level info (name, status, lastActive, etc.) - useWebSocket({ - path: `/v1/live/devices/${deviceID}`, - parse: (e) => { - try { - const raw = JSON.parse(e.data); - const dev = Device.any(raw); - if (!dev?.settings) { - console.warn("[ws] received device without settings:", raw); - return null; - } - return dev; - } catch (err) { - console.warn("[ws] failed to parse device:", err); - return null; - } - }, - onMessage: (updatedDevice) => { - if (!updatedDevice) return; - setDevice(updatedDevice); - }, - keys: liveContextKeys, - types: liveContextTypes, - as: liveAs, - token, - enabled: !!deviceID, - }); - const loadPortScan = () => { deviceAPI.listFoundComponents(parseInt(deviceID)) .then(resp => {