ems-core v1.0.0: Standard EMS platform core
Shared backend + frontend for multi-customer EMS deployments. - 12 enterprise modules: quota, cost, charging, maintenance, analysis, etc. - 120+ API endpoints, 37 database tables - Customer config mechanism (CUSTOMER env var + YAML config) - Collectors: Modbus TCP, MQTT, HTTP API, Sungrow iSolarCloud - Frontend: React 19 + Ant Design + ECharts + Three.js - Infrastructure: Redis cache, rate limiting, aggregation engine Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
129
frontend/src/pages/BigScreen3D/hooks/useDeviceData.ts
Normal file
129
frontend/src/pages/BigScreen3D/hooks/useDeviceData.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
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<string, string[]> = {
|
||||
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<string>();
|
||||
const result: DeviceWithPosition[] = [];
|
||||
|
||||
// Group devices by type
|
||||
const byType: Record<string, DeviceInfo[]> = {};
|
||||
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<DeviceInfo[]>([]);
|
||||
const [deviceStats, setDeviceStats] = useState<DeviceStats | null>(null);
|
||||
const [overview, setOverview] = useState<OverviewData | null>(null);
|
||||
const [realtimeData, setRealtimeData] = useState<RealtimePowerData | null>(null);
|
||||
const [devicesWithPositions, setDevicesWithPositions] = useState<DeviceWithPosition[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const fetchAll = useCallback(async () => {
|
||||
try {
|
||||
const results = await Promise.allSettled([
|
||||
getDevices({ page_size: 100 }) as Promise<any>,
|
||||
getDeviceStats() as Promise<any>,
|
||||
getDashboardOverview() as Promise<any>,
|
||||
getRealtimeData() as Promise<any>,
|
||||
]);
|
||||
|
||||
// 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user