diff --git a/README.md b/README.md index b2825a2..968468a 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ decimal by 1). Otherwise, the integration may complain of a duplicate unique ID. | `forecast_type` | string | **Optional** | The type of forecast data to use. One of `hourly`, `daily`, or `twice-daily`. If not specified, the card will attempt to use the finest-grained data available. | | | `name` | string | **Optional** | Card name (set to `null` to hide) | `Hourly Weather` | | `icons` | bool | **Optional** | Whether to show icons instead of text labels | `false` | +| `icon_map` | [Icon map][icon_map] | **Optional** | Custom icons to use for the weather conditions. Uses `mdi` icons by default. | | | `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` | @@ -183,6 +184,37 @@ colors: foreground: '#000' ``` +## Icon map Options + +`icon_map` can be used to customize the icon used for each weather condition. It is specified as an object containing +one or more of the keys listed below and values that are valid icons installed in Home Assistant. + +| Key | Default | +|-------------------|--------------------------------| +| `clear-night` | `mdi:weather-night` | +| `cloudy` | `mdi:weather-cloudy` | +| `fog` | `mdi:weather-fog` | +| `hail` | `mdi:weather-hail` | +| `lightning` | `mdi:weather-lightning` | +| `lightning-rainy` | `mdi:weather-lightning-rainy` | +| `partlycloudy` | `mdi:weather-partly-cloudy` | +| `pouring` | `mdi:weather-pouring` | +| `rainy` | `mdi:weather-rainy` | +| `snowy` | `mdi:weather-snowy` | +| `snowy-rainy` | `mdi:weather-snowy-rainy` | +| `sunny` | `mdi:weather-sunny` | +| `windy` | `mdi:weather-windy` | +| `windy-variant` | `mdi:weather-windy-variant` | +| `exceptional` | `mdi:alert-outline` | + +### Sample icon map configuration + +```yaml +icon_map: + sunny: mdi:emotion-cool + cloudy: phu:nextcloud # can use any icon set +``` + ### Wind Options `show_wind` can be one of the following values: @@ -243,6 +275,7 @@ structure. [releases-shield]: https://img.shields.io/github/release/decompil3d/lovelace-hourly-weather.svg?style=for-the-badge [releases]: https://github.com/decompil3d/lovelace-hourly-weather/releases +[icon_map]: #icon-map-options [color]: #color-options [wind]: #wind-options [icon_fill]: #icon-fill-options diff --git a/cypress/e2e/card.cy.ts b/cypress/e2e/card.cy.ts index 54984fa..2f0239a 100644 --- a/cypress/e2e/card.cy.ts +++ b/cypress/e2e/card.cy.ts @@ -1,3 +1,5 @@ +import { Condition } from "../../src/types"; + describe('Card', () => { beforeEach(() => { cy.visitHarness(); @@ -60,7 +62,7 @@ describe('Card', () => { "pressure": 1007, "wind_speed": 4.67, "wind_bearing": 'WSW', - "condition": "cloudy", + "condition": "cloudy" as Condition, "clouds": 60, "temperature": 84 }, @@ -71,7 +73,7 @@ describe('Card', () => { forecast: forecast1 } } - }); + }); cy.configure({ entity: 'weather.fromSub', num_segments: '2' @@ -87,7 +89,7 @@ describe('Card', () => { "pressure": 1007, "wind_speed": 4.67, "wind_bearing": 'WSW', - "condition": "cloudy", + "condition": "cloudy" as Condition, "clouds": 60, "temperature": 84 }, @@ -98,7 +100,7 @@ describe('Card', () => { "pressure": 1007, "wind_speed": 4.67, "wind_bearing": 'WSW', - "condition": "cloudy", + "condition": "cloudy" as Condition, "clouds": 60, "temperature": 84 } diff --git a/cypress/e2e/weather-bar.cy.ts b/cypress/e2e/weather-bar.cy.ts index 16ec67b..3921de9 100644 --- a/cypress/e2e/weather-bar.cy.ts +++ b/cypress/e2e/weather-bar.cy.ts @@ -212,6 +212,32 @@ describe('Weather bar', () => { expect(cs.width).to.not.eq('0px'); }); }); + it('uses custom icons when specified', () => { + const expectedIcons = [ + 'mdi:customIcon1', + 'foo:bar', + 'mdi:weather-sunny', + 'mdi:weather-night' + ]; + cy.configure({ + icons: true, + icon_map: { + cloudy: 'mdi:customIcon1', + "partlycloudy": "foo:bar" + } + }); + cy.get('weather-bar') + .shadow() + .find('div.bar > div > span.condition-icon') + .should('have.length', 4) + .find('ha-icon') + .each((el, i) => { + cy.wrap(el).invoke('attr', 'icon') + .should('exist') + .and('eq', expectedIcons[i]); + }); + }); + }); describe('Icon fill', () => { function verifyIcons (cy, expectedIcons) { diff --git a/src/hourly-weather.ts b/src/hourly-weather.ts index 38a2e71..177bcb2 100644 --- a/src/hourly-weather.ts +++ b/src/hourly-weather.ts @@ -33,6 +33,7 @@ import type { SegmentPrecipitation, SegmentTemperature, SegmentWind, + Condition } from './types'; import { actionHandler } from './action-handler-directive'; import { version } from '../package.json'; @@ -408,6 +409,7 @@ export class HourlyWeatherCard extends LitElement { .wind=${wind} .precipitation=${precipitation} .icons=${!!config.icons} + .icon_map=${config.icon_map} .colors=${colorSettings.validColors} .hide_hours=${!!config.hide_hours} .hide_temperatures=${!!config.hide_temperatures} @@ -426,11 +428,11 @@ export class HourlyWeatherCard extends LitElement { } private getConditionListFromForecast(forecast: ForecastSegment[], numSegments: number, offset: number): ConditionSpan[] { - let lastCond: string = forecast[offset].condition; + let lastCond: Condition = forecast[offset].condition; let j = 0; const res: ConditionSpan[] = [[lastCond, 1]]; for (let i = offset + 1; i < numSegments + offset; i++) { - const cond: string = forecast[i].condition; + const cond: Condition = forecast[i].condition; if (cond === lastCond) { res[j][1]++; } else { diff --git a/src/types.ts b/src/types.ts index 8e2ef80..7a77a6c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -10,6 +10,9 @@ declare global { export type WindType = 'true' | 'false' | 'speed' | 'direction' | 'barb' | 'barb-and-speed' | 'barb-and-direction' | 'barb-speed-and-direction'; export type ShowDateType = 'false' | 'boundary' | 'all'; export type IconFillType = 'single' | 'full' | number; +export type Condition = 'clear-night' | 'cloudy' | 'fog' | 'hail' | 'lightning' | 'lightning-rainy' | 'partlycloudy' | 'pouring' | 'rainy' | 'snowy' | 'snowy-rainy' | 'sunny' | 'windy' | 'windy-variant' | 'exceptional'; +type PerConditionConfig = Partial>; +export type IconMap = PerConditionConfig; export interface HourlyWeatherCardConfig extends LovelaceCardConfig { type: string; @@ -20,6 +23,7 @@ export interface HourlyWeatherCardConfig extends LovelaceCardConfig { forecast_type?: ForecastType; name?: string; icons?: boolean; + icon_map?: IconMap; offset?: string; // number colors?: ColorConfig; hide_bar?: boolean; @@ -47,27 +51,11 @@ export interface ColorObject { export type ColorDefinition = string | ColorObject; -export interface ColorConfig { - 'clear-night'?: ColorDefinition; - 'cloudy'?: ColorDefinition; - 'fog'?: ColorDefinition; - 'hail'?: ColorDefinition; - 'lightning'?: ColorDefinition; - 'lightning-rainy'?: ColorDefinition; - 'partlycloudy'?: ColorDefinition; - 'pouring'?: ColorDefinition; - 'rainy'?: ColorDefinition; - 'snowy'?: ColorDefinition; - 'snowy-rainy'?: ColorDefinition; - 'sunny'?: ColorDefinition; - 'windy'?: ColorDefinition; - 'windy-variant'?: ColorDefinition; - 'exceptional'?: ColorDefinition; -} +export type ColorConfig = PerConditionConfig; export interface ForecastSegment { clouds: number; // 100 - condition: string; // "cloudy" + condition: Condition; // "cloudy" datetime: string; // "2022-06-03T22:00:00+00:00" precipitation: number; // 0 precipitation_probability: number; // 85 @@ -78,7 +66,7 @@ export interface ForecastSegment { } export type ConditionSpan = [ - condition: string, + condition: Condition, span: number ] diff --git a/src/weather-bar.ts b/src/weather-bar.ts index 653042a..bf7b8d1 100644 --- a/src/weather-bar.ts +++ b/src/weather-bar.ts @@ -4,7 +4,7 @@ import { StyleInfo, styleMap } from 'lit/directives/style-map.js'; import tippy, { Instance } from 'tippy.js'; import { LABELS, ICONS } from "./conditions"; import { getWindBarbSVG } from "./lib/svg-wind-barbs"; -import type { ColorMap, ConditionSpan, SegmentTemperature, SegmentWind, SegmentPrecipitation, WindType, ShowDateType, IconFillType } from "./types"; +import type { ColorMap, ConditionSpan, SegmentTemperature, SegmentWind, SegmentPrecipitation, WindType, ShowDateType, IconFillType, IconMap } from "./types"; const tippyStyles: string = process.env.TIPPY_CSS!; @@ -24,6 +24,9 @@ export class WeatherBar extends LitElement { @property({ type: Boolean }) icons = false; + @property({ type: Object }) + icon_map: IconMap | undefined = void 0; + @property({ attribute: false }) colors: ColorMap | undefined = void 0; @@ -68,9 +71,13 @@ export class WeatherBar extends LitElement { if (!this.hide_bar) { for (const cond of this.conditions) { const label = this.labels[cond[0]]; - let icon = ICONS[cond[0]]; - if (icon === cond[0]) icon = 'mdi:weather-' + icon; - else icon = 'mdi:' + icon; + + let icon: string | undefined = this.icon_map?.[cond[0]]; + if (!icon) { + icon = ICONS[cond[0]]; + if (icon === cond[0]) icon = 'mdi:weather-' + icon; + else icon = 'mdi:' + icon; + } const iconMarkup: TemplateResult[] = []; if (!this.icons) {