From 24a56374b130ccda27f84fb4b0bb0c5aa0706d07 Mon Sep 17 00:00:00 2001 From: Juan Gonzalez Date: Wed, 23 Aug 2023 09:25:49 +0200 Subject: [PATCH 1/5] Allow negative offsets and show empty segments when no data is available --- src/conditions.ts | 3 +- src/editor.ts | 3 -- src/hourly-weather.ts | 36 ++++++++++++------- src/localize/languages/en.json | 3 +- src/types.ts | 1 + src/weather-bar.ts | 64 ++++++++++++++++++++++++++++++++++ 6 files changed, 93 insertions(+), 17 deletions(-) diff --git a/src/conditions.ts b/src/conditions.ts index 66f232e5..62e9d6a2 100644 --- a/src/conditions.ts +++ b/src/conditions.ts @@ -13,7 +13,8 @@ export const LABELS = { 'sunny': 'conditions.sunny', 'windy': 'conditions.windy', 'windy-variant': 'conditions.windy', - 'exceptional': 'conditions.clear' + 'exceptional': 'conditions.clear', + 'empty': 'conditions.noData' }; export const ICONS = { 'clear-night': 'weather-night', diff --git a/src/editor.ts b/src/editor.ts index f894cdb4..b1dce01c 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -124,9 +124,6 @@ export class HourlyWeatherCardEditor extends ScopedRegistryHost(LitElement) impl .configValue=${'offset'} @input=${this._valueChanged} .type=${'number'} - .min=${0} - .autoValidate=${true} - validationMessage=${localize('errors.must_be_positive_int')} > (forecast.length - offset)) { - return await this._showError(this.localize('errors.too_many_segments_requested')); - } - if (labelSpacing < 1) { // REMARK: Ok, so I'm re-using a localized string here. Probably not the best, but it avoids repeating for no good reason return await this._showError(this.localize('errors.offset_must_be_positive_int', 'offset', 'label_spacing')); @@ -283,10 +281,10 @@ export class HourlyWeatherCard extends LitElement { } const isForecastDaily = this.isForecastDaily(forecast); - const conditionList = this.getConditionListFromForecast(forecast, numSegments, offset); - const temperatures = this.getTemperatures(forecast, numSegments, offset); - const wind = this.getWind(forecast, numSegments, offset, windSpeedUnit); - const precipitation = this.getPrecipitation(forecast, numSegments, offset, precipitationUnit); + const conditionList = this.getConditionListFromForecast(forecast, numSegmentsAdjusted, offsetAdjusted); + const temperatures = this.getTemperatures(forecast, numSegmentsAdjusted, offsetAdjusted); + const wind = this.getWind(forecast, numSegmentsAdjusted, offsetAdjusted, windSpeedUnit); + const precipitation = this.getPrecipitation(forecast, numSegmentsAdjusted, offsetAdjusted, precipitationUnit); const colorSettings = this.getColorSettings(config.colors); @@ -322,6 +320,8 @@ export class HourlyWeatherCard extends LitElement { .show_precipitation_probability=${!!config.show_precipitation_probability} .show_date=${config.show_date} .label_spacing=${labelSpacing} + .num_empty_segments_leading=${numEmptySegmentsLeading} + .num_empty_segments_trailing=${numEmptySegmentsTrailing} .labels=${this.labels}> @@ -329,6 +329,9 @@ export class HourlyWeatherCard extends LitElement { } private getConditionListFromForecast(forecast: ForecastSegment[], numSegments: number, offset: number): ConditionSpan[] { + if (numSegments < 1) { + return []; + } let lastCond: string = forecast[offset].condition; let j = 0; const res: ConditionSpan[] = [[lastCond, 1]]; @@ -346,6 +349,9 @@ export class HourlyWeatherCard extends LitElement { } private getTemperatures(forecast: ForecastSegment[], numSegments: number, offset: number): SegmentTemperature[] { + if (numSegments < 1) { + return []; + } const temperatures: SegmentTemperature[] = []; for (let i = offset; i < numSegments + offset; i++) { const fs = forecast[i]; @@ -360,6 +366,9 @@ export class HourlyWeatherCard extends LitElement { } private getPrecipitation(forecast: ForecastSegment[], numSegments: number, offset: number, unit: string): SegmentPrecipitation[] { + if (numSegments < 1) { + return []; + } const precipitation: SegmentPrecipitation[] = []; for (let i = offset; i < numSegments + offset; i++) { const fs = forecast[i]; @@ -384,6 +393,9 @@ export class HourlyWeatherCard extends LitElement { } private getWind(forecast: ForecastSegment[], numSegments: number, offset: number, speedUnit: string): SegmentWind[] { + if (numSegments < 1) { + return []; + } const wind: SegmentWind[] = []; for (let i = offset; i < numSegments + offset; i++) { const fs = forecast[i]; diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 659a4913..1bd1833b 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -49,7 +49,8 @@ "snow": "Snow", "mixedPrecip": "Mixed precip", "sunny": "Sunny", - "windy": "Windy" + "windy": "Windy", + "noData": "No data" }, "direction": { "n": "N", diff --git a/src/types.ts b/src/types.ts index 9dd4a295..d8f7b104 100644 --- a/src/types.ts +++ b/src/types.ts @@ -58,6 +58,7 @@ export interface ColorConfig { 'windy'?: ColorDefinition; 'windy-variant'?: ColorDefinition; 'exceptional'?: ColorDefinition; + 'empty'?: ColorDefinition; } export interface ForecastSegment { diff --git a/src/weather-bar.ts b/src/weather-bar.ts index 7aa0976e..d1af7e93 100644 --- a/src/weather-bar.ts +++ b/src/weather-bar.ts @@ -51,6 +51,12 @@ export class WeatherBar extends LitElement { @property({ type: Number }) label_spacing = 2; + @property({ type: Number }) + num_empty_segments_leading = 0; + + @property({ type: Number }) + num_empty_segments_trailing = 0; + @property({ type: Object }) labels = LABELS; @@ -58,8 +64,16 @@ export class WeatherBar extends LitElement { render() { const conditionBars: TemplateResult[] = []; + const emptyLabel = this.labels['empty']; let gridStart = 1; if (!this.hide_bar) { + if (this.num_empty_segments_leading > 0) { + const barStylesEmpty: Readonly = { gridColumnStart: String(gridStart), gridColumnEnd: String(gridStart += this.num_empty_segments_leading * 2) }; + conditionBars.push(html` +
+ `); + } + for (const cond of this.conditions) { const label = this.labels[cond[0]]; let icon = ICONS[cond[0]]; @@ -74,11 +88,35 @@ export class WeatherBar extends LitElement { `); } + + if (this.num_empty_segments_trailing > 0) { + const barStylesEmpty: Readonly = { gridColumnStart: String(gridStart), gridColumnEnd: String(gridStart += this.num_empty_segments_trailing * 2) }; + conditionBars.push(html` +
+ `); + } } const windCfg = this.show_wind ?? ''; const barBlocks: TemplateResult[] = []; let lastDate: string | null = null; + + for (let i = 0; i < this.num_empty_segments_leading; i += 1) { + barBlocks.push(html` +
+
+
+
+
+
+
+
+
+
+
+ `); + } + for (let i = 0; i < this.temperatures.length; i += 1) { const skipLabel = i % (this.label_spacing) !== 0; const hideHours = this.hide_hours || skipLabel; @@ -136,6 +174,22 @@ export class WeatherBar extends LitElement { `); } + for (let i = 0; i < this.num_empty_segments_trailing; i += 1) { + barBlocks.push(html` +
+
+
+
+
+
+
+
+
+
+
+ `); + } + let colorStyles: TemplateResult | null = null; if (this.colors) { colorStyles = this.getColorStyles(this.colors); @@ -203,6 +257,8 @@ export class WeatherBar extends LitElement { --color-windy: var(--color-sunny); --color-windy-variant: var(--color-sunny); --color-exceptional: #ff9d00; + --color-empty: #aaaaaa; + --color-empty-foreground: #dddddd; } .bar { height: 30px; @@ -299,6 +355,11 @@ export class WeatherBar extends LitElement { background-color: var(--color-exceptional); color: var(--color-exceptional-foreground, var(--primary-text-color)); } + .empty { + background-color: var(--color-empty); + background-image: repeating-linear-gradient(135deg, var(--color-empty) 0px, var(--color-empty) 2px, var(--color-empty-foreground, var(--color-empty)) 4px, var(--color-empty-foreground, var(--color-empty)) 6px, var(--color-empty) 8px); + color: var(--color-empty-foreground, var(--primary-text-color)); + } .axes { display: grid; grid-auto-flow: column; @@ -326,6 +387,9 @@ export class WeatherBar extends LitElement { grid-area: bottom; padding-top: 5px; } + .bar-block-empty { + border-width: 0px; + } .date, .hour { color: var(--secondary-text-color, gray); font-size: 0.9rem; From f913854b40c3748b58d9f5fcdad934b87a5e7d01 Mon Sep 17 00:00:00 2001 From: Juan Gonzalez Date: Wed, 23 Aug 2023 23:09:47 +0200 Subject: [PATCH 2/5] Fixed a bug when forecast is unavailable --- src/hourly-weather.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hourly-weather.ts b/src/hourly-weather.ts index e0ba427a..3a7a43d2 100644 --- a/src/hourly-weather.ts +++ b/src/hourly-weather.ts @@ -242,11 +242,6 @@ export class HourlyWeatherCard extends LitElement { const labelSpacing = parseInt(config.label_spacing ?? '2', 10); const forecastNotAvailable = !forecast || !forecast.length; - // Adjust numSegments to only load the required number after the empty segments - const offsetAdjusted = Math.max(0, offset); - const numSegmentsAdjusted = Math.max(0,Math.min(numSegments - numEmptySegmentsLeading, forecast.length - offsetAdjusted)); - const numEmptySegmentsTrailing = numSegments - (numSegmentsAdjusted + numEmptySegmentsLeading); - if (numSegments < 1) { // REMARK: Ok, so I'm re-using a localized string here. Probably not the best, but it avoids repeating for no good reason return await this._showError(this.localize('errors.offset_must_be_positive_int', 'offset', 'num_segments')); @@ -280,6 +275,11 @@ export class HourlyWeatherCard extends LitElement { `; } + // Adjust numSegments to only load the required number after the empty segments + const offsetAdjusted = Math.max(0, offset); + const numSegmentsAdjusted = Math.max(0,Math.min(numSegments - numEmptySegmentsLeading, forecast.length - offsetAdjusted)); + const numEmptySegmentsTrailing = numSegments - (numSegmentsAdjusted + numEmptySegmentsLeading); + const isForecastDaily = this.isForecastDaily(forecast); const conditionList = this.getConditionListFromForecast(forecast, numSegmentsAdjusted, offsetAdjusted); const temperatures = this.getTemperatures(forecast, numSegmentsAdjusted, offsetAdjusted); From bc24808bd3e7bcb755e1fca682562f23df3ca75e Mon Sep 17 00:00:00 2001 From: Juan Gonzalez Date: Wed, 23 Aug 2023 23:12:05 +0200 Subject: [PATCH 3/5] Refactoring: Single bar-block generation function --- src/weather-bar.ts | 93 +++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/src/weather-bar.ts b/src/weather-bar.ts index d1af7e93..dcca55c6 100644 --- a/src/weather-bar.ts +++ b/src/weather-bar.ts @@ -101,22 +101,8 @@ export class WeatherBar extends LitElement { const barBlocks: TemplateResult[] = []; let lastDate: string | null = null; - for (let i = 0; i < this.num_empty_segments_leading; i += 1) { - barBlocks.push(html` -
-
-
-
-
-
-
-
-
-
-
- `); - } - + barBlocks.push(...this.renderEmptySegmentBarBlocks(this.num_empty_segments_leading)); + for (let i = 0; i < this.temperatures.length; i += 1) { const skipLabel = i % (this.label_spacing) !== 0; const hideHours = this.hide_hours || skipLabel; @@ -159,37 +145,19 @@ export class WeatherBar extends LitElement { if (showPrecipitationProbability) precipitation.push( html`${precipitationProbability}`); - barBlocks.push(html` -
-
-
-
-
${renderedDate}
-
${hideHours ? null : hour}
-
${hideTemperature ? null : html`${temperature}°`}
-
${wind}
-
${precipitation}
-
-
- `); - } - - for (let i = 0; i < this.num_empty_segments_trailing; i += 1) { - barBlocks.push(html` -
-
-
-
-
-
-
-
-
-
-
- `); - } - + barBlocks.push( + this.renderBarBlock( + renderedDate, + hideHours ? null : hour, + hideTemperature ? null : html`${temperature}°`, + wind, + precipitation + ) + ); + } + + barBlocks.push(...this.renderEmptySegmentBarBlocks(this.num_empty_segments_trailing)); + let colorStyles: TemplateResult | null = null; if (this.colors) { colorStyles = this.getColorStyles(this.colors); @@ -240,6 +208,37 @@ export class WeatherBar extends LitElement { `; } + private renderBarBlock(date: string | TemplateResult | TemplateResult[] | null, + hour: string | TemplateResult | TemplateResult[] | null, + temperature: string | TemplateResult | TemplateResult[] | null, + wind: string | TemplateResult | TemplateResult[] | null, + precipitation: string | TemplateResult | TemplateResult[] | null, + emptyBlock: boolean = false + ) : TemplateResult { + const emptyBlockClass : string = emptyBlock ? ' bar-block-empty' : ''; + return html` +
+
+
+
+
${date}
+
${hour}
+
${temperature}
+
${wind}
+
${precipitation}
+
+
+ `; + } + + private renderEmptySegmentBarBlocks(count: number): TemplateResult[] { + const result: TemplateResult[] = []; + for (let i = 0; i < count; i += 1) { + result.push(this.renderBarBlock(null, null, null, null, null, true)); + } + return result; + } + static styles = [unsafeCSS(tippyStyles), css` .main { --color-clear-night: #111; From 10f3e92f83e1330da8468ed581dab79a1fa93679 Mon Sep 17 00:00:00 2001 From: Juan Gonzalez Date: Wed, 23 Aug 2023 23:13:50 +0200 Subject: [PATCH 4/5] Adapted tests for the new empty segments feature --- cypress/e2e/config.cy.ts | 28 ------- cypress/e2e/weather-bar.cy.ts | 139 ++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 28 deletions(-) diff --git a/cypress/e2e/config.cy.ts b/cypress/e2e/config.cy.ts index cf15c8b1..eebf1f43 100644 --- a/cypress/e2e/config.cy.ts +++ b/cypress/e2e/config.cy.ts @@ -13,34 +13,6 @@ describe('Config', () => { .find('p') .should('have.text', 'num_segments must be a positive integer'); }); - it('errors for offset < 0', () => { - cy.configure({ - offset: '-1' - }); - cy.get('hui-error-card') - .shadow() - .find('p') - .should('have.text', 'offset must be a positive integer'); - }); - it('errors for num_segments > forecast length', () => { - cy.configure({ - num_segments: '50' - }); - cy.get('hui-error-card') - .shadow() - .find('p') - .should('have.text', 'Too many forecast segments requested in num_segments. Must be <= number of segments in forecast entity.'); - }); - it('errors for num_segments > forecast length - offset', () => { - cy.configure({ - num_segments: '12', - offset: '50' - }); - cy.get('hui-error-card') - .shadow() - .find('p') - .should('have.text', 'Too many forecast segments requested in num_segments. Must be <= number of segments in forecast entity.'); - }); it('errors for wind barbs when entity uses cardinal directions for wind bearing', () => { cy.addEntity({ 'weather.wind_bearing_string': { diff --git a/cypress/e2e/weather-bar.cy.ts b/cypress/e2e/weather-bar.cy.ts index 85aada6a..c96e3f88 100644 --- a/cypress/e2e/weather-bar.cy.ts +++ b/cypress/e2e/weather-bar.cy.ts @@ -926,4 +926,143 @@ describe('Weather bar', () => { }); }); }); + describe('Empty segments', () => { + const expectedTemperatures = [ + 84, + 85, + 84, + 79, + 70, + 65 + ]; + it('shows empty segments at the beginning if offset < 0', () => { + cy.configure({ + offset: '-1' + }); + + cy.get('weather-bar') + .shadow() + .find('div.bar > div:first-child') + .should('have.css', 'grid-column-start', '1') + .should('have.css', 'grid-column-end', '3') + .should('have.attr', 'data-tippy-content', 'No data') + .should('have.class', 'empty'); + }); + it('aligns forecast segments correctly if offset < 0', () => { + cy.configure({ + offset: '-1' + }); + + cy.get('weather-bar') + .shadow() + .find('div.axes > div.bar-block div.temperature') + .should('have.length', 12) + .each((el, i) => { + if (i % 2 === 1) { + cy.wrap(el).should('have.text', expectedTemperatures[(i - 1) / 2] + '°'); + } + }); + }); + + it('fills the entire bar if offset < -num_segments', () => { + cy.configure({ + offset: '-20' + }); + + cy.get('weather-bar') + .shadow() + .find('div.bar > div') + .should('have.length', 1) + .should('have.css', 'grid-column-start', '1') + .should('have.css', 'grid-column-end', '25') + .should('have.attr', 'data-tippy-content', 'No data') + .should('have.class', 'empty'); + }); + + + it('shows empty segments at the end if (offset + num_segments) > available data', () => { + cy.configure({ + offset: '40' + }); + + cy.get('weather-bar') + .shadow() + .find('div.bar > div:last-child') + .should('have.css', 'grid-column-start', '17') + .should('have.css', 'grid-column-end', '25') + .should('have.attr', 'data-tippy-content', 'No data') + .should('have.class', 'empty'); + }); + + it('fills the entire bar if offset > available forecast data', () => { + cy.configure({ + offset: '50' + }); + + cy.get('weather-bar') + .shadow() + .find('div.bar > div') + .should('have.length', 1) + .should('have.css', 'grid-column-start', '1') + .should('have.css', 'grid-column-end', '25') + .should('have.attr', 'data-tippy-content', 'No data') + .should('have.class', 'empty'); + }); + it('hides axes for empty segment at start', () => { + cy.configure({ + offset: '-1' + }); + cy.get('weather-bar') + .shadow() + .find('div.axes > div.bar-block > div.bar-block-left, div.axes > div.bar-block > div.bar-block-right') + .should('have.length', 24) + .each((el, i) => { + if (i < 2) { + cy.wrap(el).should('have.class', 'bar-block-empty'); + } else { + cy.wrap(el).should('not.have.class', 'bar-block-empty'); + } + }); + }); + it('hides axes for empty segment at end', () => { + cy.configure({ + offset: '40' + }); + cy.get('weather-bar') + .shadow() + .find('div.axes > div.bar-block > div.bar-block-left, div.axes > div.bar-block > div.bar-block-right') + .should('have.length', 24) + .each((el, i) => { + if (i >= 16) { + cy.wrap(el).should('have.class', 'bar-block-empty'); + } else { + cy.wrap(el).should('not.have.class', 'bar-block-empty'); + } + }); + }); + it('hides axes for empty segment filling the entire bar (offset < num_segments)', () => { + cy.configure({ + offset: '-20' + }); + cy.get('weather-bar') + .shadow() + .find('div.axes > div.bar-block > div.bar-block-left, div.axes > div.bar-block > div.bar-block-right') + .should('have.length', 24) + .each((el) => { + cy.wrap(el).should('have.class', 'bar-block-empty'); + }); + }); + it('hides axes for empty segment filling the entire bar (offset > available data)', () => { + cy.configure({ + offset: '50' + }); + cy.get('weather-bar') + .shadow() + .find('div.axes > div.bar-block > div.bar-block-left, div.axes > div.bar-block > div.bar-block-right') + .should('have.length', 24) + .each((el) => { + cy.wrap(el).should('have.class', 'bar-block-empty'); + }); + }); + }); }); From 37ce81c44d5c9ab2609111e2ff7d7f12f3f5a324 Mon Sep 17 00:00:00 2001 From: Juan Gonzalez Date: Wed, 23 Aug 2023 23:26:14 +0200 Subject: [PATCH 5/5] Updated README to show offset<0 option --- README.md | 54 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c34e2cda..1f43bd29 100644 --- a/README.md +++ b/README.md @@ -58,28 +58,28 @@ Otherwise, the integration may complain of a duplicate unique ID. ## Options -| Name | Type | Requirement | Description | Default | -|----------------------------------|------------------|--------------|----------------------------------------------------------------|---------------------| -| `type` | string | **Required** | `custom:hourly-weather` | | -| `entity` | string | **Required** | Home Assistant weather entity ID. | | -| `name` | string | **Optional** | Card name (set to `null` to hide) | `Hourly Weather` | -| `icons` | bool | **Optional** | Whether to show icons instead of text labels | `false` | -| `num_segments` | number | **Optional** | Number of forecast segments to show (integer >= 1) | `12` | -| ~~`num_hours`~~ | number | **Optional** | _Deprecated:_ Use `num_segments` instead | `12` | -| `offset` | number | **Optional** | Number of forecast segments to offset from start | `0` | -| `label_spacing` | number | **Optional** | Space between time/temperature labels (integer >= 1) | `2` | -| `colors` | [object][color] | **Optional** | Set colors for all or some conditions | | -| `hide_hours` | bool | **Optional** | Whether to hide hour labels under the bar | `false` | -| `hide_temperatures` | bool | **Optional** | Whether to hide temperatures under the bar | `false` | -| `hide_bar` | bool | **Optional** | Whether to hide the bar itself | `false` | -| `show_wind` | [Wind][wind] | **Optional** | Whether to show wind speed and/or direction under the bar | `'false'` | -| `show_precipitation_amounts` | bool | **Optional** | Whether to show precipitation (rain) amount under the bar | `false` | -| `show_precipitation_probability` | bool | **Optional** | Whether to show precipitation (rain) probability under the bar | `false` | -| `show_date` | [string][dates] | **Optional** | Whether to show date under the bar | `'false'` | -| `tap_action` | [object][action] | **Optional** | Action to take on tap | `action: more-info` | -| `hold_action` | [object][action] | **Optional** | Action to take on hold | `none` | -| `double_tap_action` | [object][action] | **Optional** | Action to take on double tap | `none` | -| `language` | string | **Optional** | Language to use for card (overrides HA & user settings) | | +| Name | Type | Requirement | Description | Default | +|----------------------------------|------------------|--------------|------------------------------------------------------------------------------------------|---------------------| +| `type` | string | **Required** | `custom:hourly-weather` | | +| `entity` | string | **Required** | Home Assistant weather entity ID. | | +| `name` | string | **Optional** | Card name (set to `null` to hide) | `Hourly Weather` | +| `icons` | bool | **Optional** | Whether to show icons instead of text labels | `false` | +| `num_segments` | number | **Optional** | Number of forecast segments to show (integer >= 1) | `12` | +| ~~`num_hours`~~ | number | **Optional** | _Deprecated:_ Use `num_segments` instead | `12` | +| `offset` | number | **Optional** | Number of forecast segments to offset from start. A negative value will insert empty segments at the beginning | `0` | +| `label_spacing` | number | **Optional** | Space between time/temperature labels (integer >= 1) | `2` | +| `colors` | [object][color] | **Optional** | Set colors for all or some conditions | | +| `hide_hours` | bool | **Optional** | Whether to hide hour labels under the bar | `false` | +| `hide_temperatures` | bool | **Optional** | Whether to hide temperatures under the bar | `false` | +| `hide_bar` | bool | **Optional** | Whether to hide the bar itself | `false` | +| `show_wind` | [Wind][wind] | **Optional** | Whether to show wind speed and/or direction under the bar | `'false'` | +| `show_precipitation_amounts` | bool | **Optional** | Whether to show precipitation (rain) amount under the bar | `false` | +| `show_precipitation_probability` | bool | **Optional** | Whether to show precipitation (rain) probability under the bar | `false` | +| `show_date` | [string][dates] | **Optional** | Whether to show date under the bar | `'false'` | +| `tap_action` | [object][action] | **Optional** | Action to take on tap | `action: more-info` | +| `hold_action` | [object][action] | **Optional** | Action to take on hold | `none` | +| `double_tap_action` | [object][action] | **Optional** | Action to take on double tap | `none` | +| `language` | string | **Optional** | Language to use for card (overrides HA & user settings) | | > Note that some of the more advanced options are not available in the card editor UI and must be configured via YAML. @@ -114,6 +114,16 @@ label_spacing: | {{ 4 if segments > 12 else 2 }} ``` +Or show the rest of today's weather while keeping the same layout throughout the day: + +```yaml +name: Today +num_segments: 24 +offset: | + {{ -now().hour }} +label_spacing: 4 +``` + ## Action Options | Name | Type | Requirement | Description | Default |