Skip to content

Commit

Permalink
Merge pull request #175 from EvanBarbour3/weather-forecast
Browse files Browse the repository at this point in the history
Weather forecast sidebar component
  • Loading branch information
matt8707 authored Jan 22, 2024
2 parents f1e6d55 + b52cad2 commit d9f7629
Show file tree
Hide file tree
Showing 7 changed files with 567 additions and 1 deletion.
2 changes: 2 additions & 0 deletions scripts/translations/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
("count", ["ui.panel.config.automation.editor.actions.type.repeat.count"]),
("set_white", ["ui.dialogs.more_info_control.light.set_white"]),
("vacuum_commands", ["ui.dialogs.more_info_control.vacuum.commands"]),
("target", ["ui.card.water_heater.target"]),
Expand Down
12 changes: 12 additions & 0 deletions src/lib/Modal/SidebarItemConfig.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -180,6 +181,14 @@
entity_id: 'weather.openweathermap'
}
},
{
id: 'weatherforecast',
type: $lang('weather_forecast'),
component: WeatherForecast,
props: {
entity_id: 'weather.forecast_home'
}
},
{
id: 'navigate',
type: $lang('navigate'),
Expand Down Expand Up @@ -230,6 +239,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,
Expand Down
118 changes: 118 additions & 0 deletions src/lib/Modal/WeatherForecastConfig.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<script lang="ts">
import { states, dashboard, lang, record } from '$lib/Stores';
import { onDestroy } from 'svelte';
import WeatherForecast from '$lib/Sidebar/WeatherForecast.svelte';
import Select from '$lib/Components/Select.svelte';
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 number_of_items = sel?.number_of_items ?? 7;
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' },
{ id: 'weathericons', name: 'weather icons' }
];
$: weatherStates = Object.keys(
Object.fromEntries(Object.entries($states)
.filter(([key, value]) => key.startsWith('weather.') && value?.attributes?.forecast))
).sort()
.map((key) => ({ id: key, label: key }));
$: range = {
min: 1,
max: Math.min(entity?.attributes?.forecast?.length ?? 7, 7)
};
function minMax(key: string | number | undefined) {
return Math.min(Math.max(parseInt(key as string), range.min), range.max);
}
function handleNumberRange(event: any) {
console.log(event?.target?.value);
const value = minMax(event?.target?.value);
set('number_of_items', value);
if (numberElement) numberElement.value = String(value);
}
function set(key: string, event?: any) {
sel = updateObj(sel, key, event);
$dashboard = $dashboard;
}
onDestroy(() => $record());
</script>

