Weather Station / ECOWITT / DNT
1import SunCalc from "suncalc";
2
3/**
4 * Represents the result of astronomical calculations.
5 * @property {Date | null} sunrise - The time of sunrise.
6 * @property {Date | null} sunset - The time of sunset.
7 * @property {Date | null} moonrise - The time of moonrise.
8 * @property {Date | null} moonset - The time of moonset.
9 * @property {number} phase - The moon phase, from 0.0 (new moon) to 1.0 (new moon).
10 * @property {string} phaseName - The name of the moon phase.
11 * @property {number} illumination - The fraction of the moon's illuminated limb.
12 * @property {Date | null} civilDawn - The time when the sun is 6 degrees below the horizon in the morning.
13 * @property {Date | null} civilDusk - The time when the sun is 6 degrees below the horizon in the evening.
14 * @property {Date | null} nauticalDawn - The time when the sun is 12 degrees below the horizon in the morning.
15 * @property {Date | null} nauticalDusk - The time when the sun is 12 degrees below the horizon in the evening.
16 * @property {Date | null} astronomicalDawn - The time when the sun is 18 degrees below the horizon in the morning.
17 * @property {Date | null} astronomicalDusk - The time when the sun is 18 degrees below the horizon in the evening.
18 */
19export type AstroResult = {
20 sunrise: Date | null;
21 sunset: Date | null;
22 moonrise: Date | null;
23 moonset: Date | null;
24 phase: number; // 0..1
25 phaseName: string;
26 illumination: number; // 0..1 fraction lit
27 // Twilight times
28 civilDawn: Date | null; // Sun -6° below horizon -> start of civil twilight
29 civilDusk: Date | null; // Sun -6° below horizon -> end of civil twilight
30 nauticalDawn: Date | null; // Sun -12° -> start of nautical twilight
31 nauticalDusk: Date | null; // Sun -12° -> end of nautical twilight
32 astronomicalDawn: Date | null;// Sun -18° -> start of astronomical twilight (night ends)
33 astronomicalDusk: Date | null;// Sun -18° -> end of astronomical twilight (night begins)
34};
35
36/**
37 * Gets the name of the moon phase for a given phase value.
38 * @param {number} phase - The moon phase, from 0.0 (new moon) to 1.0 (new moon).
39 * @param {string} [locale="en"] - The locale to use for the phase name (e.g., "en" or "de").
40 * @returns {string} The name of the moon phase.
41 */
42export function moonPhaseName(phase: number, locale: string = "en"): string {
43 // Phase: 0=new, 0.25=first quarter, 0.5=full, 0.75=last quarter
44 const namesEn = [
45 "New Moon",
46 "Waxing Crescent",
47 "First Quarter",
48 "Waxing Gibbous",
49 "Full Moon",
50 "Waning Gibbous",
51 "Last Quarter",
52 "Waning Crescent"
53 ];
54 const namesDe = [
55 "Neumond",
56 "Zunehmende Sichel",
57 "Erstes Viertel",
58 "Zunehmender Mond",
59 "Vollmond",
60 "Abnehmender Mond",
61 "Letztes Viertel",
62 "Abnehmende Sichel"
63 ];
64 const idx = Math.round(((phase % 1 + 1) % 1) * 7) as 0|1|2|3|4|5|6|7;
65 return (locale?.startsWith("de") ? namesDe : namesEn)[idx];
66}
67
68/**
69 * Computes astronomical data for a given latitude, longitude, and date.
70 * @param {number} lat - The latitude.
71 * @param {number} lon - The longitude.
72 * @param {Date} [date=new Date()] - The date for the calculation.
73 * @param {string} [locale="en"] - The locale for the moon phase name.
74 * @returns {AstroResult} An object containing the astronomical data.
75 */
76export function computeAstro(lat: number, lon: number, date: Date = new Date(), locale: string = "en"): AstroResult {
77 const times = SunCalc.getTimes(date, lat, lon);
78 const mt = SunCalc.getMoonTimes(date, lat, lon, true /* UTC to avoid host tz issues */);
79 const ill = SunCalc.getMoonIllumination(date);
80 return {
81 sunrise: times.sunrise ?? null,
82 sunset: times.sunset ?? null,
83 moonrise: mt.rise ?? null,
84 moonset: mt.set ?? null,
85 phase: ill.phase,
86 illumination: ill.fraction,
87 phaseName: moonPhaseName(ill.phase, locale),
88 // Twilight mappings according to suncalc docs
89 civilDawn: (times as any).dawn ?? null,
90 civilDusk: (times as any).dusk ?? null,
91 nauticalDawn: (times as any).nauticalDawn ?? null,
92 nauticalDusk: (times as any).nauticalDusk ?? null,
93 astronomicalDawn: (times as any).nightEnd ?? null,
94 astronomicalDusk: (times as any).night ?? null,
95 };
96}
97
98/**
99 * Formats a date object into a time string (HH:mm).
100 * @param {Date | null} d - The date to format.
101 * @param {string} [tz] - The time zone to use.
102 * @param {string} [locale="en"] - The locale to use for formatting.
103 * @returns {string} The formatted time string, or "—" if the date is null.
104 */
105export function formatTime(d: Date | null, tz?: string, locale: string = "en"): string {
106 if (!d) return "—";
107 try {
108 return new Intl.DateTimeFormat(locale || "en", {
109 hour: "2-digit",
110 minute: "2-digit",
111 hour12: false,
112 timeZone: tz || undefined
113 }).format(d);
114 } catch {
115 const hh = String(d.getHours()).padStart(2, "0");
116 const mm = String(d.getMinutes()).padStart(2, "0");
117 return `${hh}:${mm}`;
118 }
119}
120
121/**
122 * Calculates the percentage of the day that has passed for a given date and time zone.
123 * @param {Date} d - The date object.
124 * @param {string} [tz] - The time zone to use.
125 * @returns {number} The percentage of the day passed, from 0.0 to 1.0.
126 */
127export function timeOfDayPercent(d: Date, tz?: string): number {
128 // returns 0..1 position within day for the given date in tz
129 try {
130 const parts = new Intl.DateTimeFormat("en-GB", {
131 timeZone: tz || undefined,
132 hourCycle: "h23",
133 hour: "2-digit",
134 minute: "2-digit",
135 second: "2-digit"
136 }).formatToParts(d);
137 const h = Number(parts.find(p => p.type === "hour")?.value || 0);
138 const m = Number(parts.find(p => p.type === "minute")?.value || 0);
139 const s = Number(parts.find(p => p.type === "second")?.value || 0);
140 return (h * 3600 + m * 60 + s) / 86400;
141 } catch {
142 const h = d.getHours();
143 const m = d.getMinutes();
144 const s = d.getSeconds();
145 return (h * 3600 + m * 60 + s) / 86400;
146 }
147}