Skip to content

Instantly share code, notes, and snippets.

@HendrikRunte
Last active September 9, 2024 18:10
Show Gist options
  • Save HendrikRunte/4b5d03cb26e31508bc96553ad3c10f47 to your computer and use it in GitHub Desktop.
Save HendrikRunte/4b5d03cb26e31508bc96553ad3c10f47 to your computer and use it in GitHub Desktop.
Scriptable.app widget displaying the exact time of today's sunrise and sunset. Which comes in handy in the wintertime …
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: orange; icon-glyph: sun;
///////////////////////////////////////////////////////////////////////
// dawn2dusk.js
// Origin:
// https://gist.github.com/HendrikRunte/4b5d03cb26e31508bc96553ad3c10f47
// Take it and have fun.
// Hendrik Runte, Nov 12, 2020, 17:33.
///////////////////////////////////////////////////////////////////////
// Extending the JavaScritp Date object.
// Usage:
// const sunriseDateObject = new Date().sunrise(lat, long);
// const sunsetDateObject = new Date().sunrise(lat, long);
// All the other methods just help.
Date.prototype.sunrise = function (latitude, longitude, zenith) {
return this.setSun(latitude, longitude, true, zenith);
};
Date.prototype.sunset = function (latitude, longitude, zenith) {
return this.setSun(latitude, longitude, false, zenith);
};
Date.prototype.setSun = function (latitude, longitude, isSunrise, zenith) {
zenith = zenith || 90.8333;
const DEGREES_PER_HOUR = 360 / 24;
const hoursFromMeridian = longitude / DEGREES_PER_HOUR;
const dayOfYear = this.getDayOfYear();
const approxTimeOfEventInDays = isSunrise
? dayOfYear + (6 - hoursFromMeridian) / 24
: dayOfYear + (18 - hoursFromMeridian) / 24;
const sunMeanAnomaly = 0.9856 * approxTimeOfEventInDays - 3.289;
const sunTrueLongitude = Math.mod(
sunMeanAnomaly +
1.916 * Math.sinDeg(sunMeanAnomaly) +
0.02 * Math.sinDeg(2 * sunMeanAnomaly) +
282.634,
360
);
const ascension = 0.91764 * Math.tanDeg(sunTrueLongitude);
let rightAscension = (360 / (2 * Math.PI)) * Math.atan(ascension);
rightAscension = Math.mod((360 / (2 * Math.PI)) * Math.atan(ascension), 360);
const lQuadrant = Math.floor(sunTrueLongitude / 90) * 90;
const raQuadrant = Math.floor(rightAscension / 90) * 90;
rightAscension = rightAscension + (lQuadrant - raQuadrant);
rightAscension /= DEGREES_PER_HOUR;
const sinDec = 0.39782 * Math.sinDeg(sunTrueLongitude);
const cosDec = Math.cosDeg(Math.asinDeg(sinDec));
const cosLocalHourAngle =
(Math.cosDeg(zenith) - sinDec * Math.sinDeg(latitude)) /
(cosDec * Math.cosDeg(latitude));
const localHourAngle = Math.acosDeg(cosLocalHourAngle);
const localHour = isSunrise
? (360 - localHourAngle) / DEGREES_PER_HOUR
: localHourAngle / DEGREES_PER_HOUR;
const localMeanTime =
localHour + rightAscension - 0.06571 * approxTimeOfEventInDays - 6.622;
let time = localMeanTime - longitude / DEGREES_PER_HOUR;
time = Math.mod(time, 24);
const midnight = new Date(0);
midnight.setUTCFullYear(this.getUTCFullYear());
midnight.setUTCMonth(this.getUTCMonth());
midnight.setUTCDate(this.getUTCDate());
const milli = midnight.getTime() + time * 60 * 60 * 1000;
return new Date(milli);
};
// Utility functions
Date.prototype.getDayOfYear = function () {
return Math.ceil((this - new Date(this.getFullYear(), 0, 1)) / 86400000);
};
Math.degToRad = function (num) {
return (num * Math.PI) / 180;
};
Math.radToDeg = function (radians) {
return (radians * 180.0) / Math.PI;
};
Math.sinDeg = function (deg) {
return Math.sin((deg * 2.0 * Math.PI) / 360.0);
};
Math.acosDeg = function (x) {
return (Math.acos(x) * 360.0) / (2 * Math.PI);
};
Math.asinDeg = function (x) {
return (Math.asin(x) * 360.0) / (2 * Math.PI);
};
Math.tanDeg = function (deg) {
return Math.tan((deg * 2.0 * Math.PI) / 360.0);
};
Math.cosDeg = function (deg) {
return Math.cos((deg * 2.0 * Math.PI) / 360.0);
};
Math.mod = function (a, b) {
let result = a % b;
if (result < 0) {
result += b;
}
return result;
};
///////////////////////////////////////////////////////////////////////
// Here comes the actual Scriptable widget stuff.
///////////////////////////////////////////////////////////////////////
function getMoonphase(dateObj) {
// Bluntly copied from https://gist.github.com/endel/dfe6bb2fbe679781948c
let c = 0;
let e = 0;
let jd = 0;
let b = 0;
let year = dateObj.getFullYear();
let month = dateObj.getMonth() + 1;
let day = dateObj.getDate();
if (month < 3) {
year--;
month += 12;
}
++month;
c = 365.25 * year;
e = 30.6 * month;
jd = c + e + day - 694039.09; // jd is total days elapsed
jd /= 29.5305882; // divide by the moon cycle
b = parseInt(jd); // int(jd) -> b, take integer part of jd
jd -= b; // subtract integer part to leave fractional part of original jd
b = Math.round(jd * 8); // scale fraction from 0-8 and round
if (b >= 8) {
b = 0; // 0 and 8 are the same so turn 8 into 0
}
return b;
}
// Helps adding icons from SF Symbols.
function addSymbol({
symbolName = 'applelogo',
stack,
color = Color.white(),
size = 20,
}) {
const icon = stack.addImage(SFSymbol.named(symbolName).image);
icon.tintColor = color;
icon.imageSize = new Size(size, size);
}
function getSunriseAndSunset(date, location) {
return {
location: location,
todaysSunrise: date.sunrise(location.latitude, location.longitude),
todaysSunset: date.sunset(location.latitude, location.longitude),
};
}
function displayLoadingIndicator() {
const listWidget = new ListWidget();
const gradient = new LinearGradient();
gradient.locations = [0, 1];
gradient.colors = [new Color('#000618'), new Color('#121A34')];
listWidget.backgroundGradient = gradient;
const iconStack = listWidget.addStack();
addSymbol({
symbolName: 'text.bubble',
stack: iconStack,
color: Color.white(),
size: 32,
});
listWidget.addSpacer(10);
const header = listWidget.addText('Das Widget');
header.font = Font.regularRoundedSystemFont(FONTSETTINGS.medium);
header.textColor = Color.white();
listWidget.addSpacer(2);
const footer = listWidget.addText('wird geladen …');
footer.font = Font.regularRoundedSystemFont(FONTSETTINGS.medium);
footer.textColor = Color.white();
return listWidget;
}
async function displaySunriseAndSunset(
{ location, todaysSunrise, todaysSunset },
locality = null
) {
const listWidget = new ListWidget();
let todaysDate = new Date(NOW);
let headerText = 'Sonnenlauf, heute';
let headerColor = Color.white();
const gradient = new LinearGradient();
const gradientByTime =
NOW >= todaysSunrise.getTime() - 900000 &&
NOW < todaysSunset.getTime() + 900000
? { gradientStart: '#093199', gradientStop: '#4C95FE' } // day
: { gradientStart: '#000618', gradientStop: '#121A34' }; // night
gradient.locations = [0, 1];
gradient.colors = [
new Color(gradientByTime.gradientStart),
new Color(gradientByTime.gradientStop),
];
listWidget.backgroundGradient = gradient;
// Is it before midnight but later than today's sunset
// we'll look at tomorrow:
if (
NOW <= todaysDate.setHours(23, 59, 59, 999) &&
NOW > todaysSunset.getTime()
) {
todaysDate = new Date(new Date().setDate(todaysDate.getDate() + 1)); // tomorrow
headerText = 'Sonnenlauf, morgen';
todaysSunrise = todaysDate.sunrise(location.latitude, location.longitude);
todaysSunset = todaysDate.sunset(location.latitude, location.longitude);
headerColor = Color.white();
}
const header = listWidget.addText(headerText.toUpperCase());
header.font = Font.regularRoundedSystemFont(FONTSETTINGS.small);
header.textColor = headerColor;
listWidget.addSpacer(12);
// Sunrise
const sunriseStack = listWidget.addStack();
const sunriseStackColor =
todaysSunrise.getTime() < NOW ? new Color('#ffffff99') : Color.white();
addSymbol({
symbolName: 'sunrise.fill',
stack: sunriseStack,
color: sunriseStackColor,
size: 26,
});
sunriseStack.addSpacer();
const sunriseLabel = sunriseStack.addText(
` ${todaysSunrise
.getHours()
.toString()
.replace(/^0(?:0:0?)?/, '')}:${('0' + todaysSunrise.getMinutes()).slice(
-2
)}`
);
sunriseLabel.font = Font.mediumRoundedSystemFont(FONTSETTINGS.big);
sunriseLabel.textColor = sunriseStackColor;
// Sunset
const sunsetStack = listWidget.addStack();
const sunsetStackColor =
todaysSunset.getTime() < NOW ? new Color('#ffffff99') : Color.white();
addSymbol({
symbolName: 'sunset.fill',
stack: sunsetStack,
color: sunsetStackColor,
size: 26,
});
sunsetStack.addSpacer();
const sunsetLabel = sunsetStack.addText(
` ${todaysSunset
.getHours()
.toString()
.replace(/^0(?:0:0?)?/, '')}:${('0' + todaysSunset.getMinutes()).slice(
-2
)}`
);
sunsetLabel.font = Font.mediumRoundedSystemFont(FONTSETTINGS.big);
sunsetLabel.textColor = sunsetStackColor;
listWidget.addSpacer(12);
// Footer:
const footerStack = listWidget.addStack();
addSymbol({
symbolName: locality ? 'location.fill' : 'arrowtriangle.right.circle',
stack: footerStack,
color: Color.white(),
size: 12,
});
const footerLabel = locality
? footerStack.addText(` ${locality.toUpperCase()}`)
: footerStack.addText(
` ${todaysDate.toLocaleDateString(undefined, {
weekday: 'short',
})}., ${todaysDate.toLocaleDateString(undefined, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
})}`
);
footerStack.addSpacer();
footerStack.addText(MOONICONS[getMoonphase(new Date())]);
footerLabel.font = Font.regularRoundedSystemFont(FONTSETTINGS.small);
footerLabel.textColor = Color.white();
// render
return listWidget;
}
// Locate yourself or use params.
async function getLocation() {
try {
if (args.widgetParameter) {
const fixedCoordinates = args.widgetParameter.split(',').map(parseFloat);
return { latitude: fixedCoordinates[0], longitude: fixedCoordinates[1] };
} else {
Location.setAccuracyToThreeKilometers();
return await Location.current();
}
} catch (e) {
return null;
}
}
async function getLocality(geolocation) {
let locality = null;
try {
// Location.reverseGeocode returns an array with object properties.
// Uses Apple CLLocation.
const address = await Location.reverseGeocode(
geolocation.latitude,
geolocation.longitude
);
// The order is relevant for processing the
// address properties.
const cascade = [
'ocean',
'inlandWater',
'administrativeArea',
'subAdministrativeArea',
'locality',
'subLocality',
];
if (address.length) {
cascade.forEach((prop) => {
locality = address[0][prop] ? address[0][prop] : locality;
});
}
return locality;
} catch (e) {
return null;
}
}
///////////////////////////////////////////////////////////////////////
let widget = {};
const FONTSETTINGS = {
big: 30,
medium: 16,
small: 9,
};
const NOW = +new Date();
const MOONICONS = ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'];
const location = await getLocation();
const locality = await getLocality(location);
if (location) {
const sunriseAndSunset = getSunriseAndSunset(new Date(NOW), location);
widget = await displaySunriseAndSunset(sunriseAndSunset, locality);
} else {
console.error(location);
console.error(locality);
widget = await displayLoadingIndicator();
}
if (!config.runsInWidget) {
await widget.presentSmall();
}
Script.setWidget(widget);
Script.complete();
@HendrikRunte
Copy link
Author

