add solar forecast
This commit is contained in:
parent
8c94dfe135
commit
4ba953f44c
@ -26,3 +26,25 @@ export async function fetchPowerTimeseries(
|
|||||||
console.log(`🔍 API response from /power-timeseries?${params.toString()}:`, json); // ✅ log here
|
console.log(`🔍 API response from /power-timeseries?${params.toString()}:`, json); // ✅ log here
|
||||||
return json; // <-- This is a single object, not an array
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import {
|
|||||||
startOfYear,
|
startOfYear,
|
||||||
endOfYear,
|
endOfYear,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { fetchPowerTimeseries } from '@/app/utils/api';
|
import { fetchPowerTimeseries, fetchForecast } from '@/app/utils/api';
|
||||||
|
|
||||||
ChartJS.register(zoomPlugin);
|
ChartJS.register(zoomPlugin);
|
||||||
|
|
||||||
@ -41,7 +41,8 @@ function groupTimeSeries(
|
|||||||
const local = new Date(date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' }));
|
const local = new Date(date.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' }));
|
||||||
const hour = local.getHours();
|
const hour = local.getHours();
|
||||||
const minute = local.getMinutes() < 30 ? '00' : '30';
|
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;
|
break;
|
||||||
case 'daily':
|
case 'daily':
|
||||||
key = date.toLocaleDateString('en-MY', {
|
key = date.toLocaleDateString('en-MY', {
|
||||||
@ -78,6 +79,8 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
|||||||
const [consumption, setConsumption] = useState<TimeSeriesEntry[]>([]);
|
const [consumption, setConsumption] = useState<TimeSeriesEntry[]>([]);
|
||||||
const [generation, setGeneration] = useState<TimeSeriesEntry[]>([]);
|
const [generation, setGeneration] = useState<TimeSeriesEntry[]>([]);
|
||||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
|
const [forecast, setForecast] = useState<TimeSeriesEntry[]>([]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@ -115,6 +118,20 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
|||||||
const res = await fetchPowerTimeseries(siteId, isoStart, isoEnd);
|
const res = await fetchPowerTimeseries(siteId, isoStart, isoEnd);
|
||||||
setConsumption(res.consumption);
|
setConsumption(res.consumption);
|
||||||
setGeneration(res.generation);
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch energy timeseries:', error);
|
console.error('Failed to fetch energy timeseries:', error);
|
||||||
}
|
}
|
||||||
@ -125,19 +142,25 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
|||||||
|
|
||||||
const groupedConsumption = groupTimeSeries(consumption, viewMode);
|
const groupedConsumption = groupTimeSeries(consumption, viewMode);
|
||||||
const groupedGeneration = groupTimeSeries(generation, 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([
|
const allTimes = Array.from(new Set([
|
||||||
...groupedConsumption.map(d => d.time),
|
...groupedConsumption.map(d => d.time),
|
||||||
...groupedGeneration.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());
|
])).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const consumptionMap = Object.fromEntries(groupedConsumption.map(d => [d.time, d.value]));
|
const consumptionMap = Object.fromEntries(groupedConsumption.map(d => [d.time, d.value]));
|
||||||
const generationMap = Object.fromEntries(groupedGeneration.map(d => [d.time, d.value]));
|
const generationMap = Object.fromEntries(groupedGeneration.map(d => [d.time, d.value]));
|
||||||
|
|
||||||
const [startIndex, setStartIndex] = useState(0);
|
const [startIndex, setStartIndex] = useState(0);
|
||||||
const [endIndex, setEndIndex] = useState(allTimes.length - 1);
|
const [endIndex, setEndIndex] = useState(allTimes.length - 1);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
import('hammerjs');
|
import('hammerjs');
|
||||||
@ -151,6 +174,13 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
|||||||
|
|
||||||
const formatLabel = (key: string) => {
|
const formatLabel = (key: string) => {
|
||||||
switch (viewMode) {
|
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':
|
case 'monthly':
|
||||||
return new Date(`${key}-01`).toLocaleString('en-GB', { month: 'short', year: 'numeric' });
|
return new Date(`${key}-01`).toLocaleString('en-GB', { month: 'short', year: 'numeric' });
|
||||||
case 'weekly':
|
case 'weekly':
|
||||||
@ -161,8 +191,10 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const filteredLabels = allTimes.slice(startIndex, endIndex + 1);
|
const filteredLabels = allTimes.slice(startIndex, endIndex + 1);
|
||||||
const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? null);
|
const filteredConsumption = filteredLabels.map(t => consumptionMap[t] ?? 0);
|
||||||
const filteredGeneration = filteredLabels.map(t => generationMap[t] ?? null);
|
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 allValues = [...filteredConsumption, ...filteredGeneration].filter(v => v !== null) as number[];
|
||||||
const maxValue = allValues.length > 0 ? Math.max(...allValues) : 0;
|
const maxValue = allValues.length > 0 ? Math.max(...allValues) : 0;
|
||||||
@ -177,6 +209,7 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
|||||||
borderColor: '#8884d8',
|
borderColor: '#8884d8',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
fill: false,
|
fill: false,
|
||||||
|
spanGaps: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Generation',
|
label: 'Generation',
|
||||||
@ -184,7 +217,17 @@ const EnergyLineChart = ({ siteId }: EnergyLineChartProps) => {
|
|||||||
borderColor: '#82ca9d',
|
borderColor: '#82ca9d',
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
fill: false,
|
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,
|
||||||
|
}
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user