{#if isOpen}
<Modal>
<h1 slot="title">{$lang('weather_forecast')}</h1>

<h2>{$lang('preview')}</h2>

<div class="preview">
<WeatherForecast
entity_id={sel?.entity_id}
icon_pack={sel?.icon_pack}
number_of_items={sel?.number_of_items}
/>
</div>

<h2>{$lang('entity')}</h2>

{#if weatherStates}
<Select
customItems={true}
options={weatherStates}
placeholder={$lang('entity')}
value={sel?.entity_id}
on:change={(event) => set('entity_id', event)}
/>
{/if}

<h2>{$lang('icon')}</h2>

{#if iconOptions}
<Select
options={iconOptions}
placeholder={$lang('icon')}
value={sel?.icon_pack}
on:change={(event) => set('icon_pack', event)}
/>
{/if}

<h2>{$lang('count')}</h2>

{#if weatherStates}
<input
type="number"
class="input"
bind:value={number_of_items}
bind:this={numberElement}
min={range.min}
max={range.max}
on:change={handleNumberRange}
autocomplete="off"
/>
{/if}

<ConfigButtons {sel} />
</Modal>
{/if}
18 changes: 17 additions & 1 deletion src/lib/Sidebar/Index.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand All @@ -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();
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -325,6 +330,17 @@
show_apparent={item?.show_apparent}
/>
</button>

<!-- WEATHER FORECAST -->
{:else if WeatherForecast && item?.type === 'weatherforecast'}
<button on:click={() => handleClick(item?.id)}>
<svelte:component
this={WeatherForecast.default}
entity_id={item?.entity_id}
icon_pack={item?.icon_pack}
number_of_items={item?.number_of_items}
/>
</button>
{/if}
</div>
{/each}
Expand Down
155 changes: 155 additions & 0 deletions src/lib/Sidebar/WeatherForecast.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<script lang="ts">
import { states, selectedLanguage, lang } from '$lib/Stores';
import { iconMapMaterialSymbolsLight, iconMapMeteocons, iconMapWeatherIcons } from '$lib/Weather';
import type { WeatherIconSet, WeatherIconConditions, WeatherIconMapping } from '$lib/Weather';
import type { HassEntity } from 'home-assistant-js-websocket';
import Icon from '@iconify/svelte';
export let entity_id: string | undefined;
export let icon_pack: string | undefined;
export let number_of_items: number | undefined;
let entity: HassEntity;
$: {
if (entity_id) {
if ($states?.[entity_id]?.last_updated !== entity?.last_updated) {
entity = $states?.[entity_id];
}
}
}
$: entity_state = entity?.state;
$: attributes = entity?.attributes;
let iconSet: WeatherIconSet;
$: {
if (icon_pack === 'materialsymbolslight') {
iconSet = iconMapMaterialSymbolsLight;
} else if (icon_pack === 'meteocons') {
iconSet = iconMapMeteocons;
} else if (icon_pack === 'weathericons') {
iconSet = iconMapWeatherIcons;
} else {
iconSet = iconMapMeteocons;
}
}
// 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;
icon: WeatherIconMapping;
date: string;
temperature: number;
}
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 = {
condition: item?.condition,
icon: icon,
date: item?.datetime,
temperature: item?.temperature
};
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
</script>

{#if entity_state}
<div class="container">
{#each forecast as forecast, i}
<div class="item">
<div class="day">
{#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}
</div>

{#if forecast.icon.local}
<icon class="icon">
<img
src={`${forecast.icon.icon_variant_day}.svg`}
alt={entity_state}
width="100%"
height="100%"
/>
</icon>
{:else}
<Icon class="icon" icon={forecast.icon.icon_variant_day} width="100%" height="100%"></Icon>
{/if}

<div class="temp">
{Math.round(forecast.temperature)}{attributes?.temperature_unit || '°'}
</div>
</div>
{/each}
</div>
{:else}
<div class="container-empty">
{$lang('weather_forecast')}
</div>
{/if}

<style>
.item {
display: grid;
grid-column-gap: 0px;
grid-row-gap: 0px;
grid-template-areas:
'day day'
'icon'
'temp temp';
text-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1);
text-overflow: ellipsis;
overflow: hidden;
width: 3.6rem;
}
.container {
padding: var(--theme-sidebar-item-padding);
display: flex;
text-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1);
text-overflow: ellipsis;
overflow: hidden;
justify-content: space-between;
}
.day {
grid-area: 'day';
justify-content: center;
display: flex;
width: 3.6rem;
}
.icon {
grid-area: 'icon';
width: 3.6rem;
height: 3.6rem;
display: flex;
text-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1);
justify-content: center;
transform-origin: right;
}
.temp {
grid-area: 'temp';
justify-content: center;
display: flex;
white-space: nowrap;
width: 3.6rem;
overflow: hidden;
text-shadow: 0px 0px 5px rgba(0, 0, 0, 0.1);
}
</style>
10 changes: 10 additions & 0 deletions src/lib/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export type SidebarItem = BarItem &
TemplateItem &
TimeItem &
WeatherItem &
WeatherForecastItem &
DividerItem;

export interface BarItem {
Expand Down Expand Up @@ -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;
number_of_items?: number;
}
Loading

0 comments on commit d9f7629

Please sign in to comment.