Ich forsche.

@zeron23
Copy link

zeron23 commented Nov 9, 2020

Ich forsche.

Benutze das Widget in einem Stapel und habe das mehrmals täglich wenn ich innerhalb des Stapels zu dem Widget schalte. Nachdem ich das Script in der Scriptable-App ausführe, wird es normal angezeigt.
Weil aber mehrmals am Tag eine Fehlermeldung auf meinem Homescreen auftaucht, ist das doch ganz schön unbefriedigend. Sind doch Widgets dazu da, gleich Informationen zu erhalten ohne erstmal eine App öffnen zu müssen.
Ich hoffe dafür wird eine Lösung gefunden, ist nämlich ein schönes Widget, was ich gerne dauerhaft benutzen würde 🙂👍🏻

@HendrikRunte
Copy link
Author

Ich musste die Geolokalisierung zur Ermittlung der Adresse leider rausnehmen, denn manchmal funktioniert diese Methode nicht richtig. Was aber für das Widget keinen entscheidenden Nachteil bedeutet, denn die Geopositionierung (vulgo GPS) funktioniert tadellos. Update!

IMG_2D732736EFAF-1

@zeron23
Copy link

zeron23 commented Nov 10, 2020

Jetzt gibts mehrmals täglich diesen Fehler:

B8998DE1-502B-4BCB-9894-2A1B6D465A63

