/** * Energy / Modbus hooks — typed TanStack Query wrappers for /api/modbus/*. * * All write operations go through the typed apiClient (openapi-fetch), which * injects CSRF via the csrfMiddleware already wired into the client. * * Query-key conventions: * ['modbus-devices'] — device list * ['modbus-device', uuid] — single device * ['modbus-profiles'] — profile list (rarely changes) * ['modbus-latest', uuid] — latest reading per device * ['modbus-metrics', uuid] — profile metric metadata per device * ['modbus-readings', uuid, params] — time-range readings per device * * On success, mutations invalidate the device list so the UI refreshes. */ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import apiClient from '../api/client' import type { components } from '../api/schema.d.ts' // --------------------------------------------------------------------------- // Re-exported types for consumers // --------------------------------------------------------------------------- export type ModbusDevice = components['schemas']['ModbusDeviceResponse'] export type ModbusDeviceCreate = components['schemas']['ModbusDeviceCreate'] export type ModbusDeviceUpdate = components['schemas']['ModbusDeviceUpdate'] export type ProfileSummary = components['schemas']['ProfileSummary'] export type ModbusTestReadResponse = components['schemas']['ModbusTestReadResponse'] export type ModbusLatestResponse = components['schemas']['ModbusLatestResponse'] export type ModbusMetricsResponse = components['schemas']['ModbusMetricsResponse'] export type MetricInfo = components['schemas']['MetricInfo'] export type ModbusReadingResponse = components['schemas']['ModbusReadingResponse'] export type ModbusReadingsResponse = components['schemas']['ModbusReadingsResponse'] // --------------------------------------------------------------------------- // Reading query params // --------------------------------------------------------------------------- export interface ReadingsQueryParams { /** * How many milliseconds of history to show. When provided the queryFn * computes end=now() and start=now()-spanMs on every invocation, so * refetchInterval-triggered re-fetches always use a rolling window. * Mutually exclusive with `start`/`end`. */ spanMs?: number start?: string | null end?: string | null /** Capped server-side; default max is 1000 to avoid pulling full history. */ limit?: number } // --------------------------------------------------------------------------- // Query: list all devices // --------------------------------------------------------------------------- export function useDevices() { return useQuery({ queryKey: ['modbus-devices'], queryFn: async () => { const res = await apiClient.GET('/api/modbus/devices') return res.data }, }) } // --------------------------------------------------------------------------- // Query: list available profiles // --------------------------------------------------------------------------- export function useProfiles() { return useQuery({ queryKey: ['modbus-profiles'], queryFn: async () => { const res = await apiClient.GET('/api/modbus/profiles') return res.data }, // Profiles are static — 5 min stale time. staleTime: 5 * 60 * 1000, }) } // --------------------------------------------------------------------------- // Mutation: create device // --------------------------------------------------------------------------- export function useCreateDevice() { const qc = useQueryClient() return useMutation({ mutationFn: (body: ModbusDeviceCreate) => apiClient.POST('/api/modbus/devices', { body }), onSuccess: () => qc.invalidateQueries({ queryKey: ['modbus-devices'] }), }) } // --------------------------------------------------------------------------- // Mutation: update (PATCH) device // --------------------------------------------------------------------------- export function useUpdateDevice() { const qc = useQueryClient() return useMutation({ mutationFn: ({ uuid, body }: { uuid: string; body: ModbusDeviceUpdate }) => apiClient.PATCH('/api/modbus/devices/{uuid}', { params: { path: { uuid } }, body, }), onSuccess: () => qc.invalidateQueries({ queryKey: ['modbus-devices'] }), }) } // --------------------------------------------------------------------------- // Mutation: delete device // --------------------------------------------------------------------------- export function useDeleteDevice() { const qc = useQueryClient() return useMutation({ mutationFn: (uuid: string) => apiClient.DELETE('/api/modbus/devices/{uuid}', { params: { path: { uuid } }, }), onSuccess: () => qc.invalidateQueries({ queryKey: ['modbus-devices'] }), }) } // --------------------------------------------------------------------------- // Mutation: test-read device (POST /devices/{uuid}/test, does NOT persist) // --------------------------------------------------------------------------- export function useTestReadDevice() { return useMutation({ mutationFn: (uuid: string) => apiClient.POST('/api/modbus/devices/{uuid}/test', { params: { path: { uuid } }, }), }) } // --------------------------------------------------------------------------- // Query options shared by auto-refresh–capable queries // --------------------------------------------------------------------------- export interface AutoRefreshOptions { /** * When provided, TanStack Query will automatically re-fetch this query at * this interval (milliseconds). Pass `undefined` to disable auto-refresh. * Minimum enforced value: 2 000 ms. */ refetchIntervalMs?: number } // --------------------------------------------------------------------------- // Query: latest reading for a device // --------------------------------------------------------------------------- export function useLatestReading(uuid: string, options?: AutoRefreshOptions) { const refetchInterval = options?.refetchIntervalMs != null ? Math.max(2_000, options.refetchIntervalMs) : undefined return useQuery({ queryKey: ['modbus-latest', uuid], queryFn: async () => { const res = await apiClient.GET('/api/modbus/devices/{uuid}/latest', { params: { path: { uuid } }, }) return res.data }, refetchInterval, }) } // --------------------------------------------------------------------------- // Query: profile metric metadata for a device (label/unit per key) // --------------------------------------------------------------------------- export function useMetrics(uuid: string) { return useQuery({ queryKey: ['modbus-metrics', uuid], queryFn: async () => { const res = await apiClient.GET('/api/modbus/devices/{uuid}/metrics', { params: { path: { uuid } }, }) return res.data }, // Metrics are static (tied to profile version); 5 min stale time. staleTime: 5 * 60 * 1000, }) } // --------------------------------------------------------------------------- // Query: time-range readings for a device (window + limit — never full-table) // --------------------------------------------------------------------------- /** Hard upper-bound on readings fetched; prevents accidental full-table pulls. */ const READINGS_MAX_LIMIT = 1000 export function useReadings( uuid: string, params: ReadingsQueryParams, options?: AutoRefreshOptions, ) { const { spanMs, start, end, limit } = params const effectiveLimit = Math.min(limit ?? READINGS_MAX_LIMIT, READINGS_MAX_LIMIT) const refetchInterval = options?.refetchIntervalMs != null ? Math.max(2_000, options.refetchIntervalMs) : undefined // When spanMs is provided, the query key uses the stable span value (not // absolute timestamps), so switching presets triggers a fresh fetch while // refetchInterval-triggered re-fetches reuse the cached key and run the // queryFn again — computing end=now() each time, giving a rolling window. const queryKey = spanMs != null ? ['modbus-readings', uuid, { spanMs, limit: effectiveLimit }] : ['modbus-readings', uuid, { start, end, limit: effectiveLimit }] return useQuery({ queryKey, queryFn: async () => { let resolvedStart = start let resolvedEnd = end if (spanMs != null) { const now = new Date() resolvedEnd = now.toISOString() resolvedStart = new Date(now.getTime() - spanMs).toISOString() } const res = await apiClient.GET('/api/modbus/devices/{uuid}/readings', { params: { path: { uuid }, query: { ...(resolvedStart ? { start: resolvedStart } : {}), ...(resolvedEnd ? { end: resolvedEnd } : {}), limit: effectiveLimit, }, }, }) return res.data }, // Enabled only when we have valid uuid; start/end may be null (full window). enabled: !!uuid, refetchInterval, }) }