diff --git a/sternwarte-homeassistant-card/README.md b/sternwarte-homeassistant-card/README.md index 3a04bc4..5da2e3e 100644 --- a/sternwarte-homeassistant-card/README.md +++ b/sternwarte-homeassistant-card/README.md @@ -6,9 +6,12 @@ Lovelace-Card für die Sternwarte. ```text dist/ + astronomy.browser.min.js sternwarte-card.js ``` +`sternwarte-card.js` lädt `astronomy.browser.min.js` automatisch nach, um Dämmerungszeiten und Mondposition präziser zu berechnen. + ## Für HACS Dieser Ordner ist als Root eines späteren HACS-Dashboard-Repositorys vorbereitet. diff --git a/sternwarte-homeassistant-card/dist/sternwarte-card.js b/sternwarte-homeassistant-card/dist/sternwarte-card.js index e4bf57d..9d32ea4 100644 --- a/sternwarte-homeassistant-card/dist/sternwarte-card.js +++ b/sternwarte-homeassistant-card/dist/sternwarte-card.js @@ -14,6 +14,14 @@ const DEFAULT_ENTITIES = { astroLon: 11.0481766, }; +const ASTRONOMY_LIB_URL = (() => { + try { + return new URL("./astronomy.browser.min.js", import.meta.url).toString(); + } catch (error) { + return null; + } +})(); + class SternwarteCard extends HTMLElement { constructor() { super(); @@ -22,9 +30,11 @@ class SternwarteCard extends HTMLElement { close: { draft: "", editing: false, pending: false }, }; this._pendingRender = false; + this._astronomyLoadPromise = null; } connectedCallback() { + this._ensureAstronomy(); this._ensureClockTimer(); } @@ -71,6 +81,46 @@ class SternwarteCard extends HTMLElement { }, 30000); } + _astronomy() { + return window.Astronomy || null; + } + + _ensureAstronomy() { + const astronomy = this._astronomy(); + if (astronomy) return Promise.resolve(astronomy); + if (this._astronomyLoadPromise) return this._astronomyLoadPromise; + if (!ASTRONOMY_LIB_URL) return Promise.resolve(null); + + this._astronomyLoadPromise = new Promise((resolve) => { + const finish = () => { + const loaded = this._astronomy(); + if (loaded && this._hass) { + this._requestRender(); + } + this._astronomyLoadPromise = null; + resolve(loaded || null); + }; + + const existing = Array.from(document.querySelectorAll("script")) + .find((script) => script.src === ASTRONOMY_LIB_URL); + + if (existing) { + existing.addEventListener("load", finish, { once: true }); + existing.addEventListener("error", finish, { once: true }); + return; + } + + const script = document.createElement("script"); + script.src = ASTRONOMY_LIB_URL; + script.async = true; + script.addEventListener("load", finish, { once: true }); + script.addEventListener("error", finish, { once: true }); + document.head.appendChild(script); + }); + + return this._astronomyLoadPromise; + } + _requestRender() { if (this._isScheduleEditing()) { this._pendingRender = true; @@ -389,107 +439,83 @@ class SternwarteCard extends HTMLElement { return date >= lastSunday(2, 1) && date < lastSunday(9, 1); } - _calcTwilight(date) { + _astroObserver() { + const astronomy = this._astronomy(); + if (!astronomy) return null; const lat = Number(this.config.astroLat); const lon = Number(this.config.astroLon); - const jd = date.getTime() / 86400000 + 2440587.5; - const n = jd - 2451545.0; - const L = ((280.460 + 0.9856474 * n) % 360 + 360) % 360; - const g = ((357.528 + 0.9856003 * n) % 360 + 360) % 360; - const gR = g * Math.PI / 180; - const lam = L + 1.915 * Math.sin(gR) + 0.020 * Math.sin(2 * gR); - const lamR = lam * Math.PI / 180; - const eps = 23.439 - 0.0000004 * n; - const epsR = eps * Math.PI / 180; - const dec = Math.asin(Math.sin(epsR) * Math.sin(lamR)); - const RA = Math.atan2(Math.cos(epsR) * Math.sin(lamR), Math.cos(lamR)) * 180 / Math.PI / 15; - const GMST0h = ((L + 180) % 360 + 360) % 360 / 15; - const solarNoon = (12 - lon / 15 - (RA - GMST0h + 12 + 720) % 24 + 24) % 24; - const latR = lat * Math.PI / 180; - - const eventUTC = (depDeg) => { - const cosH = (Math.sin(-depDeg * Math.PI / 180) - Math.sin(latR) * Math.sin(dec)) - / (Math.cos(latR) * Math.cos(dec)); - if (cosH < -1 || cosH > 1) return null; - const H = Math.acos(cosH) * 180 / Math.PI / 15; - return { dawn: (solarNoon - H + 24) % 24, dusk: (solarNoon + H) % 24 }; - }; - - return { - sun: eventUTC(0.833), - civil: eventUTC(6), - naut: eventUTC(12), - astro: eventUTC(18), - }; + if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null; + return new astronomy.Observer(lat, lon, 0); } - _julianDate(date) { - return date.getTime() / 86400000 + 2440587.5; + _astroTimeToUtcHour(eventTime) { + if (!eventTime) return null; + const date = eventTime.date instanceof Date ? eventTime.date : null; + if (!date || Number.isNaN(date.getTime())) return null; + return date.getUTCHours() + date.getUTCMinutes() / 60 + date.getUTCSeconds() / 3600; } - _gmstHours(jd) { - const T = (jd - 2451545) / 36525; - const gmst = 280.46061837 + 360.98564736629 * (jd - 2451545) + 0.000387933 * T * T - - (T * T * T) / 38710000; - return (((gmst % 360) + 360) % 360) / 15; + _calcTwilight(date) { + const astronomy = this._astronomy(); + const observer = this._astroObserver(); + if (!astronomy || !observer) { + return { + sun: { dawn: null, dusk: null }, + civil: { dawn: null, dusk: null }, + naut: { dawn: null, dusk: null }, + astro: { dawn: null, dusk: null }, + }; + } + + try { + const bodySun = astronomy.Body?.Sun ?? "Sun"; + const start = new Date(date); + start.setHours(0, 0, 0, 0); + const limitDays = 2; + const altitudeEvent = (altitude) => ({ + dawn: this._astroTimeToUtcHour(astronomy.SearchAltitude(bodySun, observer, +1, start, limitDays, altitude)), + dusk: this._astroTimeToUtcHour(astronomy.SearchAltitude(bodySun, observer, -1, start, limitDays, altitude)), + }); + + return { + sun: { + dawn: this._astroTimeToUtcHour(astronomy.SearchRiseSet(bodySun, observer, +1, start, limitDays, 0)), + dusk: this._astroTimeToUtcHour(astronomy.SearchRiseSet(bodySun, observer, -1, start, limitDays, 0)), + }, + civil: altitudeEvent(-6), + naut: altitudeEvent(-12), + astro: altitudeEvent(-18), + }; + } catch (error) { + return { + sun: { dawn: null, dusk: null }, + civil: { dawn: null, dusk: null }, + naut: { dawn: null, dusk: null }, + astro: { dawn: null, dusk: null }, + }; + } } _moonStateAt(date) { - const jd = this._julianDate(date); - const d = jd - 2451543.5; - const lat = Number(this.config.astroLat) * Math.PI / 180; - const lonHours = Number(this.config.astroLon) / 15; - const toRad = (deg) => deg * Math.PI / 180; - const normalizeDeg = (deg) => ((deg % 360) + 360) % 360; + const astronomy = this._astronomy(); + const observer = this._astroObserver(); + if (!astronomy || !observer) { + return { altitude: Number.NEGATIVE_INFINITY, illumination: 0 }; + } - const sunMeanLongitude = normalizeDeg(280.460 + 0.9856474 * (jd - 2451545.0)); - const sunMeanAnomaly = normalizeDeg(357.528 + 0.9856003 * (jd - 2451545.0)); - const sunLongitude = normalizeDeg( - sunMeanLongitude + 1.915 * Math.sin(toRad(sunMeanAnomaly)) + 0.020 * Math.sin(2 * toRad(sunMeanAnomaly)), - ); + try { + const bodyMoon = astronomy.Body?.Moon ?? "Moon"; + const equator = astronomy.Equator(bodyMoon, date, observer, true, true); + const horizon = astronomy.Horizon(date, observer, equator.ra, equator.dec, "normal"); + const illumination = astronomy.Illumination(bodyMoon, date); - const N = normalizeDeg(125.1228 - 0.0529538083 * d); - const i = toRad(5.1454); - const w = normalizeDeg(318.0634 + 0.1643573223 * d); - const a = 60.2666; - const e = 0.0549; - const M = normalizeDeg(115.3654 + 13.0649929509 * d); - const E = M + (180 / Math.PI) * e * Math.sin(toRad(M)) * (1 + e * Math.cos(toRad(M))); - const xv = a * (Math.cos(toRad(E)) - e); - const yv = a * (Math.sqrt(1 - e * e) * Math.sin(toRad(E))); - const v = Math.atan2(yv, xv); - const r = Math.sqrt(xv * xv + yv * yv); - - const xh = r * (Math.cos(toRad(N)) * Math.cos(v + toRad(w)) - Math.sin(toRad(N)) * Math.sin(v + toRad(w)) * Math.cos(i)); - const yh = r * (Math.sin(toRad(N)) * Math.cos(v + toRad(w)) + Math.cos(toRad(N)) * Math.sin(v + toRad(w)) * Math.cos(i)); - const zh = r * Math.sin(v + toRad(w)) * Math.sin(i); - - const moonLon = Math.atan2(yh, xh); - const moonLat = Math.atan2(zh, Math.sqrt(xh * xh + yh * yh)); - const obliquity = toRad(23.4393 - 0.0000004 * d); - - const xe = r * Math.cos(moonLon) * Math.cos(moonLat); - const ye = r * Math.sin(moonLon) * Math.cos(moonLat); - const ze = r * Math.sin(moonLat); - - const xeq = xe; - const yeq = ye * Math.cos(obliquity) - ze * Math.sin(obliquity); - const zeq = ye * Math.sin(obliquity) + ze * Math.cos(obliquity); - - const raHours = (((Math.atan2(yeq, xeq) * 180 / Math.PI) / 15) + 24) % 24; - const dec = Math.atan2(zeq, Math.sqrt(xeq * xeq + yeq * yeq)); - const lstHours = (this._gmstHours(jd) + lonHours + 24) % 24; - const hourAngle = toRad((lstHours - raHours) * 15); - const altitude = Math.asin( - Math.sin(dec) * Math.sin(lat) + Math.cos(dec) * Math.cos(lat) * Math.cos(hourAngle), - ); - - const elongation = Math.acos( - Math.cos(moonLat) * Math.cos(moonLon - toRad(sunLongitude)), - ); - const illumination = Math.max(0, Math.min(1, (1 - Math.cos(elongation)) / 2)); - - return { altitude, illumination }; + return { + altitude: (Number(horizon?.altitude) || 0) * Math.PI / 180, + illumination: Math.max(0, Math.min(1, Number(illumination?.phase_fraction) || 0)), + }; + } catch (error) { + return { altitude: Number.NEGATIVE_INFINITY, illumination: 0 }; + } } _moonTrackState() {