import { useEffect, useState, useCallback, useRef } from 'react'; import { Box, Paper, Typography, Fade, useTheme, Grid, AppBar, Toolbar, CircularProgress, IconButton, Tooltip, Chip, Button, Snackbar, Alert } from '@mui/material'; import RefreshIcon from '@mui/icons-material/Refresh'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import CancelIcon from '@mui/icons-material/Cancel'; import Plot from 'react-plotly.js'; import { Layout, PlotData, Config } from 'plotly.js'; import { config } from '../config/env'; // Extend the Window interface to include Google Charts declare global { interface Window { google?: { charts?: any; }; } } // Define the structure of our data interface ChartData { power: string; flag: string; env_temp_cur: string; now_timestamp: string; future_timestamp: string; env_temp_min: string; power_future_min: string; } const API_BASE_URL = config.apiUrl; const Temperature = () => { const theme = useTheme(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [lastUpdated, setLastUpdated] = useState(null); const [refreshing, setRefreshing] = useState(false); const [decisionLoading, setDecisionLoading] = useState(false); const [powerZoom, setPowerZoom] = useState<{xRange?: [number, number]; yRange?: [number, number]}>({}); const [tempZoom, setTempZoom] = useState<{xRange?: [number, number]; yRange?: [number, number]}>({}); const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({ open: false, message: '', severity: 'success' }); // Use refs to keep track of the interval const intervalRef = useRef(null); const updateIntervalMs = 5000; // 5 seconds refresh rate // Create a memoized fetchData function with useCallback const fetchData = useCallback(async (showLoadingIndicator = false) => { try { if (showLoadingIndicator) { setRefreshing(true); } const response = await fetch(`${API_BASE_URL}/prom/get_chart_data/temperature/20`); const result = await response.json(); if (result.data && result.data.length > 0) { // Sort by timestamp first to ensure we get the latest data const sortedData = [...result.data].sort((a, b) => new Date(b.now_timestamp).getTime() - new Date(a.now_timestamp).getTime() ); // Log the most recent flag console.log('Most recent flag:', sortedData[0].flag); // Filter valid data points const validData = sortedData.filter((item: any) => item.now_timestamp && item.future_timestamp && item.power && item.power_future_min && item.env_temp_cur && item.env_temp_min ); // Limit to last 20 records but maintain chronological order const last20Data = validData.slice(-20).sort((a, b) => new Date(a.now_timestamp).getTime() - new Date(b.now_timestamp).getTime() ); console.log(`Data updated at ${new Date().toLocaleTimeString()}:`, { totalRecords: result.data.length, validRecords: validData.length, displayedRecords: last20Data.length, latestFlag: last20Data[last20Data.length - 1]?.flag }); setData(last20Data); setLastUpdated(new Date()); } } catch (error) { console.error('Error fetching data:', error); } finally { setLoading(false); setRefreshing(false); } }, []); // Manual refresh handler const handleRefresh = () => { fetchData(true); }; // Set up the interval for real-time updates useEffect(() => { // Initial fetch fetchData(true); // Set up interval for auto-refresh intervalRef.current = setInterval(() => { console.log(`Auto-refreshing data at ${new Date().toLocaleTimeString()}`); fetchData(false); }, updateIntervalMs); // Cleanup function to clear the interval when component unmounts return () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; }, [fetchData]); // Process data for Plotly charts const preparePlotlyData = () => { if (!data || data.length === 0) return { powerData: [], tempData: [] }; const currentTimestamps = data.map(item => new Date(item.now_timestamp)); const futureTimestamps = data.map(item => new Date(item.future_timestamp)); const currentPower = data.map(item => parseFloat(item.power)); const predictedPower = data.map(item => parseFloat(item.power_future_min)); const currentTemp = data.map(item => parseFloat(item.env_temp_cur)); const predictedTemp = data.map(item => parseFloat(item.env_temp_min)); // Calculate min and max values for range sliders const powerMin = Math.min(...currentPower, ...predictedPower); const powerMax = Math.max(...currentPower, ...predictedPower); const tempMin = Math.min(...currentTemp, ...predictedTemp); const tempMax = Math.max(...currentTemp, ...predictedTemp); const powerData: Partial[] = [ { x: currentTimestamps, y: currentPower, type: 'scatter', mode: 'lines+markers', name: 'Current Power', line: { color: theme.palette.primary.main, width: 2 }, marker: { size: 6 } }, { x: futureTimestamps, y: predictedPower, type: 'scatter', mode: 'lines+markers', name: 'Predicted Power', line: { color: theme.palette.success.main, width: 2, dash: 'dash' }, marker: { size: 6 } }, // Range slider trace { x: [...currentTimestamps, ...futureTimestamps], y: Array(currentTimestamps.length + futureTimestamps.length).fill(0.5), type: 'scatter', mode: 'lines', name: 'Y Range', yaxis: 'y2', line: { color: 'transparent' }, showlegend: false, hoverinfo: 'skip' as const } ]; const tempData: Partial[] = [ { x: currentTimestamps, y: currentTemp, type: 'scatter', mode: 'lines+markers', name: 'Current Temperature', line: { color: theme.palette.primary.main, width: 2 }, marker: { size: 6 } }, { x: futureTimestamps, y: predictedTemp, type: 'scatter', mode: 'lines+markers', name: 'Predicted Temperature', line: { color: theme.palette.success.main, width: 2, dash: 'dash' }, marker: { size: 6 } }, // Range slider trace { x: [...currentTimestamps, ...futureTimestamps], y: Array(currentTimestamps.length + futureTimestamps.length).fill(0.5), type: 'scatter', mode: 'lines', name: 'Y Range', yaxis: 'y2', line: { color: 'transparent' }, showlegend: false, hoverinfo: 'skip' as const } ]; return { powerData, tempData, ranges: { power: { min: powerMin, max: powerMax }, temp: { min: tempMin, max: tempMax } } }; }; const { powerData, tempData, ranges } = preparePlotlyData(); // Common layout settings for both charts const commonLayoutSettings: Partial = { showlegend: true, legend: { orientation: 'h', y: -0.2, x: 0.5, xanchor: 'center', yanchor: 'top', font: { size: 12, family: theme.typography.fontFamily, color: theme.palette.text.secondary }, bgcolor: 'rgba(255, 255, 255, 0)', bordercolor: 'rgba(255, 255, 255, 0)' }, margin: { t: 60, b: 100, l: 60, r: 60 }, // Increased right margin for Y-axis range slider plot_bgcolor: 'rgba(0,0,0,0)', paper_bgcolor: 'rgba(0,0,0,0)', hovermode: 'closest', xaxis: { type: 'date', gridcolor: theme.palette.divider, tickfont: { size: 12, color: theme.palette.text.secondary }, showgrid: true, rangeslider: { visible: true } }, yaxis2: { overlaying: 'y', side: 'right', showgrid: false, zeroline: false, showticklabels: false, range: [0, 1], rangeslider: { visible: true, thickness: 0.1, bgcolor: 'rgba(0,0,0,0)', bordercolor: theme.palette.divider } } }; // Add handlers for zoom events const handlePowerZoom = (event: any) => { if (event['xaxis.range[0]']) { setPowerZoom({ xRange: [new Date(event['xaxis.range[0]']).getTime(), new Date(event['xaxis.range[1]']).getTime()], yRange: [event['yaxis.range[0]'], event['yaxis.range[1]']] }); } }; const handleTempZoom = (event: any) => { if (event['xaxis.range[0]']) { setTempZoom({ xRange: [new Date(event['xaxis.range[0]']).getTime(), new Date(event['xaxis.range[1]']).getTime()], yRange: [event['yaxis.range[0]'], event['yaxis.range[1]']] }); } }; // Modify the power layout to use preserved zoom const powerLayout: Partial = { ...commonLayoutSettings, yaxis: { title: 'Power (W)', gridcolor: theme.palette.divider, tickfont: { size: 12, color: theme.palette.text.secondary }, titlefont: { size: 14, color: theme.palette.text.primary }, showgrid: true, rangemode: 'tozero', fixedrange: false, range: powerZoom.yRange || (ranges ? [ranges.power.min * 0.9, ranges.power.max * 1.1] : undefined) }, xaxis: { ...commonLayoutSettings.xaxis, range: powerZoom.xRange ? [new Date(powerZoom.xRange[0]), new Date(powerZoom.xRange[1])] : undefined } }; // Modify the temperature layout to use preserved zoom const tempLayout: Partial = { ...commonLayoutSettings, yaxis: { title: 'Temperature (°C)', gridcolor: theme.palette.divider, tickfont: { size: 12, color: theme.palette.text.secondary }, titlefont: { size: 14, color: theme.palette.text.primary }, showgrid: true, rangemode: 'tozero', fixedrange: false, range: tempZoom.yRange || (ranges ? [ranges.temp.min * 0.9, ranges.temp.max * 1.1] : undefined) }, xaxis: { ...commonLayoutSettings.xaxis, range: tempZoom.xRange ? [new Date(tempZoom.xRange[0]), new Date(tempZoom.xRange[1])] : undefined } }; // Common Plotly config with additional modebar buttons const plotConfig: Partial = { responsive: true, displayModeBar: true, displaylogo: false, modeBarButtonsToRemove: ['lasso2d', 'select2d'] as ('lasso2d' | 'select2d')[], toImageButtonOptions: { format: 'png' as const, filename: 'temperature_monitoring', height: 1000, width: 1500, scale: 2 } }; // Handle temperature decision const handleTemperatureDecision = async (approval: boolean) => { try { setDecisionLoading(true); const response = await fetch(`${API_BASE_URL}/prom/temperature/decisions?approval=${approval}`, { method: 'POST', headers: { 'accept': 'application/json', } }); if (!response.ok) { throw new Error(`Failed to send temperature decision: ${response.statusText}`); } const result = await response.json(); setAlert({ open: true, message: result.message || `Temperature change ${approval ? 'approved' : 'declined'} successfully`, severity: 'success' }); // Refresh data after decision await fetchData(true); } catch (error) { console.error('Error sending temperature decision:', error); setAlert({ open: true, message: error instanceof Error ? error.message : 'Failed to send temperature decision', severity: 'error' }); } finally { setDecisionLoading(false); } }; const handleCloseAlert = () => { setAlert(prev => ({ ...prev, open: false })); }; return ( Environmental Temperature & Power Monitoring (Last 20 Records) {lastUpdated && ( Last updated: {lastUpdated.toLocaleTimeString()} )} {/* Temperature Decision Panel */} Temperature Change Decision {/* Power Chart */} Power Consumption {data.length > 0 && ( )} {refreshing && ( )} {loading ? ( Loading power data... ) : data.length === 0 ? ( No power data available ) : ( )} {/* Temperature Chart */} Environmental Temperature {data.length > 0 && ( )} {refreshing && ( )} {loading ? ( Loading temperature data... ) : data.length === 0 ? ( No temperature data available ) : ( )} {/* Snackbar for alerts */} {alert.message} ); }; export default Temperature;