Refactor forecast and historian-driven meteo dashboard
This commit is contained in:
@@ -262,8 +262,10 @@ export function ChartWindowPage({ theme }: ChartWindowPageProps) {
|
||||
>
|
||||
<header className={titleBarClass}>
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex h-full flex-1 items-center px-3 text-xs font-black"
|
||||
onPointerDown={() => {
|
||||
void currentWindow.startDragging();
|
||||
}}
|
||||
>
|
||||
{chart.title}
|
||||
</div>
|
||||
|
||||
@@ -366,7 +366,7 @@ export function ClimateChartsPage({ theme }: ClimateChartsPageProps) {
|
||||
};
|
||||
|
||||
const closeDetachedChartWindow = async (chartId: string) => {
|
||||
const label = `chart-${chartId}`;
|
||||
const label = `climatecharts-${chartId}`;
|
||||
const existing = await WebviewWindow.getByLabel(label);
|
||||
|
||||
if (!existing) return;
|
||||
|
||||
@@ -365,7 +365,7 @@ export function MainChartsPage({ theme }: MainChartsPageProps) {
|
||||
};
|
||||
|
||||
const closeDetachedChartWindow = async (chartId: string) => {
|
||||
const label = `chart-${chartId}`;
|
||||
const label = `maincharts-${chartId}`;
|
||||
const existing = await WebviewWindow.getByLabel(label);
|
||||
|
||||
if (!existing) return;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { ModuleSensorResponse } from "../../../types/meteo";
|
||||
import type { HistorianPoint } from "../components/MeteoHistoryModal";
|
||||
|
||||
const BACKEND_URL = "http://localhost:18450";
|
||||
|
||||
type SensorHistoryMap = Record<string, HistorianPoint[]>;
|
||||
|
||||
export function useMeteoMultiHistory(
|
||||
sensors: Array<ModuleSensorResponse | null>,
|
||||
hours = 6,
|
||||
) {
|
||||
const [pointsByKey, setPointsByKey] = useState<SensorHistoryMap>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const sensorKeys = sensors
|
||||
.filter((sensor): sensor is ModuleSensorResponse => Boolean(sensor))
|
||||
.map((sensor) => sensor.key);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sensorKeys.length) {
|
||||
setPointsByKey({});
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
async function loadHistory() {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const to = new Date();
|
||||
const from = new Date(to.getTime() - hours * 60 * 60 * 1000);
|
||||
|
||||
const entries = await Promise.all(
|
||||
sensorKeys.map(async (key) => {
|
||||
const params = new URLSearchParams({
|
||||
key,
|
||||
from: from.toISOString(),
|
||||
to: to.toISOString(),
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
`${BACKEND_URL}/api/historian/series?${params.toString()}`,
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load history for ${key}`);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as HistorianPoint[];
|
||||
|
||||
return [key, payload] as const;
|
||||
}),
|
||||
);
|
||||
|
||||
setPointsByKey(Object.fromEntries(entries));
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
console.error("Failed to load meteo histories", error);
|
||||
setPointsByKey({});
|
||||
} finally {
|
||||
if (!controller.signal.aborted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadHistory();
|
||||
|
||||
return () => controller.abort();
|
||||
}, [sensorKeys.join("|"), hours]);
|
||||
|
||||
return {
|
||||
pointsByKey,
|
||||
loading,
|
||||
};
|
||||
}
|
||||
+1056
-680
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,206 @@
|
||||
import { useMemo } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
Droplet,
|
||||
Gauge,
|
||||
Home,
|
||||
Layers3,
|
||||
Lightbulb,
|
||||
Thermometer,
|
||||
Wind,
|
||||
} from "lucide-react";
|
||||
|
||||
import type { ModuleSensorResponse } from "../../../types/meteo";
|
||||
import { useClimateModuleStream } from "../../climate/hooks/useClimateModuleStream";
|
||||
|
||||
export type SynopticVariableGroup = "Clima" | "Rega";
|
||||
|
||||
export type SynopticVariable = {
|
||||
id: string;
|
||||
sensorId: string;
|
||||
key: string;
|
||||
group: SynopticVariableGroup;
|
||||
label: string;
|
||||
value: string | number | boolean | null;
|
||||
unit: string;
|
||||
timestamp?: string;
|
||||
connected: boolean;
|
||||
icon: LucideIcon;
|
||||
color: string;
|
||||
};
|
||||
|
||||
export function useSynopticVariables() {
|
||||
const climate = useClimateModuleStream();
|
||||
|
||||
const variables = useMemo<SynopticVariable[]>(
|
||||
() =>
|
||||
toSynopticVariables({
|
||||
sensors: climate.sensors,
|
||||
group: "Clima",
|
||||
connected: climate.connected,
|
||||
fallbackTimestamp: climate.lastTimestamp,
|
||||
}),
|
||||
[
|
||||
climate.sensors,
|
||||
climate.connected,
|
||||
climate.lastTimestamp,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
variables,
|
||||
groups: groupVariables(variables),
|
||||
connected: climate.connected,
|
||||
climateConnected: climate.connected,
|
||||
loading: !climate.module,
|
||||
total: variables.length,
|
||||
};
|
||||
}
|
||||
|
||||
function toSynopticVariables({
|
||||
sensors,
|
||||
group,
|
||||
connected,
|
||||
fallbackTimestamp,
|
||||
}: {
|
||||
sensors: ModuleSensorResponse[];
|
||||
group: SynopticVariableGroup;
|
||||
connected: boolean;
|
||||
fallbackTimestamp: string | null;
|
||||
}): SynopticVariable[] {
|
||||
return sensors.map((sensor) => {
|
||||
const visual = getVariableVisual(sensor, group);
|
||||
|
||||
return {
|
||||
id: `${group}:${sensor.sensorId ?? sensor.key}`,
|
||||
sensorId: String(sensor.sensorId ?? sensor.key),
|
||||
key: sensor.key,
|
||||
group,
|
||||
label: sensor.name || sensor.key,
|
||||
value: normalizeValue(sensor.value),
|
||||
unit: sensor.unit ?? "",
|
||||
timestamp: sensor.timestamp ?? fallbackTimestamp ?? undefined,
|
||||
connected,
|
||||
icon: visual.icon,
|
||||
color: visual.color,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function groupVariables(variables: SynopticVariable[]) {
|
||||
return variables.reduce<Record<SynopticVariableGroup, SynopticVariable[]>>(
|
||||
(acc, variable) => {
|
||||
acc[variable.group].push(variable);
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
Clima: [],
|
||||
Rega: [],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeValue(value: unknown): string | number | boolean | null {
|
||||
if (
|
||||
typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean" ||
|
||||
value === null
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getVariableVisual(
|
||||
sensor: ModuleSensorResponse,
|
||||
group: SynopticVariableGroup,
|
||||
): {
|
||||
icon: LucideIcon;
|
||||
color: string;
|
||||
} {
|
||||
const text = normalizeText(`${sensor.key} ${sensor.name} ${sensor.unit ?? ""}`);
|
||||
|
||||
if (
|
||||
text.includes("temperatura") ||
|
||||
text.includes("temperature")
|
||||
) {
|
||||
return {
|
||||
icon: Thermometer,
|
||||
color: "#4FD1C5",
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
text.includes("humidade") ||
|
||||
text.includes("humidity")
|
||||
) {
|
||||
return {
|
||||
icon: Droplet,
|
||||
color: "#38BDF8",
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
text.includes("vento") ||
|
||||
text.includes("wind")
|
||||
) {
|
||||
return {
|
||||
icon: Wind,
|
||||
color: "#22D3EE",
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
text.includes("radiacao") ||
|
||||
text.includes("radiação") ||
|
||||
text.includes("radiation")
|
||||
) {
|
||||
return {
|
||||
icon: Lightbulb,
|
||||
color: "#FACC15",
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
text.includes("pressao") ||
|
||||
text.includes("pressão") ||
|
||||
text.includes("pressure")
|
||||
) {
|
||||
return {
|
||||
icon: Gauge,
|
||||
color: "#38BDF8",
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
text.includes("nivel") ||
|
||||
text.includes("nível") ||
|
||||
text.includes("level")
|
||||
) {
|
||||
return {
|
||||
icon: Layers3,
|
||||
color: "#0EA5E9",
|
||||
};
|
||||
}
|
||||
|
||||
if (group === "Clima") {
|
||||
return {
|
||||
icon: Home,
|
||||
color: "#22C55E",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
icon: Gauge,
|
||||
color: "#A3E635",
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeText(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.normalize("NFD")
|
||||
.replace(/\p{Diacritic}/gu, "");
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user