import { useState, useEffect, useCallback, useRef } from 'react'; import { getDevices, getDeviceStats, getDashboardOverview, getRealtimeData } from '../../../services/api'; import type { DeviceInfo, DeviceWithPosition, OverviewData, RealtimePowerData } from '../types'; import { DEVICE_POSITIONS, POLL_INTERVAL } from '../constants'; interface DeviceStats { online: number; offline: number; alarm: number; maintenance: number; total: number; } // Ordered position keys by device type for fuzzy matching const POSITION_KEYS_BY_TYPE: Record = { pv_inverter: ['PV-INV-01', 'PV-INV-02', 'PV-INV-03'], heat_pump: ['HP-01', 'HP-02', 'HP-03', 'HP-04'], meter: ['MTR-GRID', 'MTR-PV', 'MTR-HP', 'MTR-PUMP'], sensor: ['SENSOR-01', 'SENSOR-02', 'SENSOR-03', 'SENSOR-04', 'SENSOR-05'], heat_meter: ['HM-01'], }; function matchDevicesToPositions(devices: DeviceInfo[]): DeviceWithPosition[] { const usedPositions = new Set(); const result: DeviceWithPosition[] = []; // Group devices by type const byType: Record = {}; for (const device of devices) { const type = device.device_type || 'unknown'; if (!byType[type]) byType[type] = []; byType[type].push(device); } for (const [type, typeDevices] of Object.entries(byType)) { const positionKeys = POSITION_KEYS_BY_TYPE[type] || []; typeDevices.forEach((device, index) => { // Try exact match by device code first let matchedKey: string | undefined; if (device.code && DEVICE_POSITIONS[device.code] && !usedPositions.has(device.code)) { matchedKey = device.code; } // Fall back to ordered assignment by type if (!matchedKey && index < positionKeys.length) { const key = positionKeys[index]; if (!usedPositions.has(key)) { matchedKey = key; } } const withPos: DeviceWithPosition = { ...device }; if (matchedKey) { usedPositions.add(matchedKey); const posData = DEVICE_POSITIONS[matchedKey]; withPos.position3D = posData.position; withPos.rotation3D = posData.rotation; // Override code with matched key so 3D components can look up positions by code withPos.code = matchedKey; } result.push(withPos); }); } return result; } export function useDeviceData() { const [devices, setDevices] = useState([]); const [deviceStats, setDeviceStats] = useState(null); const [overview, setOverview] = useState(null); const [realtimeData, setRealtimeData] = useState(null); const [devicesWithPositions, setDevicesWithPositions] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const intervalRef = useRef | null>(null); const fetchAll = useCallback(async () => { try { const results = await Promise.allSettled([ getDevices({ page_size: 100 }) as Promise, getDeviceStats() as Promise, getDashboardOverview() as Promise, getRealtimeData() as Promise, ]); // Devices if (results[0].status === 'fulfilled') { const devData = results[0].value; const items: DeviceInfo[] = devData?.items || []; setDevices(items); setDevicesWithPositions(matchDevicesToPositions(items)); } // Stats if (results[1].status === 'fulfilled') { setDeviceStats(results[1].value as DeviceStats); } // Overview if (results[2].status === 'fulfilled') { setOverview(results[2].value as OverviewData); } // Realtime if (results[3].status === 'fulfilled') { setRealtimeData(results[3].value as RealtimePowerData); } setError(null); } catch (err: any) { setError(err?.message || 'Failed to fetch device data'); } finally { setLoading(false); } }, []); useEffect(() => { fetchAll(); intervalRef.current = setInterval(fetchAll, POLL_INTERVAL); return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [fetchAll]); return { devices, deviceStats, overview, realtimeData, devicesWithPositions, loading, error }; }