added shaded wind trend
reworked labels on 12h and 24h wind arcs
This commit is contained in:
@@ -44,6 +44,9 @@ GUST_WINDOW = 600 # 10 minutes
|
||||
# Window for wind direction history buffer (seconds)
|
||||
WIND_DIR_HISTORY_WINDOW = 1800 # 30 minutes
|
||||
|
||||
# Window for sector-based wind trend tracker (seconds)
|
||||
WIND_TREND_WINDOW = 3600 # 60 minutes
|
||||
|
||||
# =============================================================================
|
||||
# PATH CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
@@ -54,6 +54,7 @@ from config import (
|
||||
OBSERVATION_MIN_INTERVAL, STATION_UPDATE_INTERVAL,
|
||||
GPS_MOVEMENT_THRESHOLD_METERS,
|
||||
WIND_AVG_WINDOW, GUST_WINDOW, WIND_DIR_HISTORY_WINDOW,
|
||||
WIND_TREND_WINDOW,
|
||||
LOGGING_CONFIG,
|
||||
)
|
||||
|
||||
@@ -311,6 +312,50 @@ class WindDirHistory:
|
||||
return json.dumps(out)
|
||||
|
||||
|
||||
class WindTrendTracker:
|
||||
"""Sector-based wind trend: aggregates speed by directional sector over a
|
||||
rolling window so the UI can render a heatmap ring showing wind shifts."""
|
||||
|
||||
SECTOR_COUNT = 120 # 3° per sector
|
||||
|
||||
def __init__(self, window_seconds=3600.0):
|
||||
self.window_seconds = window_seconds
|
||||
self.sector_size = 360.0 / self.SECTOR_COUNT
|
||||
self.samples = deque()
|
||||
|
||||
def add_sample(self, direction_deg, speed_ms):
|
||||
now = time.time()
|
||||
sector = int(direction_deg / self.sector_size) % self.SECTOR_COUNT
|
||||
self.samples.append((now, sector, speed_ms))
|
||||
self._prune(now)
|
||||
|
||||
def _prune(self, now):
|
||||
cutoff = now - self.window_seconds
|
||||
while self.samples and self.samples[0][0] < cutoff:
|
||||
self.samples.popleft()
|
||||
|
||||
def to_json(self):
|
||||
"""Compact JSON: {s: sector_count, w: window_ms,
|
||||
d: [[idx, last_seen_ms, peak_spd, count], ...]}"""
|
||||
self._prune(time.time())
|
||||
sectors = {}
|
||||
for ts, sector, spd in self.samples:
|
||||
if sector not in sectors:
|
||||
sectors[sector] = {"last": ts, "peak": spd or 0, "n": 0}
|
||||
entry = sectors[sector]
|
||||
if ts > entry["last"]:
|
||||
entry["last"] = ts
|
||||
if spd is not None and spd > entry["peak"]:
|
||||
entry["peak"] = spd
|
||||
entry["n"] += 1
|
||||
data = []
|
||||
for idx, info in sorted(sectors.items()):
|
||||
data.append([idx, int(info["last"] * 1000),
|
||||
round(info["peak"], 2), info["n"]])
|
||||
return json.dumps({"s": self.SECTOR_COUNT,
|
||||
"w": int(self.window_seconds * 1000), "d": data})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main controller
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -334,6 +379,7 @@ class WindyStationController:
|
||||
self.wind_averager = WindSpeedAverager(WIND_AVG_WINDOW)
|
||||
self.gust_tracker = WindGustTracker(GUST_WINDOW)
|
||||
self.wind_dir_history = WindDirHistory(WIND_DIR_HISTORY_WINDOW)
|
||||
self.wind_trend = WindTrendTracker(WIND_TREND_WINDOW)
|
||||
|
||||
# Timing state
|
||||
self.last_observation_time = 0
|
||||
@@ -416,6 +462,7 @@ class WindyStationController:
|
||||
self.dbus_service.add_path('/LastReportTime', 0,
|
||||
gettextcallback=self._report_time_text)
|
||||
self.dbus_service.add_path('/WindDirHistory', '[]')
|
||||
self.dbus_service.add_path('/WindTrend', '{}')
|
||||
|
||||
# Writable settings
|
||||
self.dbus_service.add_path('/Settings/Enabled', 1,
|
||||
@@ -901,6 +948,7 @@ class WindyStationController:
|
||||
self.gust_tracker.add_sample(wind_ms)
|
||||
if wind_dir is not None:
|
||||
self.wind_dir_history.add_sample(wind_dir, wind_ms)
|
||||
self.wind_trend.add_sample(wind_dir, wind_ms)
|
||||
self.last_wind_sample = now
|
||||
|
||||
# Update current conditions on D-Bus (for GUI display)
|
||||
@@ -918,6 +966,8 @@ class WindyStationController:
|
||||
if now - self.last_dir_history_publish >= 15.0:
|
||||
self.dbus_service['/WindDirHistory'] = (
|
||||
self.wind_dir_history.to_json())
|
||||
self.dbus_service['/WindTrend'] = (
|
||||
self.wind_trend.to_json())
|
||||
self.last_dir_history_publish = now
|
||||
|
||||
# GPS position
|
||||
|
||||
BIN
venus-data.zip
Normal file
BIN
venus-data.zip
Normal file
Binary file not shown.
@@ -206,6 +206,7 @@ const CompactWeatherCard: React.FC<CompactWeatherCardProps> = ({ onClick }) => {
|
||||
windDirection={meteo.windDirection}
|
||||
heading={nav.heading}
|
||||
windDirHistory={ws.windDirHistory}
|
||||
windTrend={ws.windTrend}
|
||||
imperial={ws.imperial}
|
||||
forecastArc12h={forecastArcs.arc12h}
|
||||
forecastArc24h={forecastArcs.arc24h}
|
||||
|
||||
@@ -219,6 +219,7 @@ const WeatherView = () => {
|
||||
windDirection={meteo.windDirection}
|
||||
heading={nav.heading}
|
||||
windDirHistory={ws.windDirHistory}
|
||||
windTrend={ws.windTrend}
|
||||
imperial={imperial}
|
||||
forecastArc12h={forecastArcs.arc12h}
|
||||
forecastArc24h={forecastArcs.arc24h}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useRef, useState, useEffect, useMemo } from "react"
|
||||
import type { WindTrendData } from "../../../utils/hooks/use-custom-mqtt"
|
||||
|
||||
export interface ForecastArcData {
|
||||
startDir: number
|
||||
@@ -21,6 +22,7 @@ interface WindCompassProps {
|
||||
windDirection: number | null
|
||||
heading: number | null
|
||||
windDirHistory: { time: number; dir: number; spd?: number }[]
|
||||
windTrend?: WindTrendData | null
|
||||
imperial: boolean
|
||||
forecastArc12h?: ForecastArcData | null
|
||||
forecastArc24h?: ForecastArcData | null
|
||||
@@ -31,8 +33,8 @@ interface WindCompassProps {
|
||||
const CX = 200
|
||||
const CY = 200
|
||||
const COMPASS_R = 155
|
||||
const TREND_R = 175
|
||||
const TREND_THICKNESS = 12
|
||||
const TREND_THICKNESS = 10
|
||||
const TREND_R = COMPASS_R + TREND_THICKNESS / 2 + 1
|
||||
const TICK_OUTER = COMPASS_R
|
||||
const TICK_INNER_MAJOR = COMPASS_R - 14
|
||||
const TICK_INNER_MINOR = COMPASS_R - 8
|
||||
@@ -46,10 +48,11 @@ const CARDINALS = [
|
||||
{ label: "W", deg: 270 },
|
||||
] as const
|
||||
|
||||
const FORECAST_ARC_THICKNESS = 10
|
||||
const ARC_24H_THICKNESS = 8
|
||||
const FORECAST_ARC_R = COMPASS_R - 18
|
||||
const ARC_24H_R = FORECAST_ARC_R - FORECAST_ARC_THICKNESS / 2 - ARC_24H_THICKNESS / 2 - 1
|
||||
const FORECAST_ARC_THICKNESS = 16
|
||||
const ARC_24H_THICKNESS = 14
|
||||
const FORECAST_ARC_R = COMPASS_R - 20
|
||||
const ARC_GAP = 4
|
||||
const ARC_24H_R = FORECAST_ARC_R - FORECAST_ARC_THICKNESS / 2 - ARC_GAP - ARC_24H_THICKNESS / 2
|
||||
|
||||
function polarToXY(cx: number, cy: number, r: number, angleDeg: number) {
|
||||
const rad = ((angleDeg - 90) * Math.PI) / 180
|
||||
@@ -193,12 +196,106 @@ const LIGHTNING_COLORS: Record<string, string> = {
|
||||
imminent: "#ef4444",
|
||||
}
|
||||
|
||||
interface TrendArcSegment {
|
||||
startDeg: number
|
||||
endDeg: number
|
||||
color: string
|
||||
opacity: number
|
||||
}
|
||||
|
||||
const FADE_DEPTH = 3
|
||||
const FADE_CURVE = [0.2, 0.45, 0.75]
|
||||
|
||||
function buildTrendSegments(trend: WindTrendData): TrendArcSegment[] {
|
||||
if (!trend.data.length) return []
|
||||
|
||||
const now = Date.now()
|
||||
const n = trend.sectors
|
||||
const sectorSize = 360 / n
|
||||
|
||||
const sectorData: Record<number, { color: string; opacity: number }> = {}
|
||||
for (const s of trend.data) {
|
||||
const age = Math.min(1, Math.max(0, (now - s.lastSeen) / trend.window))
|
||||
sectorData[s.idx] = {
|
||||
color: windSpeedColor(s.peakSpd * MS_TO_KTS),
|
||||
opacity: Math.max(0.15, 1.0 - age * 0.85),
|
||||
}
|
||||
}
|
||||
|
||||
const occupiedIndices = Object.keys(sectorData).map(Number)
|
||||
const occupied = new Set(occupiedIndices)
|
||||
|
||||
function distToEdge(idx: number, dir: -1 | 1): number {
|
||||
for (let d = 1; d <= FADE_DEPTH; d++) {
|
||||
const wrapped = (((idx + dir * d) % n) + n) % n
|
||||
if (!occupied.has(wrapped)) return d - 1
|
||||
}
|
||||
return FADE_DEPTH
|
||||
}
|
||||
|
||||
const segments: TrendArcSegment[] = []
|
||||
|
||||
for (const idx of occupiedIndices) {
|
||||
const info = sectorData[idx]
|
||||
const dLeft = distToEdge(idx, -1)
|
||||
const dRight = distToEdge(idx, 1)
|
||||
const minDist = Math.min(dLeft, dRight)
|
||||
const fadeMul = minDist >= FADE_DEPTH ? 1.0 : FADE_CURVE[minDist]
|
||||
segments.push({
|
||||
startDeg: idx * sectorSize,
|
||||
endDeg: (idx + 1) * sectorSize,
|
||||
color: info.color,
|
||||
opacity: info.opacity * fadeMul,
|
||||
})
|
||||
}
|
||||
|
||||
for (const idx of occupiedIndices) {
|
||||
const info = sectorData[idx]
|
||||
for (let d = 1; d <= 2; d++) {
|
||||
for (let di = 0; di < 2; di++) {
|
||||
const dir = di === 0 ? -1 : 1
|
||||
const neighbor = (((idx + dir * d) % n) + n) % n
|
||||
if (occupied.has(neighbor)) continue
|
||||
const ghostOpacity = info.opacity * (d === 1 ? 0.12 : 0.04)
|
||||
if (ghostOpacity < 0.02) continue
|
||||
segments.push({
|
||||
startDeg: neighbor * sectorSize,
|
||||
endDeg: (neighbor + 1) * sectorSize,
|
||||
color: info.color,
|
||||
opacity: ghostOpacity,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
segments.sort((a, b) => a.startDeg - b.startDeg)
|
||||
|
||||
const merged: TrendArcSegment[] = []
|
||||
let cur = { ...segments[0] }
|
||||
for (let i = 1; i < segments.length; i++) {
|
||||
const next = segments[i]
|
||||
const adj = Math.abs(next.startDeg - cur.endDeg) < 0.01
|
||||
const sameColor = next.color === cur.color
|
||||
const simOp = Math.abs(next.opacity - cur.opacity) < 0.06
|
||||
if (adj && sameColor && simOp) {
|
||||
cur.endDeg = next.endDeg
|
||||
cur.opacity = Math.min(cur.opacity, next.opacity)
|
||||
} else {
|
||||
merged.push(cur)
|
||||
cur = { ...next }
|
||||
}
|
||||
}
|
||||
merged.push(cur)
|
||||
return merged
|
||||
}
|
||||
|
||||
const WindCompass: React.FC<WindCompassProps> = ({
|
||||
windSpeed,
|
||||
windGust,
|
||||
windDirection,
|
||||
heading,
|
||||
windDirHistory,
|
||||
windTrend,
|
||||
imperial,
|
||||
forecastArc12h,
|
||||
forecastArc24h,
|
||||
@@ -210,10 +307,13 @@ const WindCompass: React.FC<WindCompassProps> = ({
|
||||
if (imperial) return (ms * 1.94384).toFixed(0)
|
||||
return ms.toFixed(1)
|
||||
}
|
||||
const speedUnit = imperial ? "kts" : "m/s"
|
||||
|
||||
const trendData = computeWindDirSpread(windDirHistory)
|
||||
|
||||
const trendSegments = useMemo(
|
||||
() => (windTrend && windTrend.data.length > 0 ? buildTrendSegments(windTrend) : null),
|
||||
[windTrend],
|
||||
)
|
||||
|
||||
const ticks = useMemo(() => {
|
||||
const result: React.ReactNode[] = []
|
||||
for (let deg = 0; deg < 360; deg += 5) {
|
||||
@@ -283,17 +383,29 @@ const WindCompass: React.FC<WindCompassProps> = ({
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
{/* Trend arc */}
|
||||
{trendData && (
|
||||
<path
|
||||
d={arcPath(CX, CY, TREND_R, trendData.startDir, trendData.endDir)}
|
||||
fill="none"
|
||||
stroke={windSpeedColor(trendData.avgSpdKts)}
|
||||
strokeWidth={TREND_THICKNESS}
|
||||
strokeLinecap="round"
|
||||
opacity={0.7}
|
||||
/>
|
||||
)}
|
||||
{/* Wind trend ring — sector heatmap when available, single arc fallback */}
|
||||
{trendSegments
|
||||
? trendSegments.map((seg, i) => (
|
||||
<path
|
||||
key={`ws${i}`}
|
||||
d={arcPath(CX, CY, TREND_R, seg.startDeg, seg.endDeg)}
|
||||
fill="none"
|
||||
stroke={seg.color}
|
||||
strokeWidth={TREND_THICKNESS}
|
||||
strokeLinecap="butt"
|
||||
opacity={seg.opacity}
|
||||
/>
|
||||
))
|
||||
: trendData && (
|
||||
<path
|
||||
d={arcPath(CX, CY, TREND_R, trendData.startDir, trendData.endDir)}
|
||||
fill="none"
|
||||
stroke={windSpeedColor(trendData.avgSpdKts)}
|
||||
strokeWidth={TREND_THICKNESS}
|
||||
strokeLinecap="round"
|
||||
opacity={0.7}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Compass ring */}
|
||||
<circle cx={CX} cy={CY} r={COMPASS_R} fill="none" stroke="#444" strokeWidth={1.5} />
|
||||
@@ -301,48 +413,15 @@ const WindCompass: React.FC<WindCompassProps> = ({
|
||||
{/* Tick marks */}
|
||||
{ticks}
|
||||
|
||||
{/* Forecast guide circles and labels */}
|
||||
{/* Forecast guide circles */}
|
||||
{(forecastArc12h || forecastArc24h) && (
|
||||
<>
|
||||
<circle cx={CX} cy={CY} r={FORECAST_ARC_R} fill="none" stroke="#555" strokeWidth={0.5} opacity={0.4} />
|
||||
<circle cx={CX} cy={CY} r={ARC_24H_R} fill="none" stroke="#555" strokeWidth={0.5} opacity={0.4} />
|
||||
{(() => {
|
||||
const labelAngle = 315
|
||||
const p12 = polarToXY(CX, CY, FORECAST_ARC_R, labelAngle)
|
||||
const p24 = polarToXY(CX, CY, ARC_24H_R, labelAngle)
|
||||
return (
|
||||
<>
|
||||
<text
|
||||
x={p12.x}
|
||||
y={p12.y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill="#777"
|
||||
fontSize={8}
|
||||
fontFamily="sans-serif"
|
||||
fontWeight={600}
|
||||
>
|
||||
12h
|
||||
</text>
|
||||
<text
|
||||
x={p24.x}
|
||||
y={p24.y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill="#777"
|
||||
fontSize={8}
|
||||
fontFamily="sans-serif"
|
||||
fontWeight={600}
|
||||
>
|
||||
24h
|
||||
</text>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
<circle cx={CX} cy={CY} r={FORECAST_ARC_R} fill="none" stroke="#555" strokeWidth={0.5} opacity={0.3} />
|
||||
<circle cx={CX} cy={CY} r={ARC_24H_R} fill="none" stroke="#555" strokeWidth={0.5} opacity={0.3} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 12h forecast direction arc (outer, colored by peak gust) */}
|
||||
{/* 12h forecast arc */}
|
||||
{forecastArc12h && (
|
||||
<path
|
||||
d={arcPath(CX, CY, FORECAST_ARC_R, forecastArc12h.startDir, forecastArc12h.endDir)}
|
||||
@@ -354,7 +433,7 @@ const WindCompass: React.FC<WindCompassProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 24h forecast direction arc (inner, dashed when high wind present) */}
|
||||
{/* 24h forecast arc */}
|
||||
{forecastArc24h && (
|
||||
<path
|
||||
d={arcPath(CX, CY, ARC_24H_R, forecastArc24h.startDir, forecastArc24h.endDir)}
|
||||
@@ -374,6 +453,29 @@ const WindCompass: React.FC<WindCompassProps> = ({
|
||||
<g
|
||||
transform={`rotate(${boatHeading}, ${CX}, ${CY}) translate(${CX}, ${CY}) scale(1.5) translate(${-CX}, ${-CY})`}
|
||||
>
|
||||
{/* Starboard (green, right side) bow fill */}
|
||||
<path
|
||||
d={`
|
||||
M ${CX} ${CY - 65}
|
||||
C ${CX + 8} ${CY - 55}, ${CX + 10} ${CY - 48}, ${CX + 12} ${CY - 42}
|
||||
L ${CX} ${CY - 42}
|
||||
Z
|
||||
`}
|
||||
fill="#22c55e"
|
||||
opacity={0.45}
|
||||
/>
|
||||
{/* Port (red, left side) bow fill */}
|
||||
<path
|
||||
d={`
|
||||
M ${CX} ${CY - 65}
|
||||
C ${CX - 8} ${CY - 55}, ${CX - 10} ${CY - 48}, ${CX - 12} ${CY - 42}
|
||||
L ${CX} ${CY - 42}
|
||||
Z
|
||||
`}
|
||||
fill="#ef4444"
|
||||
opacity={0.45}
|
||||
/>
|
||||
{/* Hull outline */}
|
||||
<path
|
||||
d={`
|
||||
M ${CX} ${CY - 65}
|
||||
@@ -389,12 +491,12 @@ const WindCompass: React.FC<WindCompassProps> = ({
|
||||
Z
|
||||
`}
|
||||
fill="none"
|
||||
stroke="#888"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.7}
|
||||
opacity={0.7}
|
||||
opacity={0.8}
|
||||
/>
|
||||
{/* Bow marker */}
|
||||
<line x1={CX} y1={CY - 65} x2={CX} y2={CY - 75} stroke="#888" strokeWidth={1.7} opacity={0.7} />
|
||||
<line x1={CX} y1={CY - 65} x2={CX} y2={CY - 75} stroke="currentColor" strokeWidth={3} opacity={0.8} />
|
||||
</g>
|
||||
|
||||
{/* Wind direction arrow with "T" label */}
|
||||
@@ -410,6 +512,8 @@ const WindCompass: React.FC<WindCompassProps> = ({
|
||||
Z
|
||||
`}
|
||||
fill="#22a55e"
|
||||
stroke="#000"
|
||||
strokeWidth={1.5}
|
||||
opacity={0.9}
|
||||
/>
|
||||
<text
|
||||
@@ -455,6 +559,78 @@ const WindCompass: React.FC<WindCompassProps> = ({
|
||||
</g>
|
||||
)}
|
||||
|
||||
{/* Forecast arc pill labels — rendered above compass/boat/wind layers */}
|
||||
{forecastArc12h &&
|
||||
(() => {
|
||||
let midDeg = (forecastArc12h.startDir + forecastArc12h.endDir) / 2
|
||||
let sweep = forecastArc12h.endDir - forecastArc12h.startDir
|
||||
if (sweep < 0) sweep += 360
|
||||
if (sweep > 180) midDeg = (midDeg + 180) % 360
|
||||
const mid = polarToXY(CX, CY, FORECAST_ARC_R, midDeg)
|
||||
return (
|
||||
<g>
|
||||
<rect
|
||||
x={mid.x - 12}
|
||||
y={mid.y - 9}
|
||||
width={24}
|
||||
height={18}
|
||||
rx={9}
|
||||
fill="var(--c-surface-secondary, #F5F5F5)"
|
||||
stroke="#aaa"
|
||||
strokeWidth={0.8}
|
||||
opacity={0.95}
|
||||
/>
|
||||
<text
|
||||
x={mid.x}
|
||||
y={mid.y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill="#999"
|
||||
fontSize={12}
|
||||
fontWeight={700}
|
||||
fontFamily="sans-serif"
|
||||
>
|
||||
12
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
})()}
|
||||
{forecastArc24h &&
|
||||
(() => {
|
||||
let midDeg = (forecastArc24h.startDir + forecastArc24h.endDir) / 2
|
||||
let sweep = forecastArc24h.endDir - forecastArc24h.startDir
|
||||
if (sweep < 0) sweep += 360
|
||||
if (sweep > 180) midDeg = (midDeg + 180) % 360
|
||||
const mid = polarToXY(CX, CY, ARC_24H_R, midDeg)
|
||||
return (
|
||||
<g>
|
||||
<rect
|
||||
x={mid.x - 12}
|
||||
y={mid.y - 9}
|
||||
width={24}
|
||||
height={18}
|
||||
rx={9}
|
||||
fill="var(--c-surface-secondary, #F5F5F5)"
|
||||
stroke="#aaa"
|
||||
strokeWidth={0.8}
|
||||
opacity={0.95}
|
||||
/>
|
||||
<text
|
||||
x={mid.x}
|
||||
y={mid.y}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill="#999"
|
||||
fontSize={12}
|
||||
fontWeight={700}
|
||||
fontFamily="sans-serif"
|
||||
>
|
||||
24
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
})()}
|
||||
|
||||
{/* Center overlay to improve wind speed readability */}
|
||||
<circle cx={CX} cy={CY} r={90} fill="url(#compassCenterFade)" />
|
||||
<circle cx={CX} cy={CY} r={78} fill="none" stroke="#888" strokeWidth={0.5} opacity={0.25} />
|
||||
@@ -475,23 +651,10 @@ const WindCompass: React.FC<WindCompassProps> = ({
|
||||
>
|
||||
{fmtSpeed(windSpeed)}
|
||||
</text>
|
||||
<text
|
||||
x={CX}
|
||||
y={CY + 30}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill="currentColor"
|
||||
opacity={0.5}
|
||||
fontSize={21}
|
||||
fontFamily="sans-serif"
|
||||
>
|
||||
{speedUnit}
|
||||
</text>
|
||||
|
||||
{/* Gust speed */}
|
||||
<text
|
||||
x={CX}
|
||||
y={CY + 58}
|
||||
y={CY + 30}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill="#eab308"
|
||||
|
||||
@@ -177,8 +177,22 @@ const WINDY_PATHS = [
|
||||
"/Settings/Units",
|
||||
"/Status",
|
||||
"/WindDirHistory",
|
||||
"/WindTrend",
|
||||
]
|
||||
|
||||
export interface WindTrendSector {
|
||||
idx: number
|
||||
lastSeen: number
|
||||
peakSpd: number
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface WindTrendData {
|
||||
sectors: number
|
||||
window: number
|
||||
data: WindTrendSector[]
|
||||
}
|
||||
|
||||
export function useWindyStation() {
|
||||
const { values, setValue, isConnected } = useCustomService("windystation", WINDY_PATHS)
|
||||
|
||||
@@ -192,6 +206,27 @@ export function useWindyStation() {
|
||||
}
|
||||
}, [rawDirHistory])
|
||||
|
||||
const rawWindTrend = values["/WindTrend"]
|
||||
const windTrend: WindTrendData | null = useMemo(() => {
|
||||
if (!rawWindTrend || typeof rawWindTrend !== "string") return null
|
||||
try {
|
||||
const parsed = JSON.parse(rawWindTrend)
|
||||
if (!parsed.d || !Array.isArray(parsed.d)) return null
|
||||
return {
|
||||
sectors: parsed.s as number,
|
||||
window: parsed.w as number,
|
||||
data: parsed.d.map((row: number[]) => ({
|
||||
idx: row[0],
|
||||
lastSeen: row[1],
|
||||
peakSpd: row[2],
|
||||
count: row[3],
|
||||
})),
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}, [rawWindTrend])
|
||||
|
||||
return {
|
||||
connected: values["/Connected"] === 1,
|
||||
windSpeed: values["/WindSpeed"] as number | null,
|
||||
@@ -204,6 +239,7 @@ export function useWindyStation() {
|
||||
imperial: values["/Settings/Units"] === 1,
|
||||
status: values["/Status"] as number | null,
|
||||
windDirHistory,
|
||||
windTrend,
|
||||
setEnabled: (v: boolean) => setValue("/Settings/Enabled", v ? 1 : 0),
|
||||
isConnected,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user