@HendrikRunte
Copy link
Author

Jetzt gibts mehrmals täglich diesen Fehler:

B8998DE1-502B-4BCB-9894-2A1B6D465A63

Ja, vielen Dank. Das beobachte ich ähnlich. Allerdings ist bei mir die Fehlermeldung nach einem Augenblick verschwunden und das Widget funktioniert dann normal. Ich vermute, dass Scriptable irgendwann den Location-Service stoppt. Aber mal sehen, ich teste gerade eine andere Version.

@HendrikRunte
Copy link
Author

Jetzt wird ein solcher Fehler, wenn er auftritt, mit einer Hinweismeldung abgefangen. Kurz danach lädt das Widget dann normal.

@zeron23
Copy link

zeron23 commented Nov 11, 2020

Okay 👍🏻 Danke für die Mühe. Sieht jetzt besser aus als eine Fehlermeldung mit rotem Text.

Wenn es an der Ortung liegt weswegen die Fehler auftreten, wäre es eine Option dass man den Ort selbst als Parameter in den Widget-Einstellungen eingibt und das Widget auf diesen Ort zurückgreift sobald die Ortung fehlschlägt?

@HendrikRunte
Copy link
Author

Okay 👍🏻 Danke für die Mühe. Sieht jetzt besser aus als eine Fehlermeldung mit rotem Text.

