From 7b969d3fd9d95e2b8100e84898fb3126cdbee25e Mon Sep 17 00:00:00 2001 From: JanGiese <54441374+JanGiese@users.noreply.github.com> Date: Wed, 29 Mar 2023 20:19:51 +0200 Subject: [PATCH] Feature/display hourly forecast (#102) Co-authored-by: Jan-Philipp Giese Co-authored-by: jan-philippgieseext --- README.md | 4 ++- src/clock-weather-card.ts | 58 +++++++++++++++++++++------------------ src/styles.ts | 2 +- src/types.ts | 2 ++ 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index b242dda8..891ed3b2 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ The basic idea of the forecast bars is to be able to understand the weather tren hide_forecast_section: false hide_clock: false hide_date: false + hourly_forecast: false ``` ### Options @@ -126,9 +127,10 @@ The basic idea of the forecast bars is to be able to understand the weather tren | time_format | `24` \| `12` | **Optional** | Format used to display the time. If not provided, falls back to the time format set in HA | `24` | | date_pattern | string | **Optional** | Pattern to use for time formatting. If not provided, falls back to the default date formatting of the configured language. See [date-fns](https://date-fns.org/v2.29.3/docs/format) for valid patterns | `P` | | hide_today_section | boolean | **Optional** | Hides the cards today section (upper section), containing the large weather icon, clock and current date | `false` | -| hide_forecast_section | boolean | **Optional** | Hides the cards forecast section (lower section), containing the weather forecast | `false` | +| hide_forecast_section | boolean | **Optional** | Hides the cards forecast section (lower section),containing the weather forecast | `false` | | hide_clock | boolean | **Optional** | Hides the clock from the today section and prominently displays the current temperature instead. | `false` | | hide_date | boolean | **Optional** | Hides the date from the today section | `false` | +| hourly_forecast | boolean | **Optional** | Displays an hourly forecast instead of daily. | `false` | ## Footnotes diff --git a/src/clock-weather-card.ts b/src/clock-weather-card.ts index 9c389e59..5c9326dc 100644 --- a/src/clock-weather-card.ts +++ b/src/clock-weather-card.ts @@ -183,13 +183,14 @@ export class ClockWeatherCard extends LitElement { private renderForecast(): TemplateResult[] { const weather = this.getWeather(); const currentTemp = roundIfNotNull(this.getCurrentTemperature()); - const days = this.config.forecast_days; - const temperatueUnit = weather.attributes.temperature_unit; + const items = this.config.forecast_days; + const hourly = this.config.hourly_forecast; + const temperatureUnit = weather.attributes.temperature_unit; - const dailyForecasts = this.extractDailyForecasts(weather.attributes.forecast, days); + const forecasts = this.extractForecasts(weather.attributes.forecast, items, hourly); - const minTemps = dailyForecasts.map((f) => f.templow); - const maxTemps = dailyForecasts.map((f) => f.temperature); + const minTemps = forecasts.map((f) => f.templow); + const maxTemps = forecasts.map((f) => f.temperature); if (currentTemp !== null) { minTemps.push(currentTemp); maxTemps.push(currentTemp); @@ -197,24 +198,27 @@ export class ClockWeatherCard extends LitElement { const minTemp = Math.round(min(minTemps)); const maxTemp = Math.round(max(maxTemps)); - const gradientRange = this.gradientRange(minTemp, maxTemp, temperatueUnit); - return dailyForecasts.map((forecast) => safeRender(() => this.renderForecastDay(forecast, gradientRange, minTemp, maxTemp, currentTemp))); + const gradientRange = this.gradientRange(minTemp, maxTemp, temperatureUnit); + return forecasts.map((forecast) => safeRender(() => this.renderForecastItem(forecast, gradientRange, minTemp, maxTemp, currentTemp, hourly))); } - private renderForecastDay(forecast: MergedWeatherForecast, gradientRange: Rgb[], minTemp: number, maxTemp: number, currentTemp: number | null): TemplateResult { - const dayText = this.localize(`day.${new Date(forecast.datetime).getDay()}`); + private renderForecastItem(forecast: MergedWeatherForecast, gradientRange: Rgb[], minTemp: number, maxTemp: number, currentTemp: number | null, hourly: boolean): TemplateResult { + const twelveHour = this.getTimeFormat() === '12'; + const displayText = !hourly ? this.localize('day.' + forecast.datetime.getDay()) : this.time(forecast.datetime); const weatherState = forecast.condition === 'pouring' ? 'raindrops' : forecast.condition === 'rainy' ? 'raindrop' : forecast.condition; const weatherIcon = this.toIcon(weatherState, 'fill', true, 'static'); const tempUnit = this.getWeather().attributes.temperature_unit; - const isToday = new Date().getDate() === new Date(forecast.datetime).getDate(); - const minTempDay = Math.round(isToday && currentTemp !== null ? Math.min(currentTemp, forecast.templow) : forecast.templow); - const maxTempDay = Math.round(isToday && currentTemp !== null ? Math.max(currentTemp, forecast.temperature) : forecast.temperature); + const isNow = !hourly ? new Date().getDate() === forecast.datetime.getDate() : new Date().getHours() === forecast.datetime.getHours(); + const minTempDay = Math.round(isNow && currentTemp !== null ? Math.min(currentTemp, forecast.templow) : forecast.templow); + const maxTempDay = Math.round(isNow && currentTemp !== null ? Math.max(currentTemp, forecast.temperature) : forecast.temperature); + const colOneSize = hourly && twelveHour ? '3rem' : hourly ? '2.5rem' : '2rem'; + return html` - - ${this.renderText(dayText)} + + ${this.renderText(displayText)} ${this.renderIcon(weatherIcon)} ${this.renderText(this.toConfiguredTempWithUnit(tempUnit, minTempDay), 'right')} - ${this.renderForecastTemperatureBar(gradientRange, minTemp, maxTemp, minTempDay, maxTempDay, isToday, currentTemp)} + ${this.renderForecastTemperatureBar(gradientRange, minTemp, maxTemp, minTempDay, maxTempDay, isNow, currentTemp)} ${this.renderText(this.toConfiguredTempWithUnit(tempUnit, maxTempDay))} `; @@ -236,7 +240,7 @@ export class ClockWeatherCard extends LitElement { `; } - private renderForecastTemperatureBar(gradientRange: Rgb[], minTemp: number, maxTemp: number, minTempDay: number, maxTempDay: number, isToday: boolean, currentTemp: number | null): TemplateResult { + private renderForecastTemperatureBar(gradientRange: Rgb[], minTemp: number, maxTemp: number, minTempDay: number, maxTempDay: number, isNow: boolean, currentTemp: number | null): TemplateResult { const { startPercent, endPercent } = this.calculateBarRangePercents(minTemp, maxTemp, minTempDay, maxTempDay) return html` @@ -248,7 +252,7 @@ export class ClockWeatherCard extends LitElement { endPercent, )};" > - ${isToday ? this.renderForecastCurrentTemp(minTempDay, maxTempDay, currentTemp) : ''} + ${isNow ? this.renderForecastCurrentTemp(minTempDay, maxTempDay, currentTemp) : ''} `; @@ -340,6 +344,7 @@ export class ClockWeatherCard extends LitElement { temperature_sensor: config.temperature_sensor, weather_icon_type: config.weather_icon_type || 'line', forecast_days: config.forecast_days || 5, + hourly_forecast: config.hourly_forecast || false, animated_icon: config.animated_icon === undefined ? true : config.animated_icon, time_format: config.time_format?.toString() as '12' | '24' | undefined, hide_forecast_section: config.hide_forecast_section || false, @@ -412,8 +417,8 @@ export class ClockWeatherCard extends LitElement { return`${weekday}, ${date}` } - private time(): string { - return format(this.currentDate, this.getTimeFormat() === '24' ? 'HH:mm' : 'h:mm aa'); + private time(date: Date = this.currentDate): string { + return format(date, this.getTimeFormat() === '24' ? 'HH:mm' : 'h:mm aa'); } private getIconAnimationKind(): 'static' | 'animated' { @@ -478,26 +483,27 @@ export class ClockWeatherCard extends LitElement { return localize(key, this.getLocale()); } - private extractDailyForecasts(forecasts: WeatherForecast[], days: number): MergedWeatherForecast[] { + private extractForecasts(forecasts: WeatherForecast[], items: number, hourly: boolean): MergedWeatherForecast[] { const agg = forecasts.reduce((forecasts, forecast) => { - const day = new Date(forecast.datetime).getDate(); - forecasts[day] = forecasts[day] || []; - forecasts[day].push(forecast); + const d = new Date(forecast.datetime); + const unit = !hourly ? d.getDate() : d.getDate()+"_"+d.getHours(); + forecasts[unit] = forecasts[unit] || []; + forecasts[unit].push(forecast); return forecasts; }, {} as Record); return Object.values(agg) .reduce((agg: MergedWeatherForecast[], forecasts) => { if (!forecasts.length) return agg; - const avg = this.calculateAverageDailyForecast(forecasts); + const avg = this.calculateAverageForecast(forecasts); agg.push(avg); return agg; }, []) .sort((a,b) => a.datetime.getTime() - b.datetime.getTime()) - .slice(0, days); + .slice(0, items); } - private calculateAverageDailyForecast(forecasts: WeatherForecast[]): MergedWeatherForecast { + private calculateAverageForecast(forecasts: WeatherForecast[]): MergedWeatherForecast { const minTemps = forecasts.map((f) => f.templow ?? f.temperature ?? this.getCurrentTemperature() ?? 0); const minTemp = min(minTemps); diff --git a/src/styles.ts b/src/styles.ts index 42694396..fba417fe 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -61,7 +61,7 @@ export default css` clock-weather-card-forecast-row { display: grid; - grid-template-columns: 2rem 2rem 2.1rem auto 2.1rem; + grid-template-columns: var(--col-one-size) 2rem 2.1rem auto 2.1rem; align-items: center; grid-gap: 0.5rem; } diff --git a/src/types.ts b/src/types.ts index f6e6a7ef..46627471 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,6 +20,7 @@ export interface ClockWeatherCardConfig extends LovelaceCardConfig { date_pattern?: string; hide_today_section?: boolean; hide_forecast_section?: boolean; + hourly_forecast?: boolean; hide_clock?: boolean; hide_date?: boolean; } @@ -37,6 +38,7 @@ export interface MergedClockWeatherCardConfig extends LovelaceCardConfig { date_pattern: string; hide_today_section: boolean; hide_forecast_section: boolean; + hourly_forecast: boolean; hide_clock?: boolean; hide_date?: boolean; }