From 4ba953f44c48202baaad017db186a9e241d91b44 Mon Sep 17 00:00:00 2001 From: Syasya Date: Wed, 6 Aug 2025 10:50:31 +0800 Subject: [PATCH] add solar forecast --- app/utils/api.ts | 22 ++++++++++ components/dashboards/EnergyLineChart.tsx | 51 +++++++++++++++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/app/utils/api.ts b/app/utils/api.ts index e5a79da..9cb282e 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -26,3 +26,25 @@ export async function fetchPowerTimeseries( console.log(`🔍 API response from /power-timeseries?${params.toString()}:`, json); // ✅ log here return json; // <-- This is a single object, not an array } + +export async function fetchForecast( + lat: number, + lon: number, + dec: number, + az: number, + kwp: number +): Promise<{ time: string; forecast: number }[]> { + const query = new URLSearchParams({ + lat: lat.toString(), + lon: lon.toString(), + dec: dec.toString(), + az: az.toString(), + kwp: kwp.toString(), + }).toString(); + + const res = await fetch(`http://localhost:8000/forecast?${query}`); + if (!res.ok) throw new Error("Failed to fetch forecast"); + + return res.json(); +} + diff --git a/components/dashboards/EnergyLineChart.tsx b/components/dashboards/EnergyLineChart.tsx index 613da56..e8e1eb3 100644 --- a/components/dashboards/EnergyLineChart.tsx +++ b/components/dashboards/EnergyLineChart.tsx @@ -13,7 +13,7 @@ import { startOfYear, endOfYear, } from 'date-fns'; -import { fetchPowerTimeseries } from '@/app/utils/api'; +import { fetchPowerTimeseries, fetchForecast } from '@/app/utils/api'; ChartJS.register(zoomPlugin); @@ -41,7 +41,8 @@ function groupTimeSeries( const local = new Date(date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' })); const hour = local.getHours(); const minute = local.getMinutes() < 30 ? '00' : '30'; - key = `${hour.toString().padStart(2, '0')}:${minute}`; + const adjusted = new Date(local.setMinutes(minute === '00' ? 0 : 30, 0)); // zero seconds + key = adjusted.toISOString(); // ✅ full timestamp key break; case 'daily': key = date.toLocaleDateString('en-MY', { @@ -78,6 +79,8 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { const [consumption, setConsumption] = useState([]); const [generation, setGeneration] = useState([]); const [selectedDate, setSelectedDate] = useState(new Date()); + const [forecast, setForecast] = useState([]); + useEffect(() => { const now = new Date(); @@ -115,6 +118,20 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { const res = await fetchPowerTimeseries(siteId, isoStart, isoEnd); setConsumption(res.consumption); setGeneration(res.generation); + + // ⬇️ ADD THIS here — fetch forecast + const forecastData = await fetchForecast(3.15, 101.7, 37, 0, 5.67); + const selectedDateStr = selectedDate.toISOString().split('T')[0]; + + setForecast( + forecastData + .filter(({ time }) => time.startsWith(selectedDateStr)) // ✅ filter only selected date + .map(({ time, forecast }) => ({ + time, + value: forecast + })) + ); + } catch (error) { console.error('Failed to fetch energy timeseries:', error); } @@ -125,19 +142,25 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { const groupedConsumption = groupTimeSeries(consumption, viewMode); const groupedGeneration = groupTimeSeries(generation, viewMode); + const groupedForecast = groupTimeSeries(forecast, viewMode); + const forecastMap = Object.fromEntries(groupedForecast.map(d => [d.time, d.value])); + const allTimes = Array.from(new Set([ ...groupedConsumption.map(d => d.time), ...groupedGeneration.map(d => d.time), + ...groupedForecast.map(d => d.time), ])).sort((a, b) => new Date(a).getTime() - new Date(b).getTime()); + const consumptionMap = Object.fromEntries(groupedConsumption.map(d => [d.time, d.value])); const generationMap = Object.fromEntries(groupedGeneration.map(d => [d.time, d.value])); const [startIndex, setStartIndex] = useState(0); const [endIndex, setEndIndex] = useState(allTimes.length - 1); + useEffect(() => { if (typeof window !== 'undefined') { import('hammerjs'); @@ -151,6 +174,13 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { const formatLabel = (key: string) => { switch (viewMode) { + case 'day': + return new Date(key).toLocaleTimeString('en-GB', { + hour: '2-digit', + minute: '2-digit', + hour12: false, + timeZone: 'Asia/Kuala_Lumpur', + }); case 'monthly': return new Date(`${key}-01`).toLocaleString('en-GB', { month: 'short', year: 'numeric' }); case 'weekly': @@ -161,8 +191,10 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { }; const filteredLabels = allTimes.slice(startIndex, endIndex + 1); - const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? null); - const filteredGeneration = filteredLabels.map(t => generationMap[t] ?? null); + const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? 0); + const filteredGeneration = filteredLabels.map(t => generationMap[t] ?? 0); + const filteredForecast = filteredLabels.map(t => forecastMap[t] ?? null); + const allValues = [...filteredConsumption, ...filteredGeneration].filter(v => v !== null) as number[]; const maxValue = allValues.length > 0 ? Math.max(...allValues) : 0; @@ -177,6 +209,7 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { borderColor: '#8884d8', tension: 0.4, fill: false, + spanGaps: true, }, { label: 'Generation', @@ -184,7 +217,17 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => { borderColor: '#82ca9d', tension: 0.4, fill: false, + spanGaps: true, }, + { + label: 'Forecasted Solar', + data: filteredForecast, + borderColor: '#ffa500', // orange + tension: 0.4, + borderDash: [5, 5], // dashed line to distinguish forecast + fill: false, + spanGaps: true, + } ], };