Wenn es an der Ortung liegt weswegen die Fehler auftreten, wäre es eine Option dass man den Ort selbst als Parameter in den Widget-Einstellungen eingibt und das Widget auf diesen Ort zurückgreift sobald die Ortung fehlschlägt?

Das sollte bereits funktionieren. Gib die Koordinaten mit Dezimalpunkt, getrennt durch Komma an.

@HendrikRunte
Copy link
Author

Update: Die Ladeanzeige ist nun ein wenig schicker.

@HendrikRunte
Copy link
Author

Update: Die Ortsbestimmung ist nun wieder eingebaut (aber technisch etwas anders). Feedback Welcome!

@wodkasreineseele
Copy link

Ich habe es aktualisiert und werde es beobachten.
03F502BB-640E-4FE3-A1F9-F0D993883FD5

@frollein-ike
Copy link

Bei mir funktioniert es in der neuen Version prima. 😃👍🏼

@HendrikRunte
Copy link
Author

Bei mir funktioniert es in der neuen Version prima. 😃👍🏼

Danke, @frollein-ike, schön zu hören! Grüße in den Südwesten!

@wodkasreineseele
Copy link

Läuft weiterhin alles stabil.

@HendrikRunte
Copy link
Author

Läuft weiterhin alles stabil.

Danke für die Rückmeldung!

@monza258
Copy link

monza258 commented Jan 18, 2021

Hi, ist es möglich eine Benachrichtigung einzubauen? Ich möchte gerne das bei Sonnenuntergang eine Pushcut gesendet wird. So könnte ich Automationen erstellen.

If sunset then

const req2 = new Request('https://api.pushcut.io/-XXXXXXX/notifications/sunset')
   req2.method = 'POST'
    req2.headers = {
  'Content-Type': 'application/json'
}
req2.body = JSON.stringify({
  title: 'Sunset',
  text: 'Sunset',
  input: 'TEXT'
})
 
    await req2.loadJSON()
	}

Kann mir jemand sagen was ich wo einfügen müsste?

@HendrikRunte
Copy link
Author

Hi, ist es möglich eine Benachrichtigung einzubauen? Ich möchte gerne das bei Sonnenuntergang eine Pushcut gesendet wird. So könnte ich Automationen erstellen.

If sunset then

const req2 = new Request('https://api.pushcut.io/-XXXXXXX/notifications/sunset')
   req2.method = 'POST'
    req2.headers = {
  'Content-Type': 'application/json'
}
req2.body = JSON.stringify({
  title: 'Sunset',
  text: 'Sunset',
  input: 'TEXT'
})
 
    await req2.loadJSON()
	}

Kann mir jemand sagen was ich wo einfügen müsste?

Hi, das Widget hat derzeit kein Event wie »onSunset()«. Soweit ich weiß, wird es auch kein zuverlässiges EventHandling geben, wenn das Widget nicht gerade erst angezeigt wurde, da kein Hintergrundprozess läuft.

@p-schiko
Copy link

Sehr schönes Widget, aber leider zu wenig Platz auf dem iPhone 7. Könnte man (selber) die Schriftgrösse im Script anpassen oder ggf. auf US Uhrzeit umstellen?
0A2679C2-567C-42AA-A63B-2E2D398A0376

@HendrikRunte
Copy link
Author

Sehr schönes Widget, aber leider zu wenig Platz auf dem iPhone 7. Könnte man (selber) die Schriftgrösse im Script anpassen oder ggf. auf US Uhrzeit umstellen?
0A2679C2-567C-42AA-A63B-2E2D398A0376

Hi, ja klar: Schau einfach im untersten Block nach FONTSETTINGS. Da kannst Du Dir die Schriftgrösse bequem anpassen.

@twaldi23
Copy link

Wäre es aufwendig, die tägliche Tageslichtdauer unter dem Sonnenlauf anzuzeigen?

@HendrikRunte
Copy link
Author

HendrikRunte commented Oct 10, 2021

Wäre es aufwendig, die tägliche Tageslichtdauer unter dem Sonnenlauf anzuzeigen?

Eigentlich nicht, ist nur vielleicht ein Platzproblem.

