diff --git a/.gitignore b/.gitignore index e1365c2fe6..9111914b5c 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,4 @@ yarn.lock # Vitest temporary test files tests/**/.tmp/ +core diff --git a/css/icon/clear-day.svg b/css/icon/clear-day.svg new file mode 100644 index 0000000000..5bd98463da --- /dev/null +++ b/css/icon/clear-day.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/clear-night.svg b/css/icon/clear-night.svg new file mode 100644 index 0000000000..529e18ae0f --- /dev/null +++ b/css/icon/clear-night.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/cloudy.svg b/css/icon/cloudy.svg new file mode 100644 index 0000000000..63f0b56732 --- /dev/null +++ b/css/icon/cloudy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/fog.svg b/css/icon/fog.svg new file mode 100644 index 0000000000..124ae6b132 --- /dev/null +++ b/css/icon/fog.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/hail.svg b/css/icon/hail.svg new file mode 100644 index 0000000000..68f0bb6c7e --- /dev/null +++ b/css/icon/hail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/hot-day.svg b/css/icon/hot-day.svg new file mode 100644 index 0000000000..c2aa90734c --- /dev/null +++ b/css/icon/hot-day.svg @@ -0,0 +1 @@ + diff --git a/css/icon/i-rain.svg b/css/icon/i-rain.svg new file mode 100644 index 0000000000..456d69564c --- /dev/null +++ b/css/icon/i-rain.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/i-snow.svg b/css/icon/i-snow.svg new file mode 100644 index 0000000000..8bced52158 --- /dev/null +++ b/css/icon/i-snow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/i-wind.svg b/css/icon/i-wind.svg new file mode 100644 index 0000000000..ff2fc3f6f4 --- /dev/null +++ b/css/icon/i-wind.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/partly-cloudy-day.svg b/css/icon/partly-cloudy-day.svg new file mode 100644 index 0000000000..dd40d02a9d --- /dev/null +++ b/css/icon/partly-cloudy-day.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/partly-cloudy-night.svg b/css/icon/partly-cloudy-night.svg new file mode 100644 index 0000000000..bdbe5f7413 --- /dev/null +++ b/css/icon/partly-cloudy-night.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/rain.svg b/css/icon/rain.svg new file mode 100644 index 0000000000..f674ec91ba --- /dev/null +++ b/css/icon/rain.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/sleet.svg b/css/icon/sleet.svg new file mode 100644 index 0000000000..f37f877c6b --- /dev/null +++ b/css/icon/sleet.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/css/icon/snow.svg b/css/icon/snow.svg new file mode 100644 index 0000000000..e383f3015c --- /dev/null +++ b/css/icon/snow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/thunderstorm.svg b/css/icon/thunderstorm.svg new file mode 100644 index 0000000000..ce9b1f924c --- /dev/null +++ b/css/icon/thunderstorm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/tornado.svg b/css/icon/tornado.svg new file mode 100644 index 0000000000..38b77c94e2 --- /dev/null +++ b/css/icon/tornado.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/css/icon/wind.svg b/css/icon/wind.svg new file mode 100644 index 0000000000..25a6f3ed1b --- /dev/null +++ b/css/icon/wind.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/defaultmodules/newsfeed/newsfeed.js b/defaultmodules/newsfeed/newsfeed.js index 9a12a4b8ed..d60ec87cfc 100644 --- a/defaultmodules/newsfeed/newsfeed.js +++ b/defaultmodules/newsfeed/newsfeed.js @@ -270,6 +270,8 @@ Module.register("newsfeed", { return true; }, this); } + + newsItems.forEach((item) => { //Remove selected tags from the beginning of rss feed items (title or description) if (this.config.removeStartTags === "title" || this.config.removeStartTags === "both") { diff --git a/defaultmodules/weather/current.njk.wip b/defaultmodules/weather/current.njk.wip new file mode 100644 index 0000000000..6e29aa2611 --- /dev/null +++ b/defaultmodules/weather/current.njk.wip @@ -0,0 +1,104 @@ +{% macro humidity() %} + {% if current.humidity %} + {{ current.humidity | decimalSymbol }}  + {% endif %} +{% endmacro %} +{% if current %} + {% if not config.onlyTemp %} +
+ + + {{ current.windSpeed | unit("wind") | round }} + {% if config.showWindDirection %} + + {% if config.showWindDirectionAsArrow %} + + {% else %} + {{ current.cardinalWindDirection() | translate }} + {% endif %} +   + + {% endif %} + + {% if config.showHumidity === "wind" %} + {{ humidity() }} + {% endif %} + {% if config.showSun %} + + + {% if current.nextSunAction() === "sunset" %} + {{ current.sunset | formatTime }} + {% else %} + {{ current.sunrise | formatTime }} + {% endif %} + + {% endif %} + {% if config.showUVIndex %} + +
+ {{ current.uv_index }} + + {% endif %} +
+ {% endif %} +
+ {% if config.showIndoorTemperature and indoor.temperature or config.showIndoorHumidity and indoor.humidity %} + + + {% if config.showIndoorTemperature and indoor.temperature %} + + {{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }} + + {% endif %} + {% if config.showIndoorHumidity and indoor.humidity %} + + {{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }} + + {% endif %} + + {% endif %} + + {% if current.weatherType %} + + {{ current.iconHTML | safe }} + + {% endif %} + + + {{ current.temperature | roundValue | unit("temperature") | decimalSymbol }} + + + {% if config.showHumidity === "temp" %} + {{ humidity() }} + {% endif %} +
+ {% if (config.showFeelsLike or config.showPrecipitationAmount or config.showPrecipitationProbability) and not config.onlyTemp %} +
+ {% if config.showFeelsLike %} + + {% if config.showHumidity === "feelslike" %} + {{ humidity() }} + {% endif %} + {{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }} + +
+ {% endif %} + {% if config.showPrecipitationAmount and current.precipitationAmount %} + {{ "PRECIP_AMOUNT" | translate }} {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }} +
+ {% endif %} + {% if config.showPrecipitationProbability and current.precipitationProbability %} + {{ "PRECIP_POP" | translate }} {{ current.precipitationProbability }}% + {% endif %} +
+ {% endif %} + {% if config.showHumidity === "below" %} + {{ humidity() }} + {% endif %} +{% else %} +
{{ "LOADING" | translate }}
+{% endif %} + + diff --git a/defaultmodules/weather/custom-weather-icons.css b/defaultmodules/weather/custom-weather-icons.css new file mode 100644 index 0000000000..703a5eb498 --- /dev/null +++ b/defaultmodules/weather/custom-weather-icons.css @@ -0,0 +1,381 @@ +.icon { + position: relative; + display: inline-block; + width: 12em; + height: 10em; + font-size: 1em; /* control icon size here */ +} + +/* === Moon / Night icon (for clear-night, etc.) === */ +.icon.night { + position: relative; + display: inline-block; + width: 12em; /* match other icons */ + height: 10em; /* match other icons */ + font-size: 1em; +} + +.icon.night .moon { + position: absolute; + top: 50%; + left: 50%; + width: 5.5em; + height: 5.5em; + background: #fff; + border-radius: 50%; + box-shadow: inset -1em 1em 0 0 #aaa; + transform: translate(-50%, -50%); /* 👈 center it exactly like sun/cloud */ +} + +.icon.night .moon .crater { + position: absolute; + background: #ccc; + border-radius: 50%; + width: 0.8em; + height: 0.8em; + top: 1em; + left: 2.2em; + box-shadow: + 1.2em 0.8em 0 0.1em #bbb, + 0.4em 1.8em 0 0.1em #bbb; +} + +/* Fog icon from qPKoyB */ +.fog { + position: absolute; + top: 50%; + left: 50%; + margin: 1.75em 0 0; +} + +.fog::before, +.fog::after { + content: ""; + position: absolute; + left: 50%; + width: 5em; + height: 0.5em; + margin-left: -2.5em; + color: rgb(255 255 255 / 25%); + background: currentcolor; + border-radius: 0.5em; + animation: fog 6s infinite linear alternate; +} + +.fog::before { + top: 0.75em; +} + +.fog::after { + top: 1.5em; + animation-delay: -6s; +} + +/* Animation keyframe from the pen */ +@keyframes fog { + 0% { + transform: translateX(-0.5em); + } + + 100% { + transform: translateX(0.5em); + } +} + +.cloud { + position: absolute; + z-index: 1; + top: 50%; + left: 50%; + width: 3.6875em; + height: 3.6875em; + margin: -1.8438em; + background: currentcolor; + border-radius: 50%; + box-shadow: + -2.1875em 0.6875em 0 -0.6875em, + 2.0625em 0.9375em 0 -0.9375em, + 0 0 0 0.375em #fff, + -2.1875em 0.6875em 0 -0.3125em #fff, + 2.0625em 0.9375em 0 -0.5625em #fff; +} + +.cloud::after { + content: ""; + position: absolute; + bottom: 0; + left: -0.5em; + display: block; + width: 4.5625em; + height: 1em; + background: currentcolor; + box-shadow: 0 0.4375em 0 -0.0625em #fff; +} + +.cloud:nth-child(2) { + z-index: 0; + background: #fff; + box-shadow: + -2.1875em 0.6875em 0 -0.6875em #fff, + 2.0625em 0.9375em 0 -0.9375em #fff, + 0 0 0 0.375em #fff, + -2.1875em 0.6875em 0 -0.3125em #fff, + 2.0625em 0.9375em 0 -0.5625em #fff; + opacity: 0.3; + transform: scale(0.5) translate(6em, -3em); + animation: cloud 4s linear infinite; +} + +.cloud:nth-child(2)::after { + background: #fff; +} + +.sun { + position: absolute; + top: 50%; + left: 50%; + width: 2.5em; + height: 2.5em; + margin: -1.25em; + background: currentcolor; + border-radius: 50%; + box-shadow: 0 0 0 0.375em #fff; + animation: spin 12s infinite linear; +} + +.rays { + position: absolute; + top: -2em; + left: 50%; + display: block; + width: 0.375em; + height: 1.125em; + margin-left: -0.1875em; + background: #fff; + border-radius: 0.25em; + box-shadow: 0 5.375em #fff; +} + +.rays::before, +.rays::after { + content: ""; + position: absolute; + top: 0; + left: 0; + display: block; + width: 0.375em; + height: 1.125em; + transform: rotate(60deg); + transform-origin: 50% 3.25em; + background: #fff; + border-radius: 0.25em; + box-shadow: 0 5.375em #fff; +} + +.rays::before { + transform: rotate(120deg); +} + +.cloud + .sun { + margin: -2em 1em; +} + +.rain, +.lightning, +.snow { + position: absolute; + z-index: 2; + top: 50%; + left: 50%; + width: 3.75em; + height: 3.75em; + margin: 0.375em 0 0 -2em; + background: currentcolor; +} + +.rain::after { + content: ""; + position: absolute; + z-index: 2; + top: 50%; + left: 50%; + width: 1.125em; + height: 1.125em; + margin: -1em 0 0 -0.25em; + background: #0cf; + border-radius: 100% 0 60% 50% / 60% 0 100% 50%; + box-shadow: + 0.625em 0.875em 0 -0.125em rgb(255 255 255 / 20%), + -0.875em 1.125em 0 -0.125em rgb(255 255 255 / 20%), + -1.375em -0.125em 0 rgb(255 255 255 / 20%); + transform: rotate(-28deg); + animation: rain 3s linear infinite; +} + +.bolt { + position: absolute; + top: 50%; + left: 50%; + margin: -0.25em 0 0 -0.125em; + color: #fff; + opacity: 0.3; + animation: lightning 2s linear infinite; +} + +.bolt:nth-child(2) { + width: 0.5em; + height: 0.25em; + margin: -1.75em 0 0 -1.875em; + transform: translate(2.5em, 2.25em); + opacity: 0.2; + animation: lightning 1.5s linear infinite; +} + +.bolt::before, +.bolt::after { + content: ""; + position: absolute; + z-index: 2; + top: 50%; + left: 50%; + margin: -1.625em 0 0 -1.0125em; + border-top: 1.25em solid transparent; + border-right: 0.75em solid; + border-bottom: 0.75em solid; + border-left: 0.5em solid transparent; + transform: skewX(-10deg); +} + +.bolt::after { + margin: -0.25em 0 0 -0.25em; + border-top: 0.75em solid; + border-right: 0.5em solid transparent; + border-bottom: 1.25em solid transparent; + border-left: 0.75em solid; + transform: skewX(-10deg); +} + +.bolt:nth-child(2)::before { + margin: -0.75em 0 0 -0.5em; + border-top: 0.625em solid transparent; + border-right: 0.375em solid; + border-bottom: 0.375em solid; + border-left: 0.25em solid transparent; +} + +.bolt:nth-child(2)::after { + margin: -0.125em 0 0 -0.125em; + border-top: 0.375em solid; + border-right: 0.25em solid transparent; + border-bottom: 0.625em solid transparent; + border-left: 0.375em solid; +} + +.flake::before, +.flake::after { + content: "\2744"; + position: absolute; + top: 50%; + left: 50%; + margin: -1.025em 0 0 -1.0125em; + color: #fff; + line-height: 1em; + opacity: 0.2; + animation: spin 8s linear infinite reverse; +} + +.flake::after { + margin: 0.125em 0 0 -1em; + font-size: 1.5em; + opacity: 0.4; + animation: spin 14s linear infinite; +} + +.flake:nth-child(2)::before { + margin: -0.5em 0 0 0.25em; + font-size: 1.25em; + opacity: 0.2; + animation: spin 10s linear infinite; +} + +.flake:nth-child(2)::after { + margin: 0.375em 0 0 0.125em; + font-size: 2em; + opacity: 0.4; + animation: spin 16s linear infinite reverse; +} + +/* Animations */ + +@keyframes spin { + 100% { + transform: rotate(360deg); + } +} + +@keyframes cloud { + 0% { + opacity: 0; + } + + 50% { + opacity: 0.3; + } + + 100% { + opacity: 0; + transform: scale(0.5) translate(-200%, -3em); + } +} + +@keyframes rain { + 0% { + background: #0cf; + box-shadow: + 0.625em 0.875em 0 -0.125em rgb(255 255 255 / 20%), + -0.875em 1.125em 0 -0.125em rgb(255 255 255 / 20%), + -1.375em -0.125em 0 #0cf; + } + + 25% { + box-shadow: + 0.625em 0.875em 0 -0.125em rgb(255 255 255 / 20%), + -0.875em 1.125em 0 -0.125em #0cf, + -1.375em -0.125em 0 rgb(255 255 255 / 20%); + } + + 50% { + background: rgb(255 255 255 / 30%); + box-shadow: + 0.625em 0.875em 0 -0.125em #0cf, + -0.875em 1.125em 0 -0.125em rgb(255 255 255 / 20%), + -1.375em -0.125em 0 rgb(255 255 255 / 20%); + } + + 100% { + box-shadow: + 0.625em 0.875em 0 -0.125em rgb(255 255 255 / 20%), + -0.875em 1.125em 0 -0.125em rgb(255 255 255 / 20%), + -1.375em -0.125em 0 #0cf; + } +} + +@keyframes lightning { + 45% { + color: #fff; + background: #fff; + opacity: 0.2; + } + + 50% { + color: #0cf; + background: #0cf; + opacity: 1; + } + + 55% { + color: #fff; + background: #fff; + opacity: 0.2; + } +} diff --git a/defaultmodules/weather/custom-weather-icons.css.wip b/defaultmodules/weather/custom-weather-icons.css.wip new file mode 100644 index 0000000000..a32e955d0b --- /dev/null +++ b/defaultmodules/weather/custom-weather-icons.css.wip @@ -0,0 +1,370 @@ +.icon { + position: relative; + display: inline-block; + width: 12em; + height: 10em; + font-size: 1em; /* control icon size here */ +} + +.cloud { + position: absolute; + z-index: 1; + top: 50%; + left: 50%; + width: 3.6875em; + height: 3.6875em; + margin: -1.84375em; + background: #000; + border-radius: 50%; + box-shadow: + -2.1875em 0.6875em 0 -0.6875em #000, + 2.0625em 0.9375em 0 -0.9375em #000, + 0 0 0 0.375em #fff, + -2.1875em 0.6875em 0 -0.3125em #fff, + 2.0625em 0.9375em 0 -0.5625em #fff; +} +.cloud:after { + content: ''; + position: absolute; + bottom: 0; + left: -0.5em; + display: block; + width: 4.5625em; + height: 1em; + background: #000; + box-shadow: 0 0.4375em 0 -0.0625em #fff; +} +.cloud:nth-child(2) { + z-index: 0; + background: #fff; + box-shadow: + -2.1875em 0.6875em 0 -0.6875em #fff, + 2.0625em 0.9375em 0 -0.9375em #fff, + 0 0 0 0.375em #fff, + -2.1875em 0.6875em 0 -0.3125em #fff, + 2.0625em 0.9375em 0 -0.5625em #fff; + opacity: 0.3; + transform: scale(0.5) translate(6em, -3em); + animation: cloud 4s linear infinite; +} +.cloud:nth-child(2):after { background: #fff; } + +.sun { + position: absolute; + top: 50%; + left: 50%; + width: 2.5em; + height: 2.5em; + margin: -1.25em; + background: #000; + border-radius: 50%; + box-shadow: 0 0 0 0.375em #fff; + animation: spin 12s infinite linear; +} +.rays { + position: absolute; + top: -2em; + left: 50%; + display: block; + width: 0.375em; + height: 1.125em; + margin-left: -0.1875em; + background: #fff; + border-radius: 0.25em; + box-shadow: 0 5.375em #fff; +} +.rays:before, +.rays:after { + content: ''; + position: absolute; + top: 0em; + left: 0em; + display: block; + width: 0.375em; + height: 1.125em; + transform: rotate(60deg); + transform-origin: 50% 3.25em; + background: #fff; + border-radius: 0.25em; + box-shadow: 0 5.375em #fff; +} +.rays:before { + transform: rotate(120deg); +} +.cloud + .sun { + margin: -2em 1em; +} + +.fog { + position: absolute; + z-index: 2; + top: 50%; + left: 50%; + width: 3.75em; + height: 3.75em; + margin: 0.375em 0 0 -2em; + background: #000; +} + +.rain, +.lightning, +.snow { + position: absolute; + z-index: 2; + top: 50%; + left: 50%; + width: 3.75em; + height: 3.75em; + margin: 0.375em 0 0 -2em; + background: #000; +} + +.rain:after { + content: ''; + position: absolute; + z-index: 2; + top: 50%; + left: 50%; + width: 1.125em; + height: 1.125em; + margin: -1em 0 0 -0.25em; + background: #0cf; + border-radius: 100% 0 60% 50% / 60% 0 100% 50%; + box-shadow: + 0.625em 0.875em 0 -0.125em rgba(255,255,255,0.2), + -0.875em 1.125em 0 -0.125em rgba(255,255,255,0.2), + -1.375em -0.125em 0 rgba(255,255,255,0.2); + transform: rotate(-28deg); + animation: rain 3s linear infinite; +} + +.bolt { + position: absolute; + top: 50%; + left: 50%; + margin: -0.25em 0 0 -0.125em; + color: #fff; + opacity: 0.3; + animation: lightning 2s linear infinite; +} +.bolt:nth-child(2) { + width: 0.5em; + height: 0.25em; + margin: -1.75em 0 0 -1.875em; + transform: translate(2.5em, 2.25em); + opacity: 0.2; + animation: lightning 1.5s linear infinite; +} +.bolt:before, +.bolt:after { + content: ''; + position: absolute; + z-index: 2; + top: 50%; + left: 50%; + margin: -1.625em 0 0 -1.0125em; + border-top: 1.25em solid transparent; + border-right: 0.75em solid; + border-bottom: 0.75em solid; + border-left: 0.5em solid transparent; + transform: skewX(-10deg); +} +.bolt:after { + margin: -0.25em 0 0 -0.25em; + border-top: 0.75em solid; + border-right: 0.5em solid transparent; + border-bottom: 1.25em solid transparent; + border-left: 0.75em solid; + transform: skewX(-10deg); +} +.bolt:nth-child(2):before { + margin: -0.75em 0 0 -0.5em; + border-top: 0.625em solid transparent; + border-right: 0.375em solid; + border-bottom: 0.375em solid; + border-left: 0.25em solid transparent; +} +.bolt:nth-child(2):after { + margin: -0.125em 0 0 -0.125em; + border-top: 0.375em solid; + border-right: 0.25em solid transparent; + border-bottom: 0.625em solid transparent; + border-left: 0.375em solid; +} + +.flake:before, +.flake:after { + content: '\2744'; + position: absolute; + top: 50%; + left: 50%; + margin: -1.025em 0 0 -1.0125em; + color: #fff; + list-height: 1em; + opacity: 0.2; + animation: spin 8s linear infinite reverse; +} +.flake:after { + margin: 0.125em 0 0 -1em; + font-size: 1.5em; + opacity: 0.4; + animation: spin 14s linear infinite; +} +.flake:nth-child(2):before { + margin: -0.5em 0 0 0.25em; + font-size: 1.25em; + opacity: 0.2; + animation: spin 10s linear infinite; +} +.flake:nth-child(2):after { + margin: 0.375em 0 0 0.125em; + font-size: 2em; + opacity: 0.4; + animation: spin 16s linear infinite reverse; +} + +.moon { + position: absolute; + top: 50%; + left: 50%; + width: 3em; + height: 3em; + margin: -1.5em; + background: #000; + border-radius: 50%; + box-shadow: + inset -0.75em 0 rgba(255,255,255,0.2), + 0 0 0 0.375em #fff; +} + +.stars, +.stars:before, +.stars:after { + content: ''; + position: absolute; + top: 25%; + left: 70%; + width: 0.25em; + height: 0.25em; + margin: -0.125em; + color: rgba(255,255,255,0.5); + background: #000; + border-radius: 50%; + box-shadow: -4.5em 5em; + animation: twinkle 5s steps(2) infinite; +} + +.stars:before { + top: 1em; + left: 0.25em; + box-shadow: -3.75em 4em; + opacity: 0.5; + animation: twinkle 4s steps(2) infinite; +} + +.stars:after { + top: 0.5em; + left: -0.75em; + box-shadow: -3.75em 3.75em; + opacity: 0.25; + animation: twinkle 3s steps(2) infinite; +} + +/*.fog { + position: absolute; + top: 50%; + left: 50%; + margin: 1.75em 0 0 0; +}*/ + +.fog::before, +.fog::after { + content: ''; + position: absolute; + left: 50%; + width: 5em; + height: 0.5em; + margin-left: -2.5em; + color: rgba(255,255,255,0.25); + background: currentColor; + border-radius: 0.5em; + animation: fog 6s infinite linear alternate; +} + +.fog::before { top: 0.75em; } + +.fog::after { + top: 1.5em; + animation-delay: -6s; +} + +/* Animations */ + +@keyframes spin { + 100% { transform: rotate(360deg); } +} + +@keyframes cloud { + 0% { opacity: 0; } + 50% { opacity: 0.3; } + 100% { + opacity: 0; + transform: scale(0.5) translate(-200%, -3em); + } +} + +@keyframes rain { + 0% { + background: #0cf; + box-shadow: + 0.625em 0.875em 0 -0.125em rgba(255,255,255,0.2), + -0.875em 1.125em 0 -0.125em rgba(255,255,255,0.2), + -1.375em -0.125em 0 #0cf; + } + 25% { + box-shadow: + 0.625em 0.875em 0 -0.125em rgba(255,255,255,0.2), + -0.875em 1.125em 0 -0.125em #0cf, + -1.375em -0.125em 0 rgba(255,255,255,0.2); + } + 50% { + background: rgba(255,255,255,0.3); + box-shadow: + 0.625em 0.875em 0 -0.125em #0cf, + -0.875em 1.125em 0 -0.125em rgba(255,255,255,0.2), + -1.375em -0.125em 0 rgba(255,255,255,0.2); + } + 100% { + box-shadow: + 0.625em 0.875em 0 -0.125em rgba(255,255,255,0.2), + -0.875em 1.125em 0 -0.125em rgba(255,255,255,0.2), + -1.375em -0.125em 0 #0cf; + } +} + +@keyframes lightning { + 45% { + color: #fff; + background: #fff; + opacity: 0.2; + } + 50% { + color: #0cf; + background: #0cf; + opacity: 1; + } + 55% { + color: #fff; + background: #fff; + opacity: 0.2; + } +} + +@keyframes twinkle { + 100% { color: rgba(255,255,255,0.2); } +} + +@keyframes fog { + 0% { transform: translateX(-0.5em); } + 100% { transform: translateX(0.5em); } +} diff --git a/defaultmodules/weather/forecast.njk.wip b/defaultmodules/weather/forecast.njk.wip new file mode 100644 index 0000000000..dffaa9b0ce --- /dev/null +++ b/defaultmodules/weather/forecast.njk.wip @@ -0,0 +1,46 @@ +{% if forecast %} + {% set numSteps = forecast | calcNumSteps %} + {% set currentStep = 0 %} + + {% if config.ignoreToday %} + {% set forecast = forecast.splice(1) %} + {% endif %} + {% set forecast = forecast.slice(0, numSteps) %} + {% for f in forecast %} + + {% if (currentStep == 0) and config.ignoreToday == false and config.absoluteDates == false %} + + {% elif (currentStep == 1) and config.ignoreToday == false and config.absoluteDates == false %} + + {% else %} + + {% endif %} + + + + {% if config.showPrecipitationAmount %} + + {% endif %} + {% if config.showPrecipitationProbability %} + + {% endif %} + {% if config.showUVIndex %} + + {% endif %} + + {% set currentStep = currentStep + 1 %} + {% endfor %} +
{{ "TODAY" | translate }}{{ "TOMORROW" | translate }}{{ f.date.format("ddd") }} + {{ f.iconHTML | safe }} + {{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}{{ f.precipitationAmount | unit("precip", f.precipitationUnits) }}{{ f.precipitationProbability | unit('precip', '%') }} + {{ f.uv_index }} + +
+{% else %} +
{{ "LOADING" | translate }}
+{% endif %} + + diff --git a/defaultmodules/weather/weather.js.wip b/defaultmodules/weather/weather.js.wip new file mode 100644 index 0000000000..f028811962 --- /dev/null +++ b/defaultmodules/weather/weather.js.wip @@ -0,0 +1,421 @@ +/* global WeatherProvider, WeatherUtils, formatTime */ + +Module.register("weather", { + // Default module config. + defaults: { + weatherProvider: "openweathermap", + roundTemp: false, + type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint) + lang: config.language, + units: config.units, + tempUnits: config.units, + windUnits: config.units, + timeFormat: config.timeFormat, + updateInterval: 10 * 60 * 1000, // every 10 minutes + animationSpeed: 1000, + showFeelsLike: true, + showHumidity: "none", // possible options for "current" weather are "none", "wind", "temp", "feelslike" or "below", for "hourly" weather "none" or "true" + hideZeroes: false, // hide zeroes (and empty columns) in hourly, currently only for precipitation + showIndoorHumidity: false, + showIndoorTemperature: false, + allowOverrideNotification: false, + showPeriod: true, + showPeriodUpper: false, + showPrecipitationAmount: false, + showPrecipitationProbability: false, + showUVIndex: false, + showSun: true, + showWindDirection: true, + showWindDirectionAsArrow: false, + degreeLabel: false, + decimalSymbol: ".", + maxNumberOfDays: 5, + maxEntries: 5, + ignoreToday: false, + fade: true, + fadePoint: 0.25, // Start on 1/4th of the list. + initialLoadDelay: 0, // 0 seconds delay + appendLocationNameToHeader: true, + calendarClass: "calendar", + tableClass: "small", + onlyTemp: false, + colored: false, + absoluteDates: false, + hourlyForecastIncrements: 1 + }, + + // Module properties. + weatherProvider: null, + + // Can be used by the provider to display location of event if nothing else is specified + firstEvent: null, + + // Define required scripts. + getStyles () { + return ["font-awesome.css", "weather-icons.css", "weather.css", "custom-weather-icons.css"]; + }, + + // Return the scripts that are necessary for the weather module. + getScripts () { + return ["moment.js", "weatherutils.js", "weatherobject.js", this.file("providers/overrideWrapper.js"), "weatherprovider.js", "suncalc.js", this.file(`providers/${this.config.weatherProvider.toLowerCase()}.js`)]; + }, + + // Override getHeader method. + getHeader () { + if (this.config.appendLocationNameToHeader && this.weatherProvider) { + if (this.data.header) return `${this.data.header} ${this.weatherProvider.fetchedLocation()}`; + else return this.weatherProvider.fetchedLocation(); + } + + return this.data.header ? this.data.header : ""; + }, + + // Start the weather module. + start () { + moment.locale(this.config.lang); + + if (this.config.useKmh) { + Log.warn("Your are using the deprecated config values 'useKmh'. Please switch to windUnits!"); + this.windUnits = "kmh"; + } else if (this.config.useBeaufort) { + Log.warn("Your are using the deprecated config values 'useBeaufort'. Please switch to windUnits!"); + this.windUnits = "beaufort"; + } + if (typeof this.config.showHumidity === "boolean") { + Log.warn("[weather] Deprecation warning: Please consider updating showHumidity to the new style (config string)."); + this.config.showHumidity = this.config.showHumidity ? "wind" : "none"; + } + + // Initialize the weather provider. + this.weatherProvider = WeatherProvider.initialize(this.config.weatherProvider, this); + + // Let the weather provider know we are starting. + this.weatherProvider.start(); + + // Add custom filters + this.addFilters(); + + // Schedule the first update. + this.scheduleUpdate(this.config.initialLoadDelay); + }, + + // Override notification handler. + notificationReceived (notification, payload, sender) { + if (notification === "CALENDAR_EVENTS") { + const senderClasses = sender.data.classes.toLowerCase().split(" "); + if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) { + this.firstEvent = null; + for (let event of payload) { + if (event.location || event.geo) { + this.firstEvent = event; + Log.debug("First upcoming event with location: ", event); + break; + } + } + } + } else if (notification === "INDOOR_TEMPERATURE") { + this.indoorTemperature = this.roundValue(payload); + this.updateDom(300); + } else if (notification === "INDOOR_HUMIDITY") { + this.indoorHumidity = this.roundValue(payload); + this.updateDom(300); + } else if (notification === "CURRENT_WEATHER_OVERRIDE" && this.config.allowOverrideNotification) { + this.weatherProvider.notificationReceived(payload); + } + }, + + // Select the template depending on the display type. + getTemplate () { + switch (this.config.type.toLowerCase()) { + case "current": + return "current.njk"; + case "hourly": + return "hourly.njk"; + case "daily": + case "forecast": + return "forecast.njk"; + //Make the invalid values use the "Loading..." from forecast + default: + return "forecast.njk"; + } + }, + + // Add all the data to the template. + getTemplateData () { + const currentData = this.weatherProvider.currentWeather(); + const forecastData = this.weatherProvider.weatherForecast(); + + const mapIcon = (weatherType) => { + console.log("Weather type received:", weatherType); + switch (weatherType) { + case "day-sunny": + case "day-windy": + case "day-light-wind": + case "solar-eclipse": + case "hot": + return `
`; + case "lunar-eclipse": + case "stars": + return `
`; + case "day-rain": + case "day-rain-mix": + case "day-rain-wind": + case "day-showers": + case "day-sleet": + case "day-sprinkle": + case "day-sunny-overcast": + case "night-alt-rain": + case "night-alt-rain-mix": + case "night-alt-rain-wind": + case "night-alt-showers": + case "night-alt-sleet": + case "night-alt-sprinkle": + case "night-rain": + case "night-rain-mix": + case "night-rain-wind": + case "night-showers": + case "night-sleet": + case "night-sprinkle": + case "rain": + case "rain-mix": + case "rain-wind": + case "showers": + case "sleet": + case "sprinkle": + return `
`; + case "day-hail": + case "day-snow": + case "day-snow-thunderstorm": + case "day-snow-wind": + case "night-alt-hail": + case "night-alt-snow": + case "night-alt-snow-thunderstorm": + case "night-alt-snow-wind": + case "night-hail": + case "night-snow": + case "night-snow-thunderstorm": + case "night-snow-wind": + case "hail": + case "snow": + case "snow-wind": + return `
`; + case "day-lightning": + case "day-sleet-storm": + case "day-storm-showers": + case "day-thunderstorm": + case "night-alt-lightning": + case "night-alt-sleet-storm": + case "night-alt-storm-showers": + case "night-alt-thunderstorm": + case "night-lightning": + case "night-sleet-storm": + case "night-storm-showers": + case "night-thunderstorm": + case "storm-showers": + case "thunderstorm": + case "lightning": + return `
`; + case "day-cloudy": + case "day-cloudy-gusts": + case "day-cloud-windy": + case "day-cloudy-high": + case "night-alt-cloudy": + case "night-alt-cloudy-gusts": + case "night-alt-cloudy-windy": + case "night-alt-cloudy-high": + case "night-alt-partly-cloudy": + case "night-cloudy": + case "night-cloudy-gusts": + case "night-cloudy-windy": + case "night-partly-cloudy": + case "night-cloudy-high": + case "cloud": + case "cloudy": + case "cloudy-gusts": + case "cloudy-windy": + return `
`; + case "night-clear": + case "day-fog": + case "day-haze": + case "night-fog": + case "fog": + case "smog": + case "smoke ": + return `
`; + default: + return `
`; + } + }; + + if (forecastData) { + forecastData.forEach(f => { + console.log("Current weather object:", currentData); + f.iconHTML = mapIcon(f.weatherType); + }); + } + + // Add iconHTML to current weather + if (currentData) { + currentData.iconHTML = mapIcon(currentData.weatherType); + } + + // Skip some hourly forecast entries if configured + const hourlyData = this.weatherProvider.weatherHourly()?.filter((e, i) => (i + 1) % this.config.hourlyForecastIncrements === this.config.hourlyForecastIncrements - 1); + + return { + config: this.config, + current: currentData, + forecast: forecastData, + hourly: hourlyData, + indoor: { + humidity: this.indoorHumidity, + temperature: this.indoorTemperature + } + }; + }, + + // What to do when the weather provider has new information available? + updateAvailable () { + Log.log("New weather information available."); + // this value was changed from 0 to 300 to stabilize weather tests: + this.updateDom(300); + this.scheduleUpdate(); + + if (this.weatherProvider.currentWeather()) { + this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType?.replace("-", "_") }); + } + + const notificationPayload = { + currentWeather: this.config.units === "imperial" + ? WeatherUtils.convertWeatherObjectToImperial(this.weatherProvider?.currentWeatherObject?.simpleClone()) ?? null + : this.weatherProvider?.currentWeatherObject?.simpleClone() ?? null, + forecastArray: this.config.units === "imperial" + ? this.weatherProvider?.weatherForecastArray?.map((ar) => WeatherUtils.convertWeatherObjectToImperial(ar.simpleClone())) ?? [] + : this.weatherProvider?.weatherForecastArray?.map((ar) => ar.simpleClone()) ?? [], + hourlyArray: this.config.units === "imperial" + ? this.weatherProvider?.weatherHourlyArray?.map((ar) => WeatherUtils.convertWeatherObjectToImperial(ar.simpleClone())) ?? [] + : this.weatherProvider?.weatherHourlyArray?.map((ar) => ar.simpleClone()) ?? [], + locationName: this.weatherProvider?.fetchedLocationName, + providerName: this.weatherProvider.providerName + }; + + this.sendNotification("WEATHER_UPDATED", notificationPayload); + }, + + scheduleUpdate (delay = null) { + let nextLoad = this.config.updateInterval; + if (delay !== null && delay >= 0) { + nextLoad = delay; + } + + setTimeout(() => { + switch (this.config.type.toLowerCase()) { + case "current": + this.weatherProvider.fetchCurrentWeather(); + break; + case "hourly": + this.weatherProvider.fetchWeatherHourly(); + break; + case "daily": + case "forecast": + this.weatherProvider.fetchWeatherForecast(); + break; + default: + Log.error(`Invalid type ${this.config.type} configured (must be one of 'current', 'hourly', 'daily' or 'forecast')`); + } + }, nextLoad); + }, + + roundValue (temperature) { + const decimals = this.config.roundTemp ? 0 : 1; + const roundValue = parseFloat(temperature).toFixed(decimals); + return roundValue === "-0" ? 0 : roundValue; + }, + + addFilters () { + this.nunjucksEnvironment().addFilter( + "formatTime", + function (date) { + return formatTime(this.config, date); + }.bind(this) + ); + + this.nunjucksEnvironment().addFilter( + "unit", + function (value, type, valueUnit) { + let formattedValue; + if (type === "temperature") { + formattedValue = `${this.roundValue(WeatherUtils.convertTemp(value, this.config.tempUnits))}°`; + if (this.config.degreeLabel) { + if (this.config.tempUnits === "metric") { + formattedValue += "C"; + } else if (this.config.tempUnits === "imperial") { + formattedValue += "F"; + } else { + formattedValue += "K"; + } + } + } else if (type === "precip") { + if (value === null || isNaN(value)) { + formattedValue = ""; + } else { + formattedValue = WeatherUtils.convertPrecipitationUnit(value, valueUnit, this.config.units); + } + } else if (type === "humidity") { + formattedValue = `${value}%`; + } else if (type === "wind") { + formattedValue = WeatherUtils.convertWind(value, this.config.windUnits); + } + return formattedValue; + }.bind(this) + ); + + this.nunjucksEnvironment().addFilter( + "roundValue", + function (value) { + return this.roundValue(value); + }.bind(this) + ); + + this.nunjucksEnvironment().addFilter( + "decimalSymbol", + function (value) { + return value.toString().replace(/\./g, this.config.decimalSymbol); + }.bind(this) + ); + + this.nunjucksEnvironment().addFilter( + "calcNumSteps", + function (forecast) { + return Math.min(forecast.length, this.config.maxNumberOfDays); + }.bind(this) + ); + + this.nunjucksEnvironment().addFilter( + "calcNumEntries", + function (dataArray) { + return Math.min(dataArray.length, this.config.maxEntries); + }.bind(this) + ); + + this.nunjucksEnvironment().addFilter( + "opacity", + function (currentStep, numSteps) { + if (this.config.fade && this.config.fadePoint < 1) { + if (this.config.fadePoint < 0) { + this.config.fadePoint = 0; + } + const startingPoint = numSteps * this.config.fadePoint; + const numFadesteps = numSteps - startingPoint; + if (currentStep >= startingPoint) { + return 1 - (currentStep - startingPoint) / numFadesteps; + } else { + return 1; + } + } else { + return 1; + } + }.bind(this) + ); + } +});