2026-06-22 13:52:33 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* 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)
|
2026-06-22 14:17:59 +02:00
|
|
|
|
* ['modbus-latest', uuid] — latest reading per device
|
|
|
|
|
|
* ['modbus-metrics', uuid] — profile metric metadata per device
|
|
|
|
|
|
* ['modbus-readings', uuid, params] — time-range readings per device
|
2026-06-22 13:52:33 +02:00
|
|
|
|
*
|
|
|
|
|
|
* 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']
|
2026-06-22 14:17:59 +02:00
|
|
|
|
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 {
|
|
|
|
|
|
start?: string | null
|
|
|
|
|
|
end?: string | null
|
|
|
|
|
|
/** Capped server-side; default max is 1000 to avoid pulling full history. */
|
|
|
|
|
|
limit?: number
|
|
|
|
|
|
}
|
2026-06-22 13:52:33 +02:00
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 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 } },
|
|
|
|
|
|
}),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-06-22 14:17:59 +02:00
|
|
|
|
|
2026-06-22 19:18:29 +02:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-22 14:17:59 +02:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Query: latest reading for a device
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2026-06-22 19:18:29 +02:00
|
|
|
|
export function useLatestReading(uuid: string, options?: AutoRefreshOptions) {
|
|
|
|
|
|
const refetchInterval = options?.refetchIntervalMs != null
|
|
|
|
|
|
? Math.max(2_000, options.refetchIntervalMs)
|
|
|
|
|
|
: undefined
|
|
|
|
|
|
|
2026-06-22 14:17:59 +02:00
|
|
|
|
return useQuery({
|
|
|
|
|
|
queryKey: ['modbus-latest', uuid],
|
|
|
|
|
|
queryFn: async () => {
|
|
|
|
|
|
const res = await apiClient.GET('/api/modbus/devices/{uuid}/latest', {
|
|
|
|
|
|
params: { path: { uuid } },
|
|
|
|
|
|
})
|
|
|
|
|
|
return res.data
|
|
|
|
|
|
},
|
2026-06-22 19:18:29 +02:00
|
|
|
|
refetchInterval,
|
2026-06-22 14:17:59 +02:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
2026-06-22 19:18:29 +02:00
|
|
|
|
export function useReadings(
|
|
|
|
|
|
uuid: string,
|
|
|
|
|
|
params: ReadingsQueryParams,
|
|
|
|
|
|
options?: AutoRefreshOptions,
|
|
|
|
|
|
) {
|
2026-06-22 14:17:59 +02:00
|
|
|
|
const { start, end, limit } = params
|
|
|
|
|
|
const effectiveLimit = Math.min(limit ?? READINGS_MAX_LIMIT, READINGS_MAX_LIMIT)
|
|
|
|
|
|
|
2026-06-22 19:18:29 +02:00
|
|
|
|
const refetchInterval = options?.refetchIntervalMs != null
|
|
|
|
|
|
? Math.max(2_000, options.refetchIntervalMs)
|
|
|
|
|
|
: undefined
|
|
|
|
|
|
|
2026-06-22 14:17:59 +02:00
|
|
|
|
return useQuery({
|
|
|
|
|
|
queryKey: ['modbus-readings', uuid, { start, end, limit: effectiveLimit }],
|
|
|
|
|
|
queryFn: async () => {
|
|
|
|
|
|
const res = await apiClient.GET('/api/modbus/devices/{uuid}/readings', {
|
|
|
|
|
|
params: {
|
|
|
|
|
|
path: { uuid },
|
|
|
|
|
|
query: {
|
|
|
|
|
|
...(start ? { start } : {}),
|
|
|
|
|
|
...(end ? { end } : {}),
|
|
|
|
|
|
limit: effectiveLimit,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
return res.data
|
|
|
|
|
|
},
|
|
|
|
|
|
// Enabled only when we have valid uuid; start/end may be null (full window).
|
|
|
|
|
|
enabled: !!uuid,
|
2026-06-22 19:18:29 +02:00
|
|
|
|
refetchInterval,
|
2026-06-22 14:17:59 +02:00
|
|
|
|
})
|
|
|
|
|
|
}
|