@p-schiko
Copy link

p-schiko commented Oct 11, 2021 via email

@HendrikRunte
Copy link
Author

HendrikRunte commented Oct 11, 2021 via email

@twaldi23
Copy link

Wäre es aufwendig, die tägliche Tageslichtdauer unter dem Sonnenlauf anzuzeigen?

Eigentlich nicht, ist nur vielleicht ein Platzproblem.

das Sonnenlauf heute könnte man ja weglassen und dort die tägliche Tageslichtdauer platzieren

@Amarganth
Copy link

Hallo Hendrik
Danke erstmal für den Code. Da habe ich ein paar schlaue astronomische Algorithmen entdeckt. Und auch die Darstellung in Deinem Widget ist sehr schön gemacht.

Mir war die Anzeige der Mondicons in den acht möglichen Positionen etwas zu dünn. Ich habe daraufhin Dein Widget um die Mondhelligkeit aufgebohrt. Bist Du interessiert an diesem Code? Ich habe ihn von https://computus.de/mondphase/mondphase.htm adaptiert. Soll ich (wenn ich es denn kann (ich hab's noch nie gemacht)) den obigen Code entsprechend erweitern?

So sieht's bei mir aus:
IMG_2377

Lieben Gruss
Rolf

@HendrikRunte
Copy link
Author

Hallo Hendrik Danke erstmal für den Code. Da habe ich ein paar schlaue astronomische Algorithmen entdeckt. Und auch die Darstellung in Deinem Widget ist sehr schön gemacht.

Mir war die Anzeige der Mondicons in den acht möglichen Positionen etwas zu dünn. Ich habe daraufhin Dein Widget um die Mondhelligkeit aufgebohrt. Bist Du interessiert an diesem Code? Ich habe ihn von https://computus.de/mondphase/mondphase.htm adaptiert. Soll ich (wenn ich es denn kann (ich hab's noch nie gemacht)) den obigen Code entsprechend erweitern?

So sieht's bei mir aus: IMG_2377

Lieben Gruss Rolf

Moin Rolf,

tolle Sache! Du kannst einen eigenen »Fork« des Codes machen (Schaltfläche ganz oben), dann können andere, wenn Du möchtest, leicht von Deiner Entwicklung partizipieren.

Im konkreten Fall finde ich die Position ein wenig problematisch, denn bei langen Ortsnamen (Castrop-Rauxel, Kleinmachnow, Königs Wusterhausen, etc.) würde diese Fläche gebraucht. Bei »Sutz« ist es natürlich prima 👍

Liebe Grüße,
Hendrik

@sfksuperman
Copy link

I added this on my homescreen and it always shows "Das Widget wird geladen …" no matter what, I refreshed the widget and it shows momentarily the actual sunrise and sunset but leaving the homescreen for few seconds/mins, it again goes to "Das Widget wird geladen …" screen.
Weird! fix please.

@HendrikRunte
Copy link
Author

I added this on my homescreen and it always shows "Das Widget wird geladen …" no matter what, I refreshed the widget and it shows momentarily the actual sunrise and sunset but leaving the homescreen for few seconds/mins, it again goes to "Das Widget wird geladen …" screen. Weird! fix please.

Unable to reproduce, sorry @sfksuperman

@sfksuperman
Copy link

I added this on my homescreen and it always shows "Das Widget wird geladen …" no matter what, I refreshed the widget and it shows momentarily the actual sunrise and sunset but leaving the homescreen for few seconds/mins, it again goes to "Das Widget wird geladen …" screen. Weird! fix please.

Unable to reproduce, sorry @sfksuperman

Are you sure bro? Have you checked the widget let say after few hours? I hope you could reproduce it.
Also, apart from this, could you please align the time and sun icons for sunrise and sunset (i think they are not properly aligned)?

@HendrikRunte
Copy link
Author

If you would like to have things different, feel free to create your own fork @sfksuperman .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment