From b1f0beaeb1a774d109b1118b7fde0d3a96d9dcff Mon Sep 17 00:00:00 2001 From: Evan Barbour Date: Sat, 13 Jan 2024 22:11:25 +0000 Subject: [PATCH 1/7] feat: create weather forecast component --- scripts/translations/fetch.py | 2 + src/lib/Modal/SidebarItemConfig.svelte | 12 + src/lib/Modal/WeatherForecastConfig.svelte | 108 +++++++++ src/lib/Sidebar/Index.svelte | 18 +- src/lib/Sidebar/WeatherForecast.svelte | 134 +++++++++++ src/lib/Types.ts | 10 + src/lib/Weather.ts | 253 +++++++++++++++++++++ 7 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 src/lib/Modal/WeatherForecastConfig.svelte create mode 100644 src/lib/Sidebar/WeatherForecast.svelte create mode 100644 src/lib/Weather.ts diff --git a/scripts/translations/fetch.py b/scripts/translations/fetch.py index 20115ca7..7fc8b82d 100755 --- a/scripts/translations/fetch.py +++ b/scripts/translations/fetch.py @@ -100,6 +100,8 @@ def process_dir(_dir, _output, _keys): ("return_home", ["ui.dialogs.more_info_control.vacuum.return_home"]), ("start_pause", ["ui.dialogs.more_info_control.vacuum.start_pause"]), ("battery", ["ui.dialogs.entity_registry.editor.device_classes.binary_sensor.battery"]), + ("weather_forecast", ["ui.panel.lovelace.editor.card.weather-forecast.name"]), + ("days_to_show", ["ui.panel.lovelace.editor.card.generic.days_to_show"]), ], ), ( # MEDIA_PLAYER diff --git a/src/lib/Modal/SidebarItemConfig.svelte b/src/lib/Modal/SidebarItemConfig.svelte index ecfb5918..5685e017 100644 --- a/src/lib/Modal/SidebarItemConfig.svelte +++ b/src/lib/Modal/SidebarItemConfig.svelte @@ -22,6 +22,7 @@ import Divider from '$lib/Sidebar/Divider.svelte'; import Navigate from '$lib/Sidebar/Navigate.svelte'; import Weather from '$lib/Sidebar/Weather.svelte'; + import WeatherForecast from '$lib/Sidebar/WeatherForecast.svelte'; import Iframe from '$lib/Sidebar/Iframe.svelte'; import Image from '$lib/Sidebar/Image.svelte'; import Camera from '$lib/Sidebar/Camera.svelte'; @@ -177,6 +178,14 @@ entity_id: 'weather.openweathermap' } }, + { + id: 'weatherforecast', + type: $lang('weather_forecast'), + component: WeatherForecast, + props: { + entity_id: 'weather.forecast_home' + } + }, { id: 'navigate', type: $lang('navigate'), @@ -227,6 +236,9 @@ case 'weather': openModal(() => import('$lib/Modal/WeatherConfig.svelte'), { sel }); break; + case 'weatherforecast': + openModal(() => import('$lib/Modal/WeatherForecastConfig.svelte'), { sel }); + break; case 'camera': openModal(() => import('$lib/Modal/CameraConfig.svelte'), { sel, diff --git a/src/lib/Modal/WeatherForecastConfig.svelte b/src/lib/Modal/WeatherForecastConfig.svelte new file mode 100644 index 00000000..6db5d49c --- /dev/null +++ b/src/lib/Modal/WeatherForecastConfig.svelte @@ -0,0 +1,108 @@ + + +{#if isOpen} + +

{$lang('weather_forecast')}

+ +

{$lang('preview')}

+ +
+ +
+ +

{$lang('entity')}

+ + {#if weatherStates} + set('icon_pack', event)} + /> + {/if} + +

{$lang('days_to_show')}

+ + {#if weatherStates} + + {/if} + + +
+{/if} diff --git a/src/lib/Sidebar/Index.svelte b/src/lib/Sidebar/Index.svelte index 5cf5d510..8ca34df5 100644 --- a/src/lib/Sidebar/Index.svelte +++ b/src/lib/Sidebar/Index.svelte @@ -28,6 +28,7 @@ let Time: typeof import('$lib/Sidebar/Time.svelte'); let Timer: typeof import('$lib/Sidebar/Timer.svelte'); let Weather: typeof import('$lib/Sidebar/Weather.svelte'); + let WeatherForecast: typeof import('$lib/Sidebar/WeatherForecast.svelte'); const importsMap = { bar: () => import('$lib/Sidebar/Bar.svelte').then((module) => (Bar = module)), @@ -45,7 +46,9 @@ template: () => import('$lib/Sidebar/Template.svelte').then((module) => (Template = module)), time: () => import('$lib/Sidebar/Time.svelte').then((module) => (Time = module)), timer: () => import('$lib/Sidebar/Timer.svelte').then((module) => (Timer = module)), - weather: () => import('$lib/Sidebar/Weather.svelte').then((module) => (Weather = module)) + weather: () => import('$lib/Sidebar/Weather.svelte').then((module) => (Weather = module)), + weatherforecast: () => + import('$lib/Sidebar/WeatherForecast.svelte').then((module) => (WeatherForecast = module)) }; $: if ($dashboard?.sidebar) importComponents(); @@ -114,6 +117,8 @@ openModal(() => import('$lib/Modal/TimerConfig.svelte'), { sel }); } else if (sel?.type === 'weather') { openModal(() => import('$lib/Modal/WeatherConfig.svelte'), { sel }); + } else if (sel?.type === 'weatherforecast') { + openModal(() => import('$lib/Modal/WeatherForecastConfig.svelte'), { sel }); } else { openModal(() => import('$lib/Modal/SidebarItemConfig.svelte'), { sel }); @@ -321,6 +326,17 @@ show_apparent={item?.show_apparent} /> + + + {:else if WeatherForecast && item?.type === 'weatherforecast'} + {/if} {/each} diff --git a/src/lib/Sidebar/WeatherForecast.svelte b/src/lib/Sidebar/WeatherForecast.svelte new file mode 100644 index 00000000..46788650 --- /dev/null +++ b/src/lib/Sidebar/WeatherForecast.svelte @@ -0,0 +1,134 @@ + + +{#if entity_state} +
+ {#each forecast as forecast, i} +
+ {new Intl.DateTimeFormat($selectedLanguage, { weekday: 'short' }).format( + new Date(forecast.date) + )} +
+ +
+ {#if forecast.icon.local} + + {entity_state} + + {:else} + + {/if} +
+ +
+ {Math.round(forecast.temperature)}{attributes?.temperature_unit || '°'} +
+ {/each} +
+{:else} +
+ {$lang('weather_forecast')} +
+{/if} + + diff --git a/src/lib/Types.ts b/src/lib/Types.ts index 1d3ad6b5..7a1f2983 100644 --- a/src/lib/Types.ts +++ b/src/lib/Types.ts @@ -83,6 +83,7 @@ export type SidebarItem = BarItem & TemplateItem & TimeItem & WeatherItem & + WeatherForecastItem & DividerItem; export interface BarItem { @@ -198,3 +199,12 @@ export interface WeatherItem { extra_sensor_icon?: string; show_apparent?: boolean; } + +export interface WeatherForecastItem { + type?: string; + id?: number; + entity_id?: string; + state?: string; + icon_pack?: string; + days_to_show?: number; +} diff --git a/src/lib/Weather.ts b/src/lib/Weather.ts new file mode 100644 index 00000000..283785de --- /dev/null +++ b/src/lib/Weather.ts @@ -0,0 +1,253 @@ +import { readable } from 'svelte/store'; + +// A set of icons, e.g. meteocons, weather icons +export interface WeatherIconSet { + type: string; + conditions: WeatherIconConditions; +} + +// Conditions as defined in +export interface WeatherIconConditions { + 'clear-night': WeatherIconMapping; + cloudy: WeatherIconMapping; + fog: WeatherIconMapping; + hail: WeatherIconMapping; + lightning: WeatherIconMapping; + 'lightning-rainy': WeatherIconMapping; + partlycloudy: WeatherIconMapping; + pouring: WeatherIconMapping; + rainy: WeatherIconMapping; + snowy: WeatherIconMapping; + 'snowy-rainy': WeatherIconMapping; + sunny: WeatherIconMapping; + windy: WeatherIconMapping; + 'windy-variant': WeatherIconMapping; + exceptional: WeatherIconMapping; +} + +// Icon variants for day & night. Icons which are either-or should be the same icon (clear-night, sunny, potentially others depending on the library) +export interface WeatherIconMapping { + local?: boolean; + icon_variant_day: string; + icon_variant_night: string; +} + +export const iconMapMaterialSymbolsLight: WeatherIconSet = { + type: 'materialsymbolslight', + conditions: { + 'clear-night': { + icon_variant_day: 'material-symbols-light:bedtime-outline-rounded', + icon_variant_night: 'material-symbols-light:bedtime-outline-rounded' + }, + cloudy: { + icon_variant_day: 'material-symbols-light:cloud-outline', + icon_variant_night: 'material-symbols-light:cloud-outline' + }, + exceptional: { + icon_variant_day: 'material-symbols-light:warning-outline-rounded', + icon_variant_night: 'material-symbols-light:warning-outline-rounded' + }, + fog: { + icon_variant_day: 'material-symbols-light:foggy-outline', + icon_variant_night: 'material-symbols-light:foggy-outline' + }, + hail: { + icon_variant_day: 'material-symbols-light:weather-hail-outline-rounded', + icon_variant_night: 'material-symbols-light:weather-hail-outline-rounded' + }, + lightning: { + icon_variant_day: 'material-symbols-light:thunderstorm-outline-rounded', + icon_variant_night: 'material-symbols-light:thunderstorm-outline-rounded' + }, + 'lightning-rainy': { + icon_variant_day: 'material-symbols-light:thunderstorm-outline-rounded', + icon_variant_night: 'material-symbols-light:thunderstorm-outline-rounded' + }, + partlycloudy: { + icon_variant_day: 'material-symbols-light:partly-cloudy-day-outline', + icon_variant_night: 'material-symbols-light:nights-stay-outline-rounded' + }, + pouring: { + icon_variant_day: 'material-symbols-light:rainy-outline', + icon_variant_night: 'material-symbols-light:rainy-outline' + }, + rainy: { + icon_variant_day: 'material-symbols-light:rainy-outline', + icon_variant_night: 'material-symbols-light:rainy-outline' + }, + snowy: { + icon_variant_day: 'material-symbols-light:cloudy-snowing-outline', + icon_variant_night: 'material-symbols-light:cloudy-snowing-outline' + }, + 'snowy-rainy': { + icon_variant_day: 'material-symbols-light:weather-mix-outline-rounded', + icon_variant_night: 'material-symbols-light:weather-mix-outline-rounded' + }, + sunny: { + icon_variant_day: 'material-symbols-light:sunny-outline-rounded', + icon_variant_night: 'material-symbols-light:sunny-outline-rounded' + }, + windy: { + icon_variant_day: 'material-symbols-light:mist', + icon_variant_night: 'material-symbols-light:mist' + }, + 'windy-variant': { + icon_variant_day: 'material-symbols-light:mistd', + icon_variant_night: 'material-symbols-light:mist' + } + } +}; + +export const iconMapMeteocons: WeatherIconSet = { + type: 'meteocons', + conditions: { + 'clear-night': { + local: true, + icon_variant_day: '/weather/meteocons/clear-night-day', + icon_variant_night: '/weather/meteocons/clear-night-night' + }, + cloudy: { + local: true, + icon_variant_day: '/weather/meteocons/cloudy-day', + icon_variant_night: '/weather/meteocons/cloudy-night' + }, + exceptional: { + local: true, + icon_variant_day: '/weather/meteocons/exceptional-day', + icon_variant_night: '/weather/meteocons/exceptional-night' + }, + fog: { + local: true, + icon_variant_day: '/weather/meteocons/fog-day', + icon_variant_night: '/weather/meteocons/fog-night' + }, + hail: { + local: true, + icon_variant_day: '/weather/meteocons/hail-day', + icon_variant_night: '/weather/meteocons/hail-night' + }, + lightning: { + local: true, + icon_variant_day: '/weather/meteocons/lightning-day', + icon_variant_night: '/weather/meteocons/lightning-night' + }, + 'lightning-rainy': { + local: true, + icon_variant_day: '/weather/meteocons/lightning-rainy-day', + icon_variant_night: '/weather/meteocons/lightning-rainy-night' + }, + partlycloudy: { + local: true, + icon_variant_day: '/weather/meteocons/partlycloudy-day', + icon_variant_night: '/weather/meteocons/partlycloudy-night' + }, + pouring: { + local: true, + icon_variant_day: '/weather/meteocons/pouring-day', + icon_variant_night: '/weather/meteocons/pouring-night' + }, + rainy: { + local: true, + icon_variant_day: '/weather/meteocons/rainy-day', + icon_variant_night: '/weather/meteocons/rainy-night' + }, + snowy: { + local: true, + icon_variant_day: '/weather/meteocons/snowy-day', + icon_variant_night: '/weather/meteocons/snowy-night' + }, + 'snowy-rainy': { + local: true, + icon_variant_day: '/weather/meteocons/snowy-rainy-day', + icon_variant_night: '/weather/meteocons/snowy-rainy-night' + }, + sunny: { + local: true, + icon_variant_day: '/weather/meteocons/sunny-day', + icon_variant_night: '/weather/meteocons/sunny-night' + }, + windy: { + local: true, + icon_variant_day: '/weather/meteocons/windy-day', + icon_variant_night: '/weather/meteocons/windy-night' + }, + 'windy-variant': { + local: true, + icon_variant_day: '/weather/meteocons/windy-variant-day', + icon_variant_night: '/weather/meteocons/windy-variant-night' + } + } +}; + +export const iconMapWeatherIcons: WeatherIconSet = { + type: 'weathericons', + conditions: { + 'clear-night': { + icon_variant_day: 'wi:night-clear', + icon_variant_night: 'wi:night-clear' + }, + cloudy: { + icon_variant_day: 'wi:day-cloudy', + icon_variant_night: 'wi:night-alt-cloudy' + }, + exceptional: { + icon_variant_day: 'wi:na', + icon_variant_night: 'wi:na' + }, + fog: { + icon_variant_day: 'wi:day-fog', + icon_variant_night: 'wi:night-fog' + }, + hail: { + icon_variant_day: 'wi:day-hail', + icon_variant_night: 'wi:night-alt-hail' + }, + lightning: { + icon_variant_day: 'wi:day-lightning', + icon_variant_night: 'wi:night-alt-lightning' + }, + 'lightning-rainy': { + icon_variant_day: 'wi:day-thunderstorm', + icon_variant_night: 'wi:night-alt-thunderstorm' + }, + partlycloudy: { + icon_variant_day: 'wi:day-cloudy', + icon_variant_night: 'wi:night-alt-cloudy' + }, + pouring: { + icon_variant_day: 'wi:day-rain', + icon_variant_night: 'wi:night-alt-rain' + }, + rainy: { + icon_variant_day: 'wi:day-rain', + icon_variant_night: 'wi:night-alt-rain' + }, + snowy: { + icon_variant_day: 'wi:day-snow', + icon_variant_night: 'wi:night-alt-snow' + }, + 'snowy-rainy': { + icon_variant_day: 'wi:day-sleet', + icon_variant_night: 'wi:night-alt-sleet' + }, + sunny: { + icon_variant_day: 'wi:day-sunny', + icon_variant_night: 'wi:day-sunny' + }, + windy: { + icon_variant_day: 'wi:strong-wind', + icon_variant_night: 'wi:strong-wind' + }, + 'windy-variant': { + icon_variant_day: 'wi:strong-wind', + icon_variant_night: 'wi:strong-wind' + } + } +}; + +// Weather icon mapping +export const iconMap = readable>({ + materialsymbolslight: iconMapMaterialSymbolsLight, + meteocons: iconMapMeteocons, + weathericons: iconMapWeatherIcons +}); From 3c5daaba096ebbfee86f76e61fd55f7d4f271d2d Mon Sep 17 00:00:00 2001 From: Evan Barbour Date: Sun, 14 Jan 2024 16:38:11 +0000 Subject: [PATCH 2/7] fix: forecast is undefined --- src/lib/Modal/WeatherForecastConfig.svelte | 7 ++++--- src/lib/Sidebar/WeatherForecast.svelte | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib/Modal/WeatherForecastConfig.svelte b/src/lib/Modal/WeatherForecastConfig.svelte index 6db5d49c..2e3af610 100644 --- a/src/lib/Modal/WeatherForecastConfig.svelte +++ b/src/lib/Modal/WeatherForecastConfig.svelte @@ -21,9 +21,10 @@ { id: 'weathericons', name: 'weather icons' } ]; - $: weatherStates = Object.keys($states) - .filter((key) => key.startsWith('weather.')) - .sort() + $: weatherStates = Object.keys( + Object.fromEntries(Object.entries($states) + .filter(([key, value]) => key.startsWith('weather.') && value?.attributes?.forecast)) + ).sort() .map((key) => ({ id: key, label: key })); const range = { diff --git a/src/lib/Sidebar/WeatherForecast.svelte b/src/lib/Sidebar/WeatherForecast.svelte index 46788650..24c398a8 100644 --- a/src/lib/Sidebar/WeatherForecast.svelte +++ b/src/lib/Sidebar/WeatherForecast.svelte @@ -42,7 +42,7 @@ date: string; temperature: number; } - $: forecast = entity?.attributes?.forecast.slice(0, days_to_show).map(function (item: any) { + $: forecast = entity?.attributes?.forecast?.slice(0, days_to_show).map(function (item: any) { let icon: WeatherIconMapping = iconSet.conditions[item?.condition as keyof WeatherIconConditions]; let x: Forecast = { From 486c093358d4fbe1c735afd519753be10501e25a Mon Sep 17 00:00:00 2001 From: Evan Barbour Date: Sun, 14 Jan 2024 17:04:43 +0000 Subject: [PATCH 3/7] fix: local icon not displaying --- src/lib/Sidebar/WeatherForecast.svelte | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/Sidebar/WeatherForecast.svelte b/src/lib/Sidebar/WeatherForecast.svelte index 24c398a8..e113219a 100644 --- a/src/lib/Sidebar/WeatherForecast.svelte +++ b/src/lib/Sidebar/WeatherForecast.svelte @@ -65,9 +65,9 @@ )} -
+
{#if forecast.icon.local} - + {entity_state} {:else} - + {/if}
@@ -108,7 +108,6 @@ } .icon { - grid-area: icon; width: 3.6rem; height: 3.6rem; display: flex; From 6d3139eaf8bc80bbb3462b4b09bc6bc3e1a3e0cb Mon Sep 17 00:00:00 2001 From: Evan Barbour Date: Sun, 21 Jan 2024 16:41:58 +0000 Subject: [PATCH 4/7] fix: number of items displaying --- src/lib/Modal/WeatherForecastConfig.svelte | 27 ++++++++++++++-------- src/lib/Sidebar/Index.svelte | 2 +- src/lib/Sidebar/WeatherForecast.svelte | 8 ++++--- src/lib/Types.ts | 2 +- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/lib/Modal/WeatherForecastConfig.svelte b/src/lib/Modal/WeatherForecastConfig.svelte index 2e3af610..977e0bf3 100644 --- a/src/lib/Modal/WeatherForecastConfig.svelte +++ b/src/lib/Modal/WeatherForecastConfig.svelte @@ -6,15 +6,25 @@ import ConfigButtons from '$lib/Modal/ConfigButtons.svelte'; import Modal from '$lib/Modal/Index.svelte'; import { updateObj } from '$lib/Utils'; + import type { HassEntity } from 'home-assistant-js-websocket'; import type { WeatherForecastItem } from '$lib/Types'; export let isOpen: boolean; export let sel: WeatherForecastItem; - let days_to_show = sel?.days_to_show; + let number_of_items = sel?.number_of_items ?? 6; let numberElement: HTMLInputElement; + let entity: HassEntity; + $: { + if (sel?.entity_id) { + if ($states?.[sel?.entity_id]?.last_updated !== entity?.last_updated) { + entity = $states?.[sel?.entity_id]; + } + } + } + const iconOptions = [ { id: 'materialsymbolslight', name: 'materialsymbolslight' }, { id: 'meteocons', name: 'meteocons' }, @@ -27,9 +37,9 @@ ).sort() .map((key) => ({ id: key, label: key })); - const range = { + $: range = { min: 1, - max: 6 + max: Math.min(entity?.attributes?.forecast?.length ?? 7, 7) }; function minMax(key: string | number | undefined) { @@ -39,7 +49,7 @@ function handleNumberRange(event: any) { console.log(event?.target?.value); const value = minMax(event?.target?.value); - set('days_to_show', value); + set('number_of_items', value); if (numberElement) numberElement.value = String(value); } @@ -61,7 +71,7 @@
@@ -88,18 +98,17 @@ /> {/if} -

{$lang('days_to_show')}

+

{$lang('count')}

{#if weatherStates} {/if} diff --git a/src/lib/Sidebar/Index.svelte b/src/lib/Sidebar/Index.svelte index 8ca34df5..700b8002 100644 --- a/src/lib/Sidebar/Index.svelte +++ b/src/lib/Sidebar/Index.svelte @@ -334,7 +334,7 @@ this={WeatherForecast.default} entity_id={item?.entity_id} icon_pack={item?.icon_pack} - days_to_show={item?.days_to_show} + number_of_items={item?.number_of_items} /> {/if} diff --git a/src/lib/Sidebar/WeatherForecast.svelte b/src/lib/Sidebar/WeatherForecast.svelte index e113219a..2ddd9e0d 100644 --- a/src/lib/Sidebar/WeatherForecast.svelte +++ b/src/lib/Sidebar/WeatherForecast.svelte @@ -7,7 +7,7 @@ export let entity_id: string | undefined; export let icon_pack: string | undefined; - export let days_to_show: number | undefined; + export let number_of_items: number | undefined; let entity: HassEntity; $: { @@ -34,7 +34,8 @@ } } - console.log(days_to_show); + // Because config may not include number_of_items, and some forecasts proviode 48 datapoints, we need to ensure it's correct + $: calculated = Math.min(number_of_items ?? 7, 7) interface Forecast { condition: string; @@ -42,7 +43,8 @@ date: string; temperature: number; } - $: forecast = entity?.attributes?.forecast?.slice(0, days_to_show).map(function (item: any) { + let forecast: Forecast[] + $: forecast = entity?.attributes?.forecast?.slice(0, calculated).map(function (item: any) { let icon: WeatherIconMapping = iconSet.conditions[item?.condition as keyof WeatherIconConditions]; let x: Forecast = { diff --git a/src/lib/Types.ts b/src/lib/Types.ts index 7a1f2983..6af8cc61 100644 --- a/src/lib/Types.ts +++ b/src/lib/Types.ts @@ -206,5 +206,5 @@ export interface WeatherForecastItem { entity_id?: string; state?: string; icon_pack?: string; - days_to_show?: number; + number_of_items?: number; } From ed4060c2e2321ad6c46f998359f5ece2e201850f Mon Sep 17 00:00:00 2001 From: Evan Barbour Date: Sun, 21 Jan 2024 16:43:18 +0000 Subject: [PATCH 5/7] feat: space forecast items evenly --- src/lib/Sidebar/WeatherForecast.svelte | 66 +++++++++++++++++--------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/src/lib/Sidebar/WeatherForecast.svelte b/src/lib/Sidebar/WeatherForecast.svelte index 2ddd9e0d..87d767c4 100644 --- a/src/lib/Sidebar/WeatherForecast.svelte +++ b/src/lib/Sidebar/WeatherForecast.svelte @@ -56,18 +56,27 @@ return x; }); + + // Different forecast providers choose different intervals, we need to figure out display based on this + $: forecast_diff = ((new Date(forecast?.[1]?.date)).valueOf() - (new Date(forecast?.[0]?.date)).valueOf()) / 3600000 {#if entity_state} -
+
{#each forecast as forecast, i} -
- {new Intl.DateTimeFormat($selectedLanguage, { weekday: 'short' }).format( - new Date(forecast.date) - )} -
+
+
+ {#if forecast_diff < 24} + {new Intl.DateTimeFormat($selectedLanguage, { hour: 'numeric' }).format( + new Date(forecast.date) + )} + {:else} + {new Intl.DateTimeFormat($selectedLanguage, { weekday: 'short' }).format( + new Date(forecast.date) + )} + {/if} +
-
{#if forecast.icon.local} {/if} -
-
- {Math.round(forecast.temperature)}{attributes?.temperature_unit || '°'} +
+ {Math.round(forecast.temperature)}{attributes?.temperature_unit || '°'} +
{/each}
@@ -94,37 +103,48 @@